From 6068d3e870df3069642ad47761b7705f4218ea3e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Aug 2024 18:24:10 +0100 Subject: [PATCH 001/979] crypto: Pull out `Session::build_encrypted_event` Break up `encrypt` a bit, so that we can write tests that do something slightly different. --- crates/matrix-sdk-crypto/src/olm/session.rs | 22 ++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/olm/session.rs b/crates/matrix-sdk-crypto/src/olm/session.rs index b0a3aa645ff..939c488e74d 100644 --- a/crates/matrix-sdk-crypto/src/olm/session.rs +++ b/crates/matrix-sdk-crypto/src/olm/session.rs @@ -127,7 +127,7 @@ impl Session { message } - /// Encrypt the given event content content as an m.room.encrypted event + /// Encrypt the given event content as an m.room.encrypted event /// content. /// /// # Arguments @@ -170,6 +170,24 @@ impl Session { let ciphertext = self.encrypt_helper(&plaintext).await; + let content = self.build_encrypted_event(ciphertext, message_id).await?; + let content = Raw::new(&content)?; + Ok(content) + } + + /// Take the given ciphertext, and package it into an `m.room.encrypted` + /// to-device message content. + /// + /// # Arguments + /// + /// * `ciphertext` - The encrypted message content. + /// * `message_id` - The ID to use for this to-device message, as + /// `org.matrix.msgid`. + pub(crate) async fn build_encrypted_event( + &self, + ciphertext: OlmMessage, + message_id: Option, + ) -> OlmResult { let content = match self.algorithm().await { EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => OlmV1Curve25519AesSha2Content { ciphertext, @@ -194,8 +212,6 @@ impl Session { _ => unreachable!(), }; - let content = Raw::new(&content)?; - Ok(content) } From f8dd5c76d2eb8aa36afea19a36520829585826ab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 9 Aug 2024 12:26:20 +0100 Subject: [PATCH 002/979] crypto: Add tests for sender_data after receiving megolm sessions --- .../src/machine/test_helpers.rs | 4 + .../src/machine/tests/megolm_sender_data.rs | 229 ++++++++++++++++++ .../src/machine/tests/mod.rs | 1 + 3 files changed, 234 insertions(+) create mode 100644 crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index e6230546ae9..4d0c9059809 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -110,6 +110,8 @@ pub async fn get_machine_pair( (alice, bob, otk) } +/// Return a pair of [`OlmMachine`]s, with an olm session created on Alice's +/// side, but with no message yet sent. pub async fn get_machine_pair_with_session( alice: &UserId, bob: &UserId, @@ -133,6 +135,8 @@ pub async fn get_machine_pair_with_session( (alice, bob) } +/// Return a pair of [`OlmMachine`]s, with an olm session (initiated +/// by Alice) established between the two. pub async fn get_machine_pair_with_setup_sessions_test_helper( alice: &UserId, bob: &UserId, diff --git a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs new file mode 100644 index 00000000000..2ac6602e4e2 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs @@ -0,0 +1,229 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use std::{fmt::Debug, iter, pin::Pin}; + +use assert_matches::assert_matches; +use futures_core::Stream; +use futures_util::{FutureExt, StreamExt}; +use matrix_sdk_test::async_test; +use ruma::{room_id, user_id, RoomId, TransactionId, UserId}; +use serde::Serialize; +use serde_json::json; + +use crate::{ + machine::{ + test_helpers::get_machine_pair_with_setup_sessions_test_helper, + tests::to_device_requests_to_content, + }, + olm::{InboundGroupSession, SenderData}, + store::RoomKeyInfo, + types::events::{room::encrypted::ToDeviceEncryptedEventContent, EventType, ToDeviceEvent}, + EncryptionSettings, EncryptionSyncChanges, OlmMachine, Session, +}; + +/// Test the behaviour when a megolm session is received from an unknown device, +/// and the device keys are not in the to-device message. +#[async_test] +async fn test_receive_megolm_session_from_unknown_device() { + // Given Bob does not know about Alice's device + let (alice, bob) = get_machine_pair().await; + let mut bob_room_keys_received_stream = Box::pin(bob.store().room_keys_received_stream()); + + // `get_machine_pair_with_setup_sessions_test_helper` tells Bob about Alice's + // device keys, so to run this test, we need to make him forget them. + forget_devices_for_user(&bob, alice.user_id()).await; + + // When Alice starts a megolm session and shares the key with Bob, *without* + // sending the sender data. + let room_id = room_id!("!test:example.org"); + let event = create_and_share_session_without_sender_data(&alice, &bob, room_id).await; + + // Bob receives the to-device message + receive_to_device_event(&bob, &event).await; + + // Then Bob should know about the session, and it should have + // `SenderData::UnknownDevice`. + let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); + let session = get_inbound_group_session_or_panic(&bob, &room_key_info).await; + + assert_matches!( + session.sender_data, + SenderData::UnknownDevice {legacy_session, owner_check_failed} => { + assert!(legacy_session); // TODO: change when https://github.com/matrix-org/matrix-rust-sdk/pull/3785 lands + assert!(!owner_check_failed); + } + ); +} + +/// Test the behaviour when a megolm session is received from a known, but +/// unsigned, device. +#[async_test] +async fn test_receive_megolm_session_from_known_device() { + // Given Bob knows about Alice's device + let (alice, bob) = get_machine_pair().await; + let mut bob_room_keys_received_stream = Box::pin(bob.store().room_keys_received_stream()); + + // When Alice shares a room key with Bob + let room_id = room_id!("!test:example.org"); + let event = ToDeviceEvent::new( + alice.user_id().to_owned(), + to_device_requests_to_content( + alice + .share_room_key(room_id, iter::once(bob.user_id()), EncryptionSettings::default()) + .await + .unwrap(), + ), + ); + + // Bob receives the to-device message + receive_to_device_event(&bob, &event).await; + + // Then Bob should know about the session, and it should have + // `SenderData::DeviceInfo` + let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); + let session = get_inbound_group_session_or_panic(&bob, &room_key_info).await; + + assert_matches!( + session.sender_data, + SenderData::DeviceInfo {legacy_session, ..} => { + assert!(legacy_session); // TODO: change when https://github.com/matrix-org/matrix-rust-sdk/pull/3785 lands + } + ); +} + +/// Convenience wrapper for [`get_machine_pair_with_setup_sessions_test_helper`] +/// using standard user ids. +async fn get_machine_pair() -> (OlmMachine, OlmMachine) { + get_machine_pair_with_setup_sessions_test_helper( + user_id!("@alice:example.org"), + user_id!("@bob:example.com"), + false, + ) + .await +} + +/// Tell the given [`OlmMachine`] to forget about any keys it has seen for the +/// given user. +async fn forget_devices_for_user(machine: &OlmMachine, other_user: &UserId) { + let mut keys_query_response = ruma::api::client::keys::get_keys::v3::Response::default(); + keys_query_response.device_keys.insert(other_user.to_owned(), Default::default()); + machine.receive_keys_query_response(&TransactionId::new(), &keys_query_response).await.unwrap(); +} + +/// Create a new [`OutboundGroupSession`], and build a to-device event to share +/// it with another [`OlmMachine`], *without* sending the MSC4147 sender data. +/// +/// # Arguments +/// +/// * `alice` - sending device. +/// * `bob` - receiving device. +/// * `room_id` - room to create a session for. +async fn create_and_share_session_without_sender_data( + alice: &OlmMachine, + bob: &OlmMachine, + room_id: &RoomId, +) -> ToDeviceEvent { + let (outbound_session, _) = alice + .inner + .group_session_manager + .get_or_create_outbound_session( + room_id, + EncryptionSettings::default(), + SenderData::unknown(), + ) + .await + .unwrap(); + + // In future, we might want to save the session to the store, to better match + // the behaviour of the real implementation. See + // `GroupSessionManager::share_room_key` for inspiration on how to do that. + + let olm_sessions = alice + .store() + .get_sessions(&bob.identity_keys().curve25519.to_base64()) + .await + .unwrap() + .unwrap(); + let mut olm_session: Session = olm_sessions.lock().await[0].clone(); + + let room_key_content = outbound_session.as_content().await; + let plaintext = serde_json::to_string(&json!({ + "sender": alice.user_id(), + "sender_device": alice.device_id(), + "keys": { "ed25519": alice.identity_keys().ed25519.to_base64() }, + // We deliberately do *not* include: + // "org.matrix.msc4147.device_keys": alice_device_keys, + "recipient": bob.user_id(), + "recipient_keys": { "ed25519": bob.identity_keys().ed25519.to_base64() }, + "type": room_key_content.event_type(), + "content": room_key_content, + })) + .unwrap(); + + let ciphertext = olm_session.encrypt_helper(&plaintext).await; + ToDeviceEvent::new( + alice.user_id().to_owned(), + olm_session.build_encrypted_event(ciphertext, None).await.unwrap(), + ) +} + +/// Pipe a to-device event into an [`OlmMachine`]. +async fn receive_to_device_event(machine: &OlmMachine, event: &ToDeviceEvent) +where + C: EventType + Serialize + Debug, +{ + let event_json = serde_json::to_string(event).expect("Unable to serialize to-device message"); + + machine + .receive_sync_changes(EncryptionSyncChanges { + to_device_events: vec![serde_json::from_str(&event_json).unwrap()], + changed_devices: &Default::default(), + one_time_keys_counts: &Default::default(), + unused_fallback_keys: None, + next_batch_token: None, + }) + .await + .expect("Error receiving to-device event"); +} + +/// Given the `room_keys_received_stream`, check that there is a pending update, +/// and pop it. +fn get_room_key_received_update( + room_keys_received_stream: &mut Pin>>>, +) -> RoomKeyInfo { + room_keys_received_stream + .next() + .now_or_never() + .flatten() + .expect("We should have received an update of room key infos") + .pop() + .expect("Received an empty room key info update") +} + +/// Load the inbound group session corresponding to an update from the +/// `room_keys_received_stream` from the given machine's store. +async fn get_inbound_group_session_or_panic( + machine: &OlmMachine, + room_key_info: &RoomKeyInfo, +) -> InboundGroupSession { + machine + .store() + .get_inbound_group_session(&room_key_info.room_id, &room_key_info.session_id) + .await + .expect("Error loading inbound group session") + .expect("Inbound group session not found") +} diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index e4a1c5730b3..a9351ed84a7 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -76,6 +76,7 @@ use crate::{ mod decryption_verification_state; mod interactive_verification; +mod megolm_sender_data; mod olm_encryption; mod room_settings; mod send_encrypted_to_device; From 7807ed8bda7b771a752f66b570b3b7ea2f3cec8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 2 Sep 2024 15:28:05 +0200 Subject: [PATCH 003/979] sqlite: Update last access time first to force write transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoids errors when the read transaction tries to upgrade to a write transaction. Signed-off-by: Kévin Commaille --- .../src/event_cache_store.rs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index c45e60992b1..360feff3be1 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -162,25 +162,20 @@ impl EventCacheStore for SqliteEventCacheStore { let conn = self.acquire().await?; let data = conn .with_transaction::<_, rusqlite::Error, _>(move |txn| { - let Some(media) = txn - .query_row::, _, _>( - "SELECT data FROM media WHERE uri = ? AND format = ?", - (&uri, &format), - |row| row.get(0), - ) - .optional()? - else { - return Ok(None); - }; - // Update the last access. + // We need to do this first so the transaction is in write mode right away. txn.execute( "UPDATE media SET last_access = CAST(strftime('%s') as INT) \ WHERE uri = ? AND format = ?", - (uri, format), + (&uri, &format), )?; - Ok(Some(media)) + txn.query_row::, _, _>( + "SELECT data FROM media WHERE uri = ? AND format = ?", + (&uri, &format), + |row| row.get(0), + ) + .optional() }) .await?; From 7f4e79e2a3bb07fa189c62987a06b3f78f2e4286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 2 Sep 2024 16:07:50 +0200 Subject: [PATCH 004/979] Add link to SQLite docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk-sqlite/src/event_cache_store.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 360feff3be1..51afb9e5deb 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -164,6 +164,7 @@ impl EventCacheStore for SqliteEventCacheStore { .with_transaction::<_, rusqlite::Error, _>(move |txn| { // Update the last access. // We need to do this first so the transaction is in write mode right away. + // See: https://sqlite.org/lang_transaction.html#read_transactions_versus_write_transactions txn.execute( "UPDATE media SET last_access = CAST(strftime('%s') as INT) \ WHERE uri = ? AND format = ?", From 3f408a9a363d8b0782a3d8c3c3fbe6cb57cb71ac Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 2 Aug 2024 11:17:37 +0100 Subject: [PATCH 005/979] crypto: Create a SenderDataType enum --- .../src/olm/group_sessions/inbound.rs | 11 +++++-- .../src/olm/group_sessions/mod.rs | 2 +- .../src/olm/group_sessions/sender_data.rs | 30 +++++++++++++++++++ crates/matrix-sdk-crypto/src/olm/mod.rs | 2 +- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index 80f28a6888b..cf17841a093 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -36,8 +36,8 @@ use vodozemac::{ }; use super::{ - BackedUpRoomKey, ExportedRoomKey, OutboundGroupSession, SenderData, SessionCreationError, - SessionKey, + BackedUpRoomKey, ExportedRoomKey, OutboundGroupSession, SenderData, SenderDataType, + SessionCreationError, SessionKey, }; use crate::{ error::{EventError, MegolmResult}, @@ -477,6 +477,13 @@ impl InboundGroupSession { pub(crate) fn mark_as_imported(&mut self) { self.imported = true; } + + /// Return the [`SenderDataType`] of our [`SenderData`]. This is used during + /// serialization, to allow us to store the type in a separate queryable + /// column/property. + pub fn sender_data_type(&self) -> SenderDataType { + self.sender_data.to_type() + } } #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs index ba06962cb53..55eee23d74b 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs @@ -25,7 +25,7 @@ pub(crate) use outbound::ShareState; pub use outbound::{ EncryptionSettings, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo, }; -pub use sender_data::{KnownSenderData, SenderData}; +pub use sender_data::{KnownSenderData, SenderData, SenderDataType}; pub(crate) use sender_data_finder::SenderDataFinder; use thiserror::Error; pub use vodozemac::megolm::{ExportedSessionKey, SessionKey}; diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index 989eb0fbb5d..ad509f924bd 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -191,6 +191,19 @@ impl SenderData { SenderData::SenderVerified(..) => 4, } } + + /// Return our type as a [`SenderDataType`]. + pub fn to_type(&self) -> SenderDataType { + match self { + Self::UnknownDevice { .. } => SenderDataType::UnknownDevice, + Self::DeviceInfo { .. } => SenderDataType::DeviceInfo, + Self::SenderUnverifiedButPreviouslyVerified { .. } => { + SenderDataType::SenderUnverifiedButPreviouslyVerified + } + Self::SenderUnverified { .. } => SenderDataType::SenderUnverified, + Self::SenderVerified { .. } => SenderDataType::SenderVerified, + } + } } /// Used when deserialising and the sender_data property is missing. @@ -266,6 +279,23 @@ impl From for SenderData { } } +/// Used when serializing [`crate::olm::group_sessions::InboundGroupSession`]s. +/// We want just the type of the session's [`SenderData`] to be queryable, so we +/// store the type as a separate column/property in the database. +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum SenderDataType { + /// The [`SenderData`] is of type `UnknownDevice`. + UnknownDevice = 1, + /// The [`SenderData`] is of type `DeviceInfo`. + DeviceInfo = 2, + /// The [`SenderData`] is of type `SenderUnverifiedButPreviouslyVerified`. + SenderUnverifiedButPreviouslyVerified = 3, + /// The [`SenderData`] is of type `SenderUnverified`. + SenderUnverified = 4, + /// The [`SenderData`] is of type `SenderVerified`. + SenderVerified = 5, +} + #[cfg(test)] mod tests { use std::{cmp::Ordering, collections::BTreeMap}; diff --git a/crates/matrix-sdk-crypto/src/olm/mod.rs b/crates/matrix-sdk-crypto/src/olm/mod.rs index c5882c48ffe..2f47d65eea6 100644 --- a/crates/matrix-sdk-crypto/src/olm/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/mod.rs @@ -28,7 +28,7 @@ pub(crate) use account::{OlmDecryptionInfo, SessionType}; pub use group_sessions::{ BackedUpRoomKey, EncryptionSettings, ExportedRoomKey, InboundGroupSession, KnownSenderData, OutboundGroupSession, PickledInboundGroupSession, PickledOutboundGroupSession, SenderData, - SessionCreationError, SessionExportError, SessionKey, ShareInfo, + SenderDataType, SessionCreationError, SessionExportError, SessionKey, ShareInfo, }; pub(crate) use group_sessions::{SenderDataFinder, ShareState}; pub use session::{PickledSession, Session}; From 228a117ccb692503af25f117afe59ea0260d4642 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 27 Aug 2024 22:23:22 +0100 Subject: [PATCH 006/979] crypto: add `get_inbound_group_sessions_for_device_batch` to CryptoStore --- .../src/store/memorystore.rs | 33 ++++++++++- crates/matrix-sdk-crypto/src/store/traits.rs | 57 ++++++++++++++++++- .../src/crypto_store/mod.rs | 16 +++++- crates/matrix-sdk-sqlite/src/crypto_store.rs | 13 ++++- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index fe378a84099..f0d782896f6 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -26,6 +26,7 @@ use ruma::{ }; use tokio::sync::RwLock; use tracing::warn; +use vodozemac::Curve25519PublicKey; use super::{ caches::{DeviceStore, GroupSessionStore}, @@ -35,7 +36,7 @@ use super::{ use crate::{ gossiping::{GossipRequest, GossippedSecret, SecretInfo}, identities::{DeviceData, UserIdentityData}, - olm::{OutboundGroupSession, PrivateCrossSigningIdentity}, + olm::{OutboundGroupSession, PrivateCrossSigningIdentity, SenderDataType}, types::events::room_key_withheld::RoomKeyWithheldEvent, TrackedUser, }; @@ -380,6 +381,16 @@ impl CryptoStore for MemoryStore { Ok(RoomKeyCounts { total: self.inbound_group_sessions.count(), backed_up }) } + async fn get_inbound_group_sessions_for_device_batch( + &self, + sender_key: Curve25519PublicKey, + sender_data_type: SenderDataType, + after_session_id: Option, + limit: usize, + ) -> Result> { + todo!() + } + async fn inbound_group_sessions_for_backup( &self, backup_version: &str, @@ -1102,13 +1113,14 @@ mod integration_tests { use ruma::{ events::secret::request::SecretName, DeviceId, OwnedDeviceId, RoomId, TransactionId, UserId, }; + use vodozemac::Curve25519PublicKey; use super::MemoryStore; use crate::{ cryptostore_integration_tests, cryptostore_integration_tests_time, olm::{ InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, - StaticAccountData, + SenderDataType, StaticAccountData, }, store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings}, types::events::room_key_withheld::RoomKeyWithheldEvent, @@ -1230,6 +1242,23 @@ mod integration_tests { self.0.inbound_group_session_counts(backup_version).await } + async fn get_inbound_group_sessions_for_device_batch( + &self, + sender_key: Curve25519PublicKey, + sender_data_type: SenderDataType, + after_session_id: Option, + limit: usize, + ) -> Result, Self::Error> { + self.0 + .get_inbound_group_sessions_for_device_batch( + sender_key, + sender_data_type, + after_session_id, + limit, + ) + .await + } + async fn inbound_group_sessions_for_backup( &self, backup_version: &str, diff --git a/crates/matrix-sdk-crypto/src/store/traits.rs b/crates/matrix-sdk-crypto/src/store/traits.rs index 1e6de4e295a..3031e33ef8d 100644 --- a/crates/matrix-sdk-crypto/src/store/traits.rs +++ b/crates/matrix-sdk-crypto/src/store/traits.rs @@ -19,14 +19,17 @@ use matrix_sdk_common::AsyncTraitDeps; use ruma::{ events::secret::request::SecretName, DeviceId, OwnedDeviceId, RoomId, TransactionId, UserId, }; +use vodozemac::Curve25519PublicKey; use super::{ BackupKeys, Changes, CryptoStoreError, PendingChanges, Result, RoomKeyCounts, RoomSettings, }; +#[cfg(doc)] +use crate::olm::SenderData; use crate::{ olm::{ InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, - Session, + SenderDataType, Session, }, types::events::room_key_withheld::RoomKeyWithheldEvent, Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData, @@ -121,6 +124,40 @@ pub trait CryptoStore: AsyncTraitDeps { backup_version: Option<&str>, ) -> Result; + /// Get a batch of inbound group sessions for the device with the supplied + /// curve key, whose sender data is of the supplied type. + /// + /// Sessions are not necessarily returned in any specific order, but the + /// returned batches are consistent: if this function is called repeatedly + /// with `after_session_id` set to the session ID from the last result + /// from the previous call, until an empty result is returned, then + /// eventually all matching sessions are returned. (New sessions that are + /// added in the course of iteration may or may not be returned.) + /// + /// This function is used when the device information is updated via a + /// `/keys/query` response and we want to update the sender data based + /// on the new information. + /// + /// # Arguments + /// + /// * `curve_key` - only return sessions created by the device with this + /// curve key. + /// + /// * `sender_data_type` - only return sessions whose [`SenderData`] record + /// is in this state. + /// + /// * `after_session_id` - return the sessions after this id, or start at + /// the earliest if this is None. + /// + /// * `limit` - return a maximum of this many sessions. + async fn get_inbound_group_sessions_for_device_batch( + &self, + curve_key: Curve25519PublicKey, + sender_data_type: SenderDataType, + after_session_id: Option, + limit: usize, + ) -> Result, Self::Error>; + /// Return a batch of ['InboundGroupSession'] ("room keys") that have not /// yet been backed up in the supplied backup version. /// @@ -377,6 +414,24 @@ impl CryptoStore for EraseCryptoStoreError { self.0.get_inbound_group_sessions().await.map_err(Into::into) } + async fn get_inbound_group_sessions_for_device_batch( + &self, + curve_key: Curve25519PublicKey, + sender_data_type: SenderDataType, + after_session_id: Option, + limit: usize, + ) -> Result> { + self.0 + .get_inbound_group_sessions_for_device_batch( + curve_key, + sender_data_type, + after_session_id, + limit, + ) + .await + .map_err(Into::into) + } + async fn inbound_group_session_counts( &self, backup_version: Option<&str>, diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 80ec5ddab9a..62cc4ee5c62 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -21,10 +21,12 @@ use async_trait::async_trait; use gloo_utils::format::JsValueSerdeExt; use hkdf::Hkdf; use indexed_db_futures::prelude::*; +use js_sys::Array; use matrix_sdk_crypto::{ olm::{ - InboundGroupSession, OlmMessageHash, OutboundGroupSession, PickledInboundGroupSession, - PrivateCrossSigningIdentity, Session, StaticAccountData, + Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession, + PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session, + StaticAccountData, }, store::{ BackupKeys, Changes, CryptoStore, CryptoStoreError, PendingChanges, RoomKeyCounts, @@ -938,6 +940,16 @@ impl_crypto_store! { ).await } + async fn get_inbound_group_sessions_for_device_batch( + &self, + sender_key: Curve25519PublicKey, + sender_data_type: SenderDataType, + after_session_id: Option, + limit: usize, + ) -> Result> { + todo!(); + } + async fn inbound_group_session_counts(&self, _backup_version: Option<&str>) -> Result { let tx = self .inner diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index af0b3be70da..7a6dbea6c59 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -25,7 +25,7 @@ use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_crypto::{ olm::{ InboundGroupSession, OutboundGroupSession, PickledInboundGroupSession, - PrivateCrossSigningIdentity, Session, StaticAccountData, + PrivateCrossSigningIdentity, SenderDataType, Session, StaticAccountData, }, store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings}, types::events::room_key_withheld::RoomKeyWithheldEvent, @@ -40,6 +40,7 @@ use rusqlite::{params_from_iter, OptionalExtension}; use serde::{de::DeserializeOwned, Serialize}; use tokio::{fs, sync::Mutex}; use tracing::{debug, instrument, warn}; +use vodozemac::Curve25519PublicKey; use crate::{ error::{Error, Result}, @@ -947,6 +948,16 @@ impl CryptoStore for SqliteCryptoStore { .collect() } + async fn get_inbound_group_sessions_for_device_batch( + &self, + sender_key: Curve25519PublicKey, + sender_data_type: SenderDataType, + after_session_id: Option, + limit: usize, + ) -> Result, Self::Error> { + todo!() + } + async fn inbound_group_session_counts( &self, backup_version: Option<&str>, From eeaf31ce530577de4a4a3dee4f08e75d6fdc83a1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 27 Aug 2024 22:28:31 +0100 Subject: [PATCH 007/979] crypto: implement MemoryStore::get_inbound_group_sessions_for_device_batch --- .../src/store/memorystore.rs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index f0d782896f6..1a8b00ffca1 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -388,7 +388,39 @@ impl CryptoStore for MemoryStore { after_session_id: Option, limit: usize, ) -> Result> { - todo!() + // First, find all InboundGroupSessions, filtering for those that match the + // device and sender_data type. + let mut sessions: Vec<_> = self + .get_inbound_group_sessions() + .await? + .into_iter() + .filter(|session: &InboundGroupSession| { + session.creator_info.curve25519_key == sender_key + && session.sender_data.to_type() == sender_data_type + }) + .collect(); + + // Then, sort the sessions in order of ascending session ID... + sessions.sort_by_key(|s| s.session_id().to_owned()); + + // Figure out where in the array to start returning results from + let start_index = { + match after_session_id { + None => 0, + Some(id) => { + let idx = sessions + .iter() + .position(|session| session.session_id() == id) + .map(|idx| idx + 1); + + // If `after_session_id` was not found in the array, go to the end of the array + idx.unwrap_or(sessions.len()) + } + } + }; + + // Return up to `limit` items from the array, starting from `start_index` + Ok(sessions.drain(start_index..).take(limit).collect()) } async fn inbound_group_sessions_for_backup( From 12653fb2b675a88063078cc6df2b8e6ce3680689 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 27 Aug 2024 22:39:46 +0100 Subject: [PATCH 008/979] sqlite: add new curve_key and sender_data_type columns --- ...up_session_sender_key_sender_data_type.sql | 12 +++++++ crates/matrix-sdk-sqlite/src/crypto_store.rs | 32 +++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 crates/matrix-sdk-sqlite/migrations/crypto_store/009_inbound_group_session_sender_key_sender_data_type.sql diff --git a/crates/matrix-sdk-sqlite/migrations/crypto_store/009_inbound_group_session_sender_key_sender_data_type.sql b/crates/matrix-sdk-sqlite/migrations/crypto_store/009_inbound_group_session_sender_key_sender_data_type.sql new file mode 100644 index 00000000000..0392cf79e01 --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/crypto_store/009_inbound_group_session_sender_key_sender_data_type.sql @@ -0,0 +1,12 @@ +ALTER TABLE "inbound_group_session" + ADD COLUMN "sender_key" BLOB; + +ALTER TABLE "inbound_group_session" + ADD COLUMN "sender_data_type" INTEGER; + +-- Create an index on sender curve25519 key and sender_data type, to help with +-- `get_inbound_group_sessions_for_device_batch`. +-- +-- `session_id` is included so that the results are sorted by (hashed) session id. +CREATE INDEX "inbound_group_session_sender_key_sender_data_type_idx" + ON "inbound_group_session" ("sender_key", "sender_data_type", "session_id"); diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 7a6dbea6c59..0b192340afe 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -187,7 +187,7 @@ impl SqliteCryptoStore { } } -const DATABASE_VERSION: u8 = 8; +const DATABASE_VERSION: u8 = 9; /// Run migrations for the given version of the database. async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { @@ -270,6 +270,16 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { .await?; } + if version < 9 { + conn.with_transaction(|txn| { + txn.execute_batch(include_str!( + "../migrations/crypto_store/009_inbound_group_session_sender_key_sender_data_type.sql" + ))?; + txn.set_db_version(9) + }) + .await?; + } + Ok(()) } @@ -287,6 +297,8 @@ trait SqliteConnectionExt { session_id: &[u8], data: &[u8], backed_up: bool, + sender_key: Option<&[u8]>, + sender_data_type: Option, ) -> rusqlite::Result<()>; fn set_outbound_group_session(&self, room_id: &[u8], data: &[u8]) -> rusqlite::Result<()>; @@ -339,12 +351,14 @@ impl SqliteConnectionExt for rusqlite::Connection { session_id: &[u8], data: &[u8], backed_up: bool, + sender_key: Option<&[u8]>, + sender_data_type: Option, ) -> rusqlite::Result<()> { self.execute( - "INSERT INTO inbound_group_session (session_id, room_id, data, backed_up) \ - VALUES (?1, ?2, ?3, ?4) - ON CONFLICT (session_id) DO UPDATE SET data = ?3, backed_up = ?4", - (session_id, room_id, data, backed_up), + "INSERT INTO inbound_group_session (session_id, room_id, data, backed_up, sender_key, sender_data_type) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT (session_id) DO UPDATE SET data = ?3, backed_up = ?4, sender_key = ?5, sender_data_type = ?6", + (session_id, room_id, data, backed_up, sender_key, sender_data_type), )?; Ok(()) } @@ -757,7 +771,9 @@ impl CryptoStore for SqliteCryptoStore { let room_id = self.encode_key("inbound_group_session", session.room_id().as_bytes()); let session_id = self.encode_key("inbound_group_session", session.session_id()); let pickle = session.pickle().await; - inbound_session_changes.push((room_id, session_id, pickle)); + let sender_key = + self.encode_key("inbound_group_session", session.sender_key().to_base64()); + inbound_session_changes.push((room_id, session_id, pickle, sender_key)); } let mut outbound_session_changes = Vec::new(); @@ -816,13 +832,15 @@ impl CryptoStore for SqliteCryptoStore { txn.set_session(session_id, sender_key, &serialized_session)?; } - for (room_id, session_id, pickle) in &inbound_session_changes { + for (room_id, session_id, pickle, sender_key) in &inbound_session_changes { let serialized_session = this.serialize_value(&pickle)?; txn.set_inbound_group_session( room_id, session_id, &serialized_session, pickle.backed_up, + Some(sender_key), + Some(pickle.sender_data.to_type() as u8), )?; } From 7bcc920514f5181c362f29044a7df0a1895b157e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 27 Aug 2024 22:40:47 +0100 Subject: [PATCH 009/979] sqlite: add `get_inbound_group_sessions_for_device_batch` --- crates/matrix-sdk-sqlite/src/crypto_store.rs | 59 +++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 0b192340afe..b16a05e1800 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -36,7 +36,7 @@ use ruma::{ events::secret::request::SecretName, DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, RoomId, TransactionId, UserId, }; -use rusqlite::{params_from_iter, OptionalExtension}; +use rusqlite::{named_params, params_from_iter, OptionalExtension}; use serde::{de::DeserializeOwned, Serialize}; use tokio::{fs, sync::Mutex}; use tracing::{debug, instrument, warn}; @@ -506,6 +506,44 @@ trait SqliteObjectCryptoStoreExt: SqliteAsyncConnExt { Ok(RoomKeyCounts { total, backed_up }) } + async fn get_inbound_group_sessions_for_device_batch( + &self, + sender_key: Key, + sender_data_type: SenderDataType, + after_session_id: Option, + limit: usize, + ) -> Result, bool)>> { + Ok(self + .prepare( + " + SELECT data, backed_up + FROM inbound_group_session + WHERE sender_key = :sender_key + AND sender_data_type = :sender_data_type + AND session_id > :after_session_id + ORDER BY session_id + LIMIT :limit + ", + move |mut stmt| { + let sender_data_type = sender_data_type as u8; + + // If we are not provided with an `after_session_id`, use a key which will sort + // before all real keys: the empty string. + let after_session_id = after_session_id.unwrap_or(Key::Plain(Vec::new())); + + stmt.query(named_params! { + ":sender_key": sender_key, + ":sender_data_type": sender_data_type, + ":after_session_id": after_session_id, + ":limit": limit, + })? + .mapped(|row| Ok((row.get(0)?, row.get(1)?))) + .collect() + }, + ) + .await?) + } + async fn get_inbound_group_sessions_for_backup(&self, limit: usize) -> Result>> { Ok(self .prepare( @@ -973,7 +1011,24 @@ impl CryptoStore for SqliteCryptoStore { after_session_id: Option, limit: usize, ) -> Result, Self::Error> { - todo!() + let after_session_id = + after_session_id.map(|session_id| self.encode_key("inbound_group_session", session_id)); + let sender_key = self.encode_key("inbound_group_session", sender_key.to_base64()); + + self.acquire() + .await? + .get_inbound_group_sessions_for_device_batch( + sender_key, + sender_data_type, + after_session_id, + limit, + ) + .await? + .into_iter() + .map(|(value, backed_up)| { + self.deserialize_and_unpickle_inbound_group_session(value, backed_up) + }) + .collect() } async fn inbound_group_session_counts( From 7cf8e9eb9b22d3eb444c93bbc365aff34cacdcb6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 27 Aug 2024 22:25:14 +0100 Subject: [PATCH 010/979] indexeddb: add new fields to `InboundGroupSessionIndexedDbObject` Add new `session_id`, `sender_key` and `sender_data_type` properties to stored inbound group session objects. --- .../src/crypto_store/migrations/mod.rs | 68 ++++++++-- .../src/crypto_store/mod.rs | 128 +++++++++++++++++- 2 files changed, 183 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs index e2dac253fbc..08a67a5c6df 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs @@ -410,8 +410,11 @@ mod tests { let session_dbo = InboundGroupSessionIndexedDbObject { pickled_session: serializer.maybe_encrypt_value(pickled_session).unwrap(), + session_id: None, needs_backup: false, backed_up_to: -1, + sender_key: None, + sender_data_type: None, }; let session_js: JsValue = serde_wasm_bindgen::to_value(&session_dbo).unwrap(); @@ -477,22 +480,25 @@ mod tests { /// Test migrating `inbound_group_sessions` data from store v5 to latest, /// on a store with encryption disabled. #[async_test] - async fn test_v8_v10_migration_unencrypted() { - test_v8_v10_migration_with_cipher("test_v8_migration_unencrypted", None).await + async fn test_v8_v10_v12_migration_unencrypted() { + test_v8_v10_v12_migration_with_cipher("test_v8_migration_unencrypted", None).await } /// Test migrating `inbound_group_sessions` data from store v5 to store v8, /// on a store with encryption enabled. #[async_test] - async fn test_v8_v10_migration_encrypted() { + async fn test_v8_v10_v12_migration_encrypted() { let cipher = StoreCipher::new().unwrap(); - test_v8_v10_migration_with_cipher("test_v8_migration_encrypted", Some(Arc::new(cipher))) - .await; + test_v8_v10_v12_migration_with_cipher( + "test_v8_migration_encrypted", + Some(Arc::new(cipher)), + ) + .await; } - /// Helper function for `test_v8_v10_migration_{un,}encrypted`: test - /// migrating `inbound_group_sessions` data from store v5 to store v10. - async fn test_v8_v10_migration_with_cipher( + /// Helper function for `test_v8_v10_v12_migration_{un,}encrypted`: test + /// migrating `inbound_group_sessions` data from store v5 to store v12. + async fn test_v8_v10_v12_migration_with_cipher( db_prefix: &str, store_cipher: Option>, ) { @@ -536,13 +542,16 @@ mod tests { assert!(!fetched_not_backed_up_session.backed_up()); // For v10: they have the backed_up_to property and it is indexed - assert_matches_v10_schema(db_name, store, fetched_backed_up_session).await; + assert_matches_v10_schema(&db_name, &store, &fetched_backed_up_session).await; + + // For v12: they have the session_id, sender_key and sender_data_type properties + assert_matches_v12_schema(&db_name, &store, &fetched_backed_up_session).await; } async fn assert_matches_v10_schema( - db_name: String, - store: IndexeddbCryptoStore, - fetched_backed_up_session: InboundGroupSession, + db_name: &str, + store: &IndexeddbCryptoStore, + fetched_backed_up_session: &InboundGroupSession, ) { let db = IdbDatabase::open(&db_name).unwrap().await.unwrap(); assert!(db.version() >= 10.0); @@ -562,6 +571,41 @@ mod tests { db.close(); } + async fn assert_matches_v12_schema( + db_name: &str, + store: &IndexeddbCryptoStore, + session: &InboundGroupSession, + ) { + let db = IdbDatabase::open(&db_name).unwrap().await.unwrap(); + assert!(db.version() >= 10.0); + let transaction = db.transaction_on_one("inbound_group_sessions3").unwrap(); + let raw_store = transaction.object_store("inbound_group_sessions3").unwrap(); + let key = store + .serializer + .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (session.room_id(), session.session_id())); + let idb_object: InboundGroupSessionIndexedDbObject = + serde_wasm_bindgen::from_value(raw_store.get(&key).unwrap().await.unwrap().unwrap()) + .unwrap(); + + assert_eq!( + idb_object.session_id, + Some( + store + .serializer + .encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id()) + ) + ); + assert_eq!( + idb_object.sender_key, + Some(store.serializer.encode_key_as_string( + keys::INBOUND_GROUP_SESSIONS_V3, + session.sender_key().to_base64() + )) + ); + assert_eq!(idb_object.sender_data_type, Some(session.sender_data_type() as u8)); + db.close(); + } + fn create_sessions(room_id: &RoomId) -> (InboundGroupSession, InboundGroupSession) { let curve_key = Curve25519PublicKey::from(&Curve25519SecretKey::new()); let ed_key = Ed25519SecretKey::new().public_key(); diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 62cc4ee5c62..613641dd058 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -1601,6 +1601,16 @@ struct InboundGroupSessionIndexedDbObject { /// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`] pickled_session: MaybeEncrypted, + /// The (hashed) session ID of this session. This is somewhat redundant, but + /// we have to pull it out to its own object so that we can do batched + /// queries such as + /// [`IndexeddbStore::get_inbound_group_sessions_for_device_batch`]. + /// + /// Added in database schema v12, and lazily populated, so it is only + /// present for sessions received or modified since DB schema v12. + #[serde(default, skip_serializing_if = "Option::is_none")] + session_id: Option, + /// Whether the session data has yet to be backed up. /// /// Since we only need to be able to find entries where this is `true`, we @@ -1627,6 +1637,22 @@ struct InboundGroupSessionIndexedDbObject { /// "refer to the `needs_backup` property". See: /// https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076 backed_up_to: i32, + + /// The (hashed) curve25519 key of the device that sent us this room key, + /// base64-encoded. + /// + /// Added in database schema v12, and lazily populated, so it is only + /// present for sessions received or modified since DB schema v12. + #[serde(default, skip_serializing_if = "Option::is_none")] + sender_key: Option, + + /// The type of the [`SenderData`] within this session, converted to a u8 + /// from [`SenderDataType`]. + /// + /// Added in database schema v12, and lazily populated, so it is only + /// present for sessions received or modified since DB schema v12. + #[serde(default, skip_serializing_if = "Option::is_none")] + sender_data_type: Option, } impl InboundGroupSessionIndexedDbObject { @@ -1636,20 +1662,38 @@ impl InboundGroupSessionIndexedDbObject { session: &InboundGroupSession, serializer: &IndexeddbSerializer, ) -> Result { + let session_id = + serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id()); + + let sender_key = serializer.encode_key_as_string( + keys::INBOUND_GROUP_SESSIONS_V3, + session.sender_key().to_base64(), + ); + Ok(InboundGroupSessionIndexedDbObject { pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?, + session_id: Some(session_id), needs_backup: !session.backed_up(), backed_up_to: -1, + sender_key: Some(sender_key), + sender_data_type: Some(session.sender_data_type() as u8), }) } } #[cfg(test)] mod unit_tests { + use matrix_sdk_crypto::{ + olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey}, + types::EventEncryptionAlgorithm, + vodozemac::Ed25519Keypair, + }; use matrix_sdk_store_encryption::EncryptedValueBase64; + use matrix_sdk_test::async_test; + use ruma::{device_id, room_id, user_id}; use super::InboundGroupSessionIndexedDbObject; - use crate::crypto_store::indexeddb_serializer::MaybeEncrypted; + use crate::crypto_store::indexeddb_serializer::{IndexeddbSerializer, MaybeEncrypted}; #[test] fn needs_backup_is_serialized_as_a_u8_in_json() { @@ -1676,17 +1720,76 @@ mod unit_tests { pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject { InboundGroupSessionIndexedDbObject { pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")), + session_id: None, needs_backup, backed_up_to: -1, + sender_key: None, + sender_data_type: None, } } + + #[async_test] + async fn test_sender_key_and_sender_data_type_are_serialized_in_json() { + let sender_key = Curve25519PublicKey::from_bytes([0; 32]); + + let sender_data = SenderData::sender_verified( + user_id!("@test:user"), + device_id!("ABC"), + Ed25519Keypair::new().public_key(), + ); + + let db_object = sender_data_test_session(sender_key, sender_data).await; + let serialized = serde_json::to_string(&db_object).unwrap(); + + assert!( + serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#) + ); + assert!(serialized.contains(r#""sender_data_type":5"#)); + } + + pub async fn sender_data_test_session( + sender_key: Curve25519PublicKey, + sender_data: SenderData, + ) -> InboundGroupSessionIndexedDbObject { + let session = InboundGroupSession::new( + sender_key, + Ed25519Keypair::new().public_key(), + room_id!("!test:localhost"), + // Arbitrary session data + &SessionKey::from_base64( + "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\ + +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\ + /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\ + Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\ + 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg", + ) + .unwrap(), + sender_data, + EventEncryptionAlgorithm::MegolmV1AesSha2, + None, + ) + .unwrap(); + + InboundGroupSessionIndexedDbObject::from_session(&session, &IndexeddbSerializer::new(None)) + .await + .unwrap() + } } #[cfg(all(test, target_arch = "wasm32"))] mod wasm_unit_tests { + use std::collections::BTreeMap; + + use matrix_sdk_crypto::{ + olm::{Curve25519PublicKey, SenderData}, + types::{DeviceKeys, Signatures}, + }; use matrix_sdk_test::async_test; + use ruma::{device_id, user_id}; use wasm_bindgen::JsValue; + use crate::crypto_store::unit_tests::sender_data_test_session; + fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) { assert_eq!( js_sys::Reflect::get(&js_value, &field.into()).unwrap(), @@ -1712,6 +1815,29 @@ mod wasm_unit_tests { assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap()); } + + #[async_test] + async fn test_sender_key_and_device_type_are_serialized_in_js() { + let sender_key = Curve25519PublicKey::from_bytes([0; 32]); + + let sender_data = SenderData::device_info(DeviceKeys::new( + user_id!("@test:user").to_owned(), + device_id!("ABC").to_owned(), + vec![], + BTreeMap::new(), + Signatures::new(), + )); + let db_object = sender_data_test_session(sender_key, sender_data).await; + + let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap(); + + assert!(js_value.is_object()); + assert_field_equals(&js_value, "sender_data_type", 2); + assert_eq!( + js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(), + JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + ); + } } #[cfg(all(test, target_arch = "wasm32"))] From 675f576343cb69bce78813934ab16cb151d03950 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 29 Aug 2024 12:39:59 +0100 Subject: [PATCH 011/979] indexeddb: add new index on inbound_group_sessions Add an index on `(sender_key, sender_data_type, session_id)`. --- .../src/crypto_store/migrations/mod.rs | 15 +++++++- .../src/crypto_store/migrations/v11_to_v12.rs | 37 +++++++++++++++++++ .../src/crypto_store/mod.rs | 2 + 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs index 08a67a5c6df..b12e5055e66 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs @@ -26,6 +26,7 @@ use crate::{ mod old_keys; mod v0_to_v5; mod v10_to_v11; +mod v11_to_v12; mod v5_to_v7; mod v7; mod v7_to_v8; @@ -156,6 +157,10 @@ pub async fn open_and_upgrade_db( v10_to_v11::schema_bump(name).await?; } + if old_version < 12 { + v11_to_v12::schema_add(name).await?; + } + // If you add more migrations here, you'll need to update // `tests::EXPECTED_SCHEMA_VERSION`. @@ -260,7 +265,7 @@ mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); /// The schema version we expect after we open the store. - const EXPECTED_SCHEMA_VERSION: u32 = 11; + const EXPECTED_SCHEMA_VERSION: u32 = 12; /// Adjust this to test do a more comprehensive perf test const NUM_RECORDS_FOR_PERF: usize = 2_000; @@ -545,6 +550,7 @@ mod tests { assert_matches_v10_schema(&db_name, &store, &fetched_backed_up_session).await; // For v12: they have the session_id, sender_key and sender_data_type properties + // and they are indexed assert_matches_v12_schema(&db_name, &store, &fetched_backed_up_session).await; } @@ -577,7 +583,7 @@ mod tests { session: &InboundGroupSession, ) { let db = IdbDatabase::open(&db_name).unwrap().await.unwrap(); - assert!(db.version() >= 10.0); + assert!(db.version() >= 12.0); let transaction = db.transaction_on_one("inbound_group_sessions3").unwrap(); let raw_store = transaction.object_store("inbound_group_sessions3").unwrap(); let key = store @@ -603,6 +609,11 @@ mod tests { )) ); assert_eq!(idb_object.sender_data_type, Some(session.sender_data_type() as u8)); + assert!(raw_store + .index_names() + .find(|idx| idx == "inbound_group_session_sender_key_sender_data_type_idx") + .is_some()); + db.close(); } diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs new file mode 100644 index 00000000000..d2aa9d25930 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs @@ -0,0 +1,37 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Migration code that moves from inbound_group_sessions2 to +//! inbound_group_sessions3, shrinking the values stored in each record. + +use indexed_db_futures::IdbKeyPath; +use web_sys::DomException; + +use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; + +/// Perform the schema upgrade v11 to v12, adding an index on +/// `(curve_key, sender_data_type, session_id)` to `inbound_group_sessions3`. +pub(crate) async fn schema_add(name: &str) -> Result<(), DomException> { + do_schema_upgrade(name, 12, |_, transaction, _| { + let object_store = transaction.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?; + + object_store.create_index( + keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX, + &IdbKeyPath::str_sequence(&["sender_key", "sender_data_type", "session_id"]), + )?; + + Ok(()) + }) + .await +} diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 613641dd058..7101ca8ce1d 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -64,6 +64,8 @@ mod keys { pub const INBOUND_GROUP_SESSIONS_V3: &str = "inbound_group_sessions3"; pub const INBOUND_GROUP_SESSIONS_BACKUP_INDEX: &str = "backup"; pub const INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX: &str = "backed_up_to"; + pub const INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX: &str = + "inbound_group_session_sender_key_sender_data_type_idx"; pub const OUTBOUND_GROUP_SESSIONS: &str = "outbound_group_sessions"; From 1de99161e2a911580a85f5089843f92af4665218 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 28 Aug 2024 19:11:48 +0100 Subject: [PATCH 012/979] indexeddb: implement `get_inbound_group_sessions_for_device_batch` --- Cargo.lock | 8 ++-- crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- .../src/crypto_store/mod.rs | 37 ++++++++++++++++++- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c6f1049164..d45dd6200d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2542,9 +2542,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexed_db_futures" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" +checksum = "43315957678a70eb21fb0d2384fe86dde0d6c859a01e24ce127eb65a0143d28c" dependencies = [ "accessory", "cfg-if", @@ -6592,9 +6592,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.6.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "serde", diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index f224453d0c6..4a3a547f804 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -25,7 +25,7 @@ async-trait = { workspace = true } base64 = { workspace = true } gloo-utils = { version = "0.2.0", features = ["serde"] } growable-bloom-filter = { workspace = true, optional = true } -indexed_db_futures = "0.4.1" +indexed_db_futures = "0.5.0" js-sys = { version = "0.3.58" } matrix-sdk-base = { workspace = true, features = ["js"], optional = true } matrix-sdk-crypto = { workspace = true, features = ["js"], optional = true } diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 7101ca8ce1d..cf45fa2be2f 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -949,7 +949,42 @@ impl_crypto_store! { after_session_id: Option, limit: usize, ) -> Result> { - todo!(); + let sender_key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64()); + + // The empty string is before all keys in Indexed DB - first batch starts there. + let after_session_id = after_session_id.map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s)).unwrap_or("".into()); + + let lower_bound: Array = [sender_key.clone(), (sender_data_type as u8).into(), after_session_id].iter().collect(); + let upper_bound: Array = [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect(); + let key = IdbKeyRange::bound_with_lower_open_and_upper_open( + &lower_bound, + &upper_bound, + true, true + ).expect("Key was not valid!"); + + let tx = self + .inner + .transaction_on_one_with_mode( + keys::INBOUND_GROUP_SESSIONS_V3, + IdbTransactionMode::Readonly, + )?; + + let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?; + let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?; + let serialized_sessions = idx.get_all_with_key_and_limit_owned(key, limit as u32)?.await?; + + // Deserialize and decrypt after the transaction is complete. + let result = serialized_sessions.into_iter() + .filter_map(|v| match self.deserialize_inbound_group_session(v) { + Ok(session) => Some(session), + Err(e) => { + warn!("Failed to deserialize inbound group session: {e}"); + None + } + }) + .collect::>(); + + Ok(result) } async fn inbound_group_session_counts(&self, _backup_version: Option<&str>) -> Result { From d8b0f9f3d757cf9f9d6a5dac709ecb65d301097b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 27 Aug 2024 22:24:12 +0100 Subject: [PATCH 013/979] crypto: add cryptostore integ test Add a new integration test for `CryptoStore::get_inbound_group_sessions_for_device_batch` --- .../src/store/integration_tests.rs | 150 +++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 5cad8b74ec3..d696751bd5d 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -48,7 +48,7 @@ macro_rules! cryptostore_integration_tests { use $crate::{ olm::{ Account, Curve25519PublicKey, InboundGroupSession, OlmMessageHash, - PrivateCrossSigningIdentity, Session, + PrivateCrossSigningIdentity, SenderData, SenderDataType, Session }, store::{ BackupDecryptionKey, Changes, CryptoStore, DeviceChanges, GossipRequest, @@ -71,6 +71,9 @@ macro_rules! cryptostore_integration_tests { EventEncryptionAlgorithm, }, GossippedSecret, LocalTrust, DeviceData, SecretInfo, ToDeviceRequest, TrackedUser, + vodozemac::{ + megolm::{GroupSession, SessionConfig}, + }, }; use super::get_store; @@ -561,6 +564,118 @@ macro_rules! cryptostore_integration_tests { assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 1); } + #[async_test] + async fn test_fetch_inbound_group_sessions_for_device() { + // Given a store exists, containing inbound group sessions from different devices + let (account, store) = + get_loaded_store("fetch_inbound_group_sessions_for_device").await; + + let dev1 = Curve25519PublicKey::from_base64( + "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4" + ).unwrap(); + let dev2 = Curve25519PublicKey::from_base64( + "LTpv2DGMhggPAXO02+7f68CNEp6A40F0Yl8B094Y8gc" + ).unwrap(); + + let dev_1_unknown_a = create_session(&account, &dev1, SenderDataType::UnknownDevice).await; + let dev_1_unknown_b = create_session(&account, &dev1, SenderDataType::UnknownDevice).await; + + let dev_1_keys_a = create_session(&account, &dev1, SenderDataType::DeviceInfo).await; + let dev_1_keys_b = create_session(&account, &dev1, SenderDataType::DeviceInfo).await; + let dev_1_keys_c = create_session(&account, &dev1, SenderDataType::DeviceInfo).await; + let dev_1_keys_d = create_session(&account, &dev1, SenderDataType::DeviceInfo).await; + + let dev_2_unknown = create_session( + &account, &dev2, SenderDataType::UnknownDevice).await; + + let dev_2_keys = create_session( + &account, &dev2, SenderDataType::DeviceInfo).await; + + let sessions = vec![ + dev_1_unknown_a.clone(), + dev_1_unknown_b.clone(), + dev_1_keys_a.clone(), + dev_1_keys_b.clone(), + dev_1_keys_c.clone(), + dev_1_keys_d.clone(), + dev_2_unknown.clone(), + dev_2_keys.clone(), + ]; + + let changes = Changes { + inbound_group_sessions: sessions, + ..Default::default() + }; + store.save_changes(changes).await.expect("Can't save group session"); + + // When we fetch the list of sessions for device 1, unknown + let sessions_1_u = store.get_inbound_group_sessions_for_device_batch( + dev1, + SenderDataType::UnknownDevice, + None, + 10 + ).await.expect("Failed to get sessions for dev1"); + + // Then the expected sessions are returned + assert_session_lists_eq(sessions_1_u, [dev_1_unknown_a, dev_1_unknown_b], "device 1 sessions"); + + // And when we ask for the list of sessions for device 2, with device keys + let sessions_2_d = store + .get_inbound_group_sessions_for_device_batch(dev2, SenderDataType::DeviceInfo, None, 10) + .await + .expect("Failed to get sessions for dev2"); + + // Then the matching session is returned + assert_eq!(sessions_2_d, vec![dev_2_keys], "device 2 sessions"); + + // And we can fetch device 1, keys in batches. + // We call the batch function repeatedly, to ensure it terminates correctly. + let mut sessions_1_k = Vec::new(); + let mut previous_last_session_id: Option = None; + loop { + let mut sessions_1_k_batch = store.get_inbound_group_sessions_for_device_batch( + dev1, + SenderDataType::DeviceInfo, + previous_last_session_id, + 2 + ).await.expect("Failed to get batch 1"); + + // If there are no results in the batch, we have reached the end of the results. + let Some(last_session) = sessions_1_k_batch.last() else { + break; + }; + + // Check that there are exactly two results in the batch + assert_eq!(sessions_1_k_batch.len(), 2); + + previous_last_session_id = Some(last_session.session_id().to_owned()); + sessions_1_k.append(&mut sessions_1_k_batch); + } + + assert_session_lists_eq( + sessions_1_k, + [dev_1_keys_a, dev_1_keys_b, dev_1_keys_c, dev_1_keys_d], + "device 1 batched results" + ); + } + + /// Assert that two lists of sessions are the same, modulo ordering. + /// + /// There is no requirement for `get_inbound_group_sessions_for_device_batch` to + /// return the results in a specific order. This helper ensures that the two lists + /// of inbound group sessions are equivalent, without worrying about the ordering. + fn assert_session_lists_eq(actual: I, expected: J, message: &str) + where I: IntoIterator, J: IntoIterator + { + let sorter = |a: &InboundGroupSession, b: &InboundGroupSession| Ord::cmp(a.session_id(), b.session_id()); + + let mut actual = Vec::from_iter(actual); + actual.sort_unstable_by(sorter); + let mut expected = Vec::from_iter(expected); + expected.sort_unstable_by(sorter); + assert_eq!(actual, expected, "{}", message); + } + #[async_test] async fn test_tracked_users() { let dir = "test_tracked_users"; @@ -1112,6 +1227,39 @@ macro_rules! cryptostore_integration_tests { fn session_info(session: &InboundGroupSession) -> (&RoomId, &str) { (&session.room_id(), &session.session_id()) } + + async fn create_session( + account: &Account, + device_curve_key: &Curve25519PublicKey, + sender_data_type: SenderDataType, + ) -> InboundGroupSession { + let sender_data = match sender_data_type { + SenderDataType::UnknownDevice => { + SenderData::UnknownDevice { legacy_session: false, owner_check_failed: false } + } + SenderDataType::DeviceInfo => SenderData::DeviceInfo { + device_keys: account.device_keys().clone(), + legacy_session: false, + }, + SenderDataType::SenderUnverifiedButPreviouslyVerified => + panic!("SenderUnverifiedButPreviouslyVerified not supported"), + SenderDataType::SenderUnverified=> panic!("SenderUnverified not supported"), + SenderDataType::SenderVerified => panic!("SenderVerified not supported"), + }; + + let session_key = GroupSession::new(SessionConfig::default()).session_key(); + + InboundGroupSession::new( + device_curve_key.clone(), + account.device_keys().ed25519_key().unwrap(), + room_id!("!r:s.co"), + &session_key, + sender_data, + EventEncryptionAlgorithm::MegolmV1AesSha2, + None, + ) + .unwrap() + } } }; } From 49252b53426dd8d937363811a878689a1379d4b9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 3 Sep 2024 11:36:01 +0200 Subject: [PATCH 014/979] test: Restore Complement Crypto. --- .github/workflows/bindings_ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index babd5419e72..28a630b6c84 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -177,12 +177,12 @@ jobs: - name: Build Framework run: target/debug/xtask swift build-framework --target=aarch64-apple-ios - #complement-crypto: - # name: "Run Complement Crypto tests" - # uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main - # with: - # use_rust_sdk: "." # use local checkout - # use_complement_crypto: "MATCHING_BRANCH" + complement-crypto: + name: "Run Complement Crypto tests" + uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main + with: + use_rust_sdk: "." # use local checkout + use_complement_crypto: "MATCHING_BRANCH" test-crypto-apple-framework-generation: name: Generate Crypto FFI Apple XCFramework From a737421875a1f1f81ac416dd4576fbd776d5d307 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 3 Sep 2024 15:37:22 +0200 Subject: [PATCH 015/979] chore(ui): Rename variables. This is not a timestamp but a regular stamp. Make it clear with the variable names. --- .../matrix-sdk-ui/src/room_list_service/sorters/recency.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/sorters/recency.rs b/crates/matrix-sdk-ui/src/room_list_service/sorters/recency.rs index 7dde5db79d8..c15a46d5047 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/sorters/recency.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/sorters/recency.rs @@ -37,7 +37,7 @@ where // as shallow clones of each others. In practice it's really great: a `Room` can // never be outdated. However, for the case of sorting rooms, it breaks the // search algorithm. `left` and `right` will have the exact same recency - // timestamp, so `left` and `right` will always be `Ordering::Equal`. This is + // stamp, so `left` and `right` will always be `Ordering::Equal`. This is // wrong: if `left` is compared with `right` and if they are both the same room, // it means that one of them (either `left`, or `right`, it's not important) has // received an update. The room position is very likely to change. But if they @@ -50,9 +50,7 @@ where } match (self.recency_stamps)(left, right) { - (Some(left_timestamp), Some(right_timestamp)) => { - left_timestamp.cmp(&right_timestamp).reverse() - } + (Some(left_stamp), Some(right_stamp)) => left_stamp.cmp(&right_stamp).reverse(), (Some(_), None) => Ordering::Less, From 5b14fe6f344c2ecac42f8e164bf58b50fdf00d9e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 3 Sep 2024 17:43:46 +0300 Subject: [PATCH 016/979] crypto: fix OIDC cross-signing reset flows after backend authorization failure response change (#3933) --- bindings/matrix-sdk-ffi/src/encryption.rs | 5 +- crates/matrix-sdk/src/encryption/mod.rs | 80 ++++++++++--------- .../integration/encryption/cross_signing.rs | 55 ++++--------- 3 files changed, 57 insertions(+), 83 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index f9fec9d9965..85468b2cf93 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -462,15 +462,12 @@ impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningRe #[derive(uniffi::Record)] pub struct OidcCrossSigningResetInfo { - /// The error message we received from the homeserver after we attempted to - /// reset the cross-signing keys. - pub error: String, /// The URL where the user can approve the reset of the cross-signing keys. pub approval_url: String, } impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo { fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self { - Self { error: value.error.to_owned(), approval_url: value.approval_url.to_string() } + Self { approval_url: value.approval_url.to_string() } } } diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 712c90bff67..1438db50fcd 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -36,7 +36,6 @@ use matrix_sdk_base::crypto::{ use matrix_sdk_common::executor::spawn; use ruma::{ api::client::{ - error::{ErrorBody, ErrorKind}, keys::{ get_keys, upload_keys, upload_signatures::v3::Request as UploadSignaturesRequest, upload_signing_keys::v3::Request as UploadSigningKeysRequest, @@ -57,6 +56,7 @@ use ruma::{ }, DeviceId, OwnedDeviceId, OwnedUserId, TransactionId, UserId, }; +use serde::Deserialize; use tokio::sync::{Mutex, RwLockReadGuard}; use tracing::{debug, error, instrument, trace, warn}; use url::Url; @@ -279,13 +279,12 @@ impl CrossSigningResetHandle { let mut upload_request = self.upload_request.clone(); upload_request.auth = auth; - // TODO: Do we want to put a limit on this infinite loop? 🤷 while let Err(e) = self.client.send(upload_request.clone(), None).await { if *self.is_cancelled.lock().await { return Ok(()); } - if e.client_api_error_kind() != Some(&ErrorKind::Unrecognized) { + if e.as_uiaa_response().is_none() { return Err(e.into()); } } @@ -313,15 +312,22 @@ pub enum CrossSigningResetAuthType { } impl CrossSigningResetAuthType { - async fn new(client: &Client, error: &HttpError) -> Result> { + #[allow(clippy::unused_async)] + async fn new( + #[allow(unused_variables)] client: &Client, + error: &HttpError, + ) -> Result> { if let Some(auth_info) = error.as_uiaa_response() { + #[cfg(feature = "experimental-oidc")] + if client.oidc().issuer().is_some() { + OidcCrossSigningResetInfo::from_auth_info(client, auth_info) + .map(|t| Some(CrossSigningResetAuthType::Oidc(t))) + } else { + Ok(Some(CrossSigningResetAuthType::Uiaa(auth_info.clone()))) + } + + #[cfg(not(feature = "experimental-oidc"))] Ok(Some(CrossSigningResetAuthType::Uiaa(auth_info.clone()))) - } else if let Some(ErrorBody::Standard { kind, message }) = - error.as_client_api_error().map(|e| &e.body) - { - OidcCrossSigningResetInfo::from_matrix_error(client, kind, message) - .await - .map(|t| t.map(CrossSigningResetAuthType::Oidc)) } else { Ok(None) } @@ -330,45 +336,43 @@ impl CrossSigningResetAuthType { /// OIDC specific information about the required authentication for the upload /// of cross-signing keys. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] pub struct OidcCrossSigningResetInfo { - /// The error message we received from the homeserver after we attempted to - /// reset the cross-signing keys. - pub error: String, /// The URL where the user can approve the reset of the cross-signing keys. pub approval_url: Url, } impl OidcCrossSigningResetInfo { - #[allow(clippy::unused_async)] - async fn from_matrix_error( + #[cfg(feature = "experimental-oidc")] + fn from_auth_info( // This is used if the OIDC feature is enabled. #[allow(unused_variables)] client: &Client, - kind: &ErrorKind, - message: &str, - ) -> Result> { - #[cfg(feature = "experimental-oidc")] - use mas_oidc_client::requests::account_management::AccountManagementActionFull; + auth_info: &UiaaInfo, + ) -> Result { + let parameters = + serde_json::from_str::(auth_info.params.get())?; - if kind == &ErrorKind::Unrecognized { - #[cfg(feature = "experimental-oidc")] - let approval_url = client - .oidc() - .account_management_url(Some(AccountManagementActionFull::CrossSigningReset)) - .await?; + Ok(OidcCrossSigningResetInfo { approval_url: parameters.reset.url }) + } +} - #[cfg(not(feature = "experimental-oidc"))] - let approval_url = None; +/// The parsed `parameters` part of a [`ruma::api::client::uiaa::UiaaInfo`] +/// response +#[cfg(feature = "experimental-oidc")] +#[derive(Debug, Deserialize)] +struct OidcCrossSigningResetUiaaParameters { + /// The URL where the user can approve the reset of the cross-signing keys. + #[serde(rename = "org.matrix.cross_signing_reset")] + reset: OidcCrossSigningResetUiaaResetParameter, +} - if let Some(approval_url) = approval_url { - Ok(Some(OidcCrossSigningResetInfo { error: message.to_owned(), approval_url })) - } else { - Ok(None) - } - } else { - Ok(None) - } - } +/// The `org.matrix.cross_signing_reset` part of the Uiaa response `parameters`` +/// dictionary. +#[cfg(feature = "experimental-oidc")] +#[derive(Debug, Deserialize)] +struct OidcCrossSigningResetUiaaResetParameter { + /// The URL where the user can approve the reset of the cross-signing keys. + url: Url, } impl Client { diff --git a/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs b/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs index 38cf5e2d26e..79122252c69 100644 --- a/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs +++ b/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs @@ -141,32 +141,6 @@ async fn test_reset_oidc() { let (client, server) = no_retry_test_client_with_server().await; - let auth_issuer_body = json!({ - "issuer": server.uri(), - "authorization_endpoint": format!("{}/authorize", server.uri()), - "token_endpoint": format!("{}/oauth2/token", server.uri()), - "jwks_uri": format!("{}/oauth2/keys.json", server.uri()), - "response_types_supported": [ - "code", - ], - "response_modes_supported": [ - "fragment" - ], - "subject_types_supported": [ - "public" - ], - "id_token_signing_alg_values_supported": [ - "RS256", - ], - "claim_types_supported": [ - "normal" - ], - "account_management_uri": format!("{}/account/", server.uri()), - "account_management_actions_supported": [ - "org.matrix.cross_signing_reset" - ] - }); - pub fn mock_registered_client_data() -> (ClientCredentials, VerifiedClientMetadata) { ( ClientCredentials::None { client_id: CLIENT_ID.to_owned() }, @@ -227,21 +201,23 @@ async fn test_reset_oidc() { .mount(&server) .await; - Mock::given(method("GET")) - .and(path(".well-known/openid-configuration")) - .respond_with(ResponseTemplate::new(200).set_body_json(auth_issuer_body)) - .expect(1) - .named("Auth issuer discovery") - .mount(&server) - .await; + let uiaa_response_body = json!({ + "session": "dummy", + "flows": [{ + "stages": [ "org.matrix.cross_signing_reset" ] + }], + "params": { + "org.matrix.cross_signing_reset": { + "url": format!("{}/account/?action=org.matrix.cross_signing_reset", server.uri()) + } + }, + "msg": "To reset your end-to-end encryption cross-signing identity, you first need to approve it and then try again." + }); let reset_handle = { let _guard = Mock::given(method("POST")) .and(path("/_matrix/client/unstable/keys/device_signing/upload")) - .respond_with(ResponseTemplate::new(501).set_body_json(json!({ - "errcode": "M_UNRECOGNIZED", - "error": "To reset your cross-signing keys you first need to approve it in your auth issuer settings", - }))) + .respond_with(ResponseTemplate::new(401).set_body_json(uiaa_response_body.clone())) .expect(1) .named("Initial cross-signing upload attempt") .mount_as_scoped(&server) @@ -279,10 +255,7 @@ async fn test_reset_oidc() { if current_value >= 4 { ResponseTemplate::new(200).set_body_json(json!({})) } else { - ResponseTemplate::new(501).set_body_json(json!({ - "errcode": "M_UNRECOGNIZED", - "error": "", - })) + ResponseTemplate::new(401).set_body_json(uiaa_response_body.clone()) } } }) From 1dd8c908c590ac1dc78815869d18ea79c3cd14ba Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Sep 2024 13:06:32 -0400 Subject: [PATCH 017/979] crypto: Error when sending keys to previously-verified users with identity-based strategy (#3896) --- bindings/matrix-sdk-ffi/CHANGELOG.md | 10 + bindings/matrix-sdk-ffi/src/timeline/mod.rs | 15 +- crates/matrix-sdk-crypto/CHANGELOG.md | 6 + crates/matrix-sdk-crypto/src/error.rs | 22 +- .../group_sessions/share_strategy.rs | 301 +++++++++++++++++- .../src/test_json/keys_query_sets.rs | 148 +++++++++ 6 files changed, 485 insertions(+), 17 deletions(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 996921bdf17..44eaaa65eff 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -2,6 +2,16 @@ Breaking changes: +- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and + `SendingFromUnverifiedDevice`. These indicate that your own device is not + properly cross-signed, which is a requirement when using the identity-based + strategy, and can only be returned when using the identity-based strategy. + + In addition, the `VerifiedUserHasUnsignedDevice` and + `VerifiedUserChangedIdentity` variants can be returned when using the + identity-based strategy, in addition to when using the device-based strategy + with `error_on_verified_user_problem` is set. + - `EventSendState` now has two additional variants: `VerifiedUserHasUnsignedDevice` and `VerifiedUserChangedIdentity`. These reflect problems with verified users in the room and as such can only be returned when the room key recipient strategy has diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 50ff1972726..eed2fdba4b4 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -929,12 +929,21 @@ pub enum EventSendState { /// /// Happens only when the room key recipient strategy (as set by /// [`ClientBuilder::room_key_recipient_strategy`]) has - /// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem) set. + /// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem) + /// set, or when using [`CollectStrategy::IdentityBasedStrategy`]. VerifiedUserChangedIdentity { /// The users that were previously verified, but are no longer users: Vec, }, + /// The user does not have cross-signing set up, but + /// [`CollectStrategy::IdentityBasedStrategy`] was used. + CrossSigningNotSetup, + + /// The current device is not verified, but + /// [`CollectStrategy::IdentityBasedStrategy`] was used. + SendingFromUnverifiedDevice, + /// The local event has been sent to the server, but unsuccessfully: The /// sending has failed. SendingFailed { @@ -988,6 +997,10 @@ fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) -> VerifiedUserChangedIdentity(bad_users) => EventSendState::VerifiedUserChangedIdentity { users: bad_users.iter().map(|user_id| user_id.to_string()).collect(), }, + + CrossSigningNotSetup => EventSendState::CrossSigningNotSetup, + + SendingFromUnverifiedDevice => EventSendState::SendingFromUnverifiedDevice, }, _ => EventSendState::SendingFailed { error: error.to_string(), is_recoverable }, diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index a463ee92a2e..96804cef64f 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -56,6 +56,11 @@ Breaking changes: `OlmMachine::share_room_key` to fail with an error if any verified users on the recipient list have unsigned devices, or are no lonver verified. + When `CallectStrategy::IdentityBasedStrategy` is used, + `OlmMachine::share_room_key` will fail with an error if any verified users on + the recipient list are no longer verified, or if our own device is not + properly cross-signed. + Also remove `CollectStrategy::new_device_based`: callers should construct a `CollectStrategy::DeviceBasedStrategy` directly. @@ -63,6 +68,7 @@ Breaking changes: a list of booleans. ([#3810](https://github.com/matrix-org/matrix-rust-sdk/pull/3810)) ([#3816](https://github.com/matrix-org/matrix-rust-sdk/pull/3816)) + ([#3896](https://github.com/matrix-org/matrix-rust-sdk/pull/3896)) - Remove the method `OlmMachine::clear_crypto_cache()`, crypto stores are not supposed to have any caches anymore. diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index f11051d95db..0fcead5a609 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -391,7 +391,7 @@ pub enum SessionRecipientCollectionError { /// /// Happens only with [`CollectStrategy::DeviceBasedStrategy`] when /// [`error_on_verified_user_problem`](`CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem`) - /// is true. + /// is true, or with [`CollectStrategy::IdentityBasedStrategy`]. /// /// In order to resolve this, the user can: /// @@ -407,4 +407,24 @@ pub enum SessionRecipientCollectionError { /// The caller can then retry the encryption operation. #[error("one or more users that were verified have changed their identity")] VerifiedUserChangedIdentity(Vec), + + /// Cross-signing has not been configured on our own identity. + /// + /// Happens only with [`CollectStrategy::IdentityBasedStrategy`]. + /// (Cross-signing is required for encryption when using + /// `IdentityBasedStrategy`.) Apps should detect this condition and prevent + /// sending in the UI rather than waiting for this error to be returned when + /// encrypting. + #[error("Encryption failed because cross-signing is not set up on your account")] + CrossSigningNotSetup, + + /// The current device has not been cross-signed by our own identity. + /// + /// Happens only with [`CollectStrategy::IdentityBasedStrategy`]. + /// (Cross-signing is required for encryption when using + /// `IdentityBasedStrategy`.) Apps should detect this condition and prevent + /// sending in the UI rather than waiting for this error to be returned when + /// encrypting. + #[error("Encryption failed because your device is not verified")] + SendingFromUnverifiedDevice, } diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 5c426d0d425..310b040e3ce 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -120,6 +120,7 @@ pub(crate) async fn collect_session_recipients( let users: BTreeSet<&UserId> = users.collect(); let mut devices: BTreeMap> = Default::default(); let mut withheld_devices: Vec<(DeviceData, WithheldCode)> = Default::default(); + let mut verified_users_with_new_identities: Vec = Default::default(); trace!(?users, ?settings, "Calculating group session recipients"); @@ -145,6 +146,8 @@ pub(crate) async fn collect_session_recipients( // This is calculated in the following code and stored in this variable. let mut should_rotate = user_left || visibility_changed || algorithm_changed; + let own_identity = store.get_user_identity(store.user_id()).await?.and_then(|i| i.into_own()); + // Get the recipient and withheld devices, based on the collection strategy. match settings.sharing_strategy { CollectStrategy::DeviceBasedStrategy { @@ -153,10 +156,6 @@ pub(crate) async fn collect_session_recipients( } => { let mut unsigned_devices_of_verified_users: BTreeMap> = Default::default(); - let mut verified_users_with_new_identities: Vec = Default::default(); - - let own_identity = - store.get_user_identity(store.user_id()).await?.and_then(|i| i.into_own()); for user_id in users { trace!("Considering recipient devices for user {}", user_id); @@ -233,24 +232,39 @@ pub(crate) async fn collect_session_recipients( ), )); } - - // Alternatively, we may have encountered previously-verified users who have - // changed their identities. We bail out for that, too. - if !verified_users_with_new_identities.is_empty() { - return Err(OlmError::SessionRecipientCollectionError( - SessionRecipientCollectionError::VerifiedUserChangedIdentity( - verified_users_with_new_identities, - ), - )); - } } CollectStrategy::IdentityBasedStrategy => { + // We require our own cross-signing to be properly set up for the + // identity-based strategy, so return an error if it isn't. + match &own_identity { + None => { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::CrossSigningNotSetup, + )) + } + Some(identity) if !identity.is_verified() => { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::SendingFromUnverifiedDevice, + )) + } + Some(_) => (), + } + for user_id in users { trace!("Considering recipient devices for user {}", user_id); let user_devices = store.get_device_data_for_user_filtered(user_id).await?; let device_owner_identity = store.get_user_identity(user_id).await?; + if has_identity_verification_violation( + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + verified_users_with_new_identities.push(user_id.to_owned()); + // No point considering the individual devices of this user. + continue; + } + let recipient_devices = split_recipients_withhelds_for_user_based_on_identity( user_devices, &device_owner_identity, @@ -277,6 +291,16 @@ pub(crate) async fn collect_session_recipients( } } + // We may have encountered previously-verified users who have changed their + // identities. If so, we bail out with an error. + if !verified_users_with_new_identities.is_empty() { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity( + verified_users_with_new_identities, + ), + )); + } + if should_rotate { debug!( should_rotate, @@ -491,10 +515,14 @@ fn is_user_verified( mod tests { use std::{collections::BTreeMap, iter, sync::Arc}; + use assert_matches::assert_matches; use assert_matches2::assert_let; use matrix_sdk_test::{ async_test, test_json, - test_json::keys_query_sets::{KeyDistributionTestData, PreviouslyVerifiedTestData}, + test_json::keys_query_sets::{ + IdentityChangeDataSet, KeyDistributionTestData, MaloIdentityChangeDataSet, + PreviouslyVerifiedTestData, + }, }; use ruma::{ device_id, events::room::history_visibility::HistoryVisibility, room_id, TransactionId, @@ -1122,6 +1150,249 @@ mod tests { assert_eq!(code, &WithheldCode::Unauthorised); } + /// Test key sharing with the identity-based strategy with different + /// states of our own verification. + #[async_test] + async fn test_share_identity_strategy_no_cross_signing() { + // Starting off, we have not yet set up our own cross-signing, so + // sharing with the identity-based strategy should fail. + let machine: OlmMachine = OlmMachine::new( + KeyDistributionTestData::me_id(), + KeyDistributionTestData::me_device_id(), + ) + .await; + + let keys_query = KeyDistributionTestData::dan_keys_query_response(); + machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap(); + + let fake_room_id = room_id!("!roomid:localhost"); + + let encryption_settings = EncryptionSettings { + sharing_strategy: CollectStrategy::new_identity_based(), + ..Default::default() + }; + + let request_result = machine + .share_room_key( + fake_room_id, + iter::once(KeyDistributionTestData::dan_id()), + encryption_settings.clone(), + ) + .await; + + assert_matches!( + request_result, + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::CrossSigningNotSetup + )) + ); + + // We now get our public cross-signing keys, but we don't trust them + // yet. In this case, sharing the keys should still fail since our own + // device is still unverified. + let keys_query = KeyDistributionTestData::me_keys_query_response(); + machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap(); + + let request_result = machine + .share_room_key( + fake_room_id, + iter::once(KeyDistributionTestData::dan_id()), + encryption_settings.clone(), + ) + .await; + + assert_matches!( + request_result, + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::SendingFromUnverifiedDevice + )) + ); + + // Finally, after we trust our own cross-signing keys, key sharing + // should succeed. + machine + .import_cross_signing_keys(CrossSigningKeyExport { + master_key: KeyDistributionTestData::MASTER_KEY_PRIVATE_EXPORT.to_owned().into(), + self_signing_key: KeyDistributionTestData::SELF_SIGNING_KEY_PRIVATE_EXPORT + .to_owned() + .into(), + user_signing_key: KeyDistributionTestData::USER_SIGNING_KEY_PRIVATE_EXPORT + .to_owned() + .into(), + }) + .await + .unwrap(); + + let requests = machine + .share_room_key( + fake_room_id, + iter::once(KeyDistributionTestData::dan_id()), + encryption_settings.clone(), + ) + .await + .unwrap(); + + // Dan has two devices, but only one is cross-signed, so there should + // only be one key share. + assert_eq!(requests.len(), 1); + } + + /// Test that identity-based key sharing gives an error when a verified + /// user changes their identity, and that the key can be shared when the + /// identity change is resolved. + #[async_test] + async fn test_share_identity_strategy_report_verification_violation() { + let machine: OlmMachine = OlmMachine::new( + KeyDistributionTestData::me_id(), + KeyDistributionTestData::me_device_id(), + ) + .await; + + machine.bootstrap_cross_signing(false).await.unwrap(); + + // We will try sending a key to two different users. + let user1 = IdentityChangeDataSet::user_id(); + let user2 = MaloIdentityChangeDataSet::user_id(); + + // We first get both users' initial device and identity keys. + let keys_query = IdentityChangeDataSet::key_query_with_identity_a(); + machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap(); + + let keys_query = MaloIdentityChangeDataSet::initial_key_query(); + machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap(); + + // And then we get both user' changed identity keys. We simulate a + // verification violation by marking both users as having been + // previously verified, in which case the key sharing should fail. + let keys_query = IdentityChangeDataSet::key_query_with_identity_b(); + machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap(); + machine + .get_identity(user1, None) + .await + .unwrap() + .unwrap() + .other() + .unwrap() + .mark_as_previously_verified() + .await + .unwrap(); + + let keys_query = MaloIdentityChangeDataSet::updated_key_query(); + machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap(); + machine + .get_identity(user2, None) + .await + .unwrap() + .unwrap() + .other() + .unwrap() + .mark_as_previously_verified() + .await + .unwrap(); + + let fake_room_id = room_id!("!roomid:localhost"); + + // We share the key using the identity-based strategy. + let encryption_settings = EncryptionSettings { + sharing_strategy: CollectStrategy::new_identity_based(), + ..Default::default() + }; + + let request_result = machine + .share_room_key( + fake_room_id, + vec![user1, user2].into_iter(), + encryption_settings.clone(), + ) + .await; + + // The key share should fail with an error indicating that recipients + // were previously verified. + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity(affected_users) + )) = request_result + ); + // Both our recipients should be in `affected_users`. + assert_eq!(2, affected_users.len()); + + // We resolve this for user1 by withdrawing their verification. + machine + .get_identity(user1, None) + .await + .unwrap() + .unwrap() + .withdraw_verification() + .await + .unwrap(); + + // We resolve this for user2 by re-verifying. + let verification_request = machine + .get_identity(user2, None) + .await + .unwrap() + .unwrap() + .other() + .unwrap() + .verify() + .await + .unwrap(); + let raw_extracted = + verification_request.signed_keys.get(user2).unwrap().iter().next().unwrap().1.get(); + let signed_key: crate::types::CrossSigningKey = + serde_json::from_str(raw_extracted).unwrap(); + let new_signatures = signed_key.signatures.get(KeyDistributionTestData::me_id()).unwrap(); + let mut master_key = machine + .get_identity(user2, None) + .await + .unwrap() + .unwrap() + .other() + .unwrap() + .master_key + .as_ref() + .clone(); + + for (key_id, signature) in new_signatures.iter() { + master_key.as_mut().signatures.add_signature( + KeyDistributionTestData::me_id().to_owned(), + key_id.to_owned(), + signature.as_ref().unwrap().ed25519().unwrap(), + ); + } + let json = serde_json::json!({ + "device_keys": {}, + "failures": {}, + "master_keys": { + user2: master_key, + }, + "user_signing_keys": {}, + "self_signing_keys": MaloIdentityChangeDataSet::updated_key_query().self_signing_keys, + } + ); + + let kq_response = matrix_sdk_test::ruma_response_from_json(&json); + machine + .mark_request_as_sent( + &TransactionId::new(), + crate::IncomingResponse::KeysQuery(&kq_response), + ) + .await + .unwrap(); + + assert!(machine.get_identity(user2, None).await.unwrap().unwrap().is_verified()); + + // And now the key share should succeed. + machine + .share_room_key( + fake_room_id, + vec![user1, user2].into_iter(), + encryption_settings.clone(), + ) + .await + .unwrap(); + } + #[async_test] async fn test_should_rotate_based_on_visibility() { let machine = set_up_test_machine().await; diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index ea12ff899ef..4fa38e100a6 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -1263,3 +1263,151 @@ impl PreviouslyVerifiedTestData { ruma_response_from_json(&data) } } + +/// A set of keys query to test identity changes, +/// For user @malo, that performed an identity change with the same device. +pub struct MaloIdentityChangeDataSet {} + +#[allow(dead_code)] +impl MaloIdentityChangeDataSet { + pub fn user_id() -> &'static UserId { + user_id!("@malo:localhost") + } + + pub fn device_id() -> &'static DeviceId { + device_id!("NZFSPBRLDO") + } + + /// @malo's keys before their identity change + pub fn initial_key_query() -> KeyQueryResponse { + let data = json!({ + "device_keys": { + "@malo:localhost": { + "NZFSPBRLDO": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "NZFSPBRLDO", + "keys": { + "curve25519:NZFSPBRLDO": "L3jdbw42+9i+K7LPjAY+kmqG9nr2n/U0ow8hEbLCoCs", + "ed25519:NZFSPBRLDO": "VDJt3xI4SzrgQkuE3sEIauluaXawx3wWoWOynPI8Zko" + }, + "signatures": { + "@malo:localhost": { + "ed25519:NZFSPBRLDO": "lmtbdrJ5xBweo677Fg2qrSHsRi4R3x2WNlvSNJY6Zbg0R5lJS9syN2HZw/irL9PA644GYm4QM/t+DX0grnn+BQ", + "ed25519:+wbxNfSuDrch1jKuydQmEf4qlA4u4NgwqNXNuLVwug8": "Ql1fq+SvVDx+8mjNMzSaR0hBCEkdPirbs2+BK0gwsIH1zkuMADnBoNWP7LJiKo/EO9gnpiCzyQQgI4e9pIVPDA" + } + }, + "user_id": "@malo:localhost", + "unsigned": {} + } + } + }, + "failures": {}, + "master_keys": { + "@malo:localhost": { + "keys": { + "ed25519:WBxliSP29guYr4ux0MW6otRe3V/wOLXXElpOcOmpdlE": "WBxliSP29guYr4ux0MW6otRe3V/wOLXXElpOcOmpdlE" + }, + "signatures": { + "@malo:localhost": { + "ed25519:NZFSPBRLDO": "crJcXqFpEHRM8KNUw419XrVFaHoM8/kV4ebgpuuIiD9wfX0AhHE2iGRGpKzsrVCqne9k181/uN0sgDMpK2y4Aw", + "ed25519:WBxliSP29guYr4ux0MW6otRe3V/wOLXXElpOcOmpdlE": "/xwFF5AC3GhkpvJ449Srh8kNQS6CXAxQMmBpQvPEHx5BHPXJ08u2ZDd1EPYY4zk4QsePk+tEYu8gDnB0bggHCA" + } + }, + "usage": [ + "master" + ], + "user_id": "@malo:localhost" + } + }, + "self_signing_keys": { + "@malo:localhost": { + "keys": { + "ed25519:+wbxNfSuDrch1jKuydQmEf4qlA4u4NgwqNXNuLVwug8": "+wbxNfSuDrch1jKuydQmEf4qlA4u4NgwqNXNuLVwug8" + }, + "signatures": { + "@malo:localhost": { + "ed25519:WBxliSP29guYr4ux0MW6otRe3V/wOLXXElpOcOmpdlE": "sSGQ6ny6aXtIvgKPGOYJzcmnNDSkbaJFVRe9wekOry7EaiWf2l28MkGTUBt4cPoRiMkNjuRBupNEARqHF72sAQ" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@malo:localhost" + } + }, + "user_signing_keys": {}, + }); + + ruma_response_from_json(&data) + } + + /// @malo's keys after their identity change + pub fn updated_key_query() -> KeyQueryResponse { + let data = json!({ + "device_keys": { + "@malo:localhost": { + "NZFSPBRLDO": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "NZFSPBRLDO", + "keys": { + "curve25519:NZFSPBRLDO": "L3jdbw42+9i+K7LPjAY+kmqG9nr2n/U0ow8hEbLCoCs", + "ed25519:NZFSPBRLDO": "VDJt3xI4SzrgQkuE3sEIauluaXawx3wWoWOynPI8Zko" + }, + "signatures": { + "@malo:localhost": { + "ed25519:NZFSPBRLDO": "lmtbdrJ5xBweo677Fg2qrSHsRi4R3x2WNlvSNJY6Zbg0R5lJS9syN2HZw/irL9PA644GYm4QM/t+DX0grnn+BQ", + "ed25519:+wbxNfSuDrch1jKuydQmEf4qlA4u4NgwqNXNuLVwug8": "Ql1fq+SvVDx+8mjNMzSaR0hBCEkdPirbs2+BK0gwsIH1zkuMADnBoNWP7LJiKo/EO9gnpiCzyQQgI4e9pIVPDA", + "ed25519:8my6+zgnzEP0ZqmQFyvscJh7isHlf8lxBmHg+fzdJkE": "OvqDE7C2mrHxjwNyMIEz+m/AO6I6lM5HoPYY2bvLjrJJDOF5sJOtw4JoYiCWyt90ZIWsbEqmfbazrblLD50tCg" + } + }, + "user_id": "@malo:localhost", + "unsigned": {} + } + } + }, + "failures": {}, + "master_keys": { + "@malo:localhost": { + "keys": { + "ed25519:dv2Mk7bFlRtP/0oSZpB01Ouc5frCXKfG8Bn9YrFxbxU": "dv2Mk7bFlRtP/0oSZpB01Ouc5frCXKfG8Bn9YrFxbxU" + }, + "signatures": { + "@malo:localhost": { + "ed25519:NZFSPBRLDO": "2Ye96l4srBSWskNQszuMpea1r97rFoUyfNqegvu/hGeP47w0OVvqYuNtZRNwqb7TMS7aPEn6l9lhWEk7v06wCg", + "ed25519:dv2Mk7bFlRtP/0oSZpB01Ouc5frCXKfG8Bn9YrFxbxU": "btkxAJpJeVtc9wgBmeHUI9QDpojd6ddLxK11E3403KoTQtP6Mnr5GsVdQr1HJToG7PG4k4eEZGWxVZr1GPndAA" + } + }, + "usage": [ + "master" + ], + "user_id": "@malo:localhost" + } + }, + "self_signing_keys": { + "@malo:localhost": { + "keys": { + "ed25519:8my6+zgnzEP0ZqmQFyvscJh7isHlf8lxBmHg+fzdJkE": "8my6+zgnzEP0ZqmQFyvscJh7isHlf8lxBmHg+fzdJkE" + }, + "signatures": { + "@malo:localhost": { + "ed25519:dv2Mk7bFlRtP/0oSZpB01Ouc5frCXKfG8Bn9YrFxbxU": "KJt0y1p8v8RGLGk2wUyCMbX1irXJqup/mdRuG/cxJxs24BZhDMyIzyGrGXnWq2gx3I4fKIMtFPi/ecxf92ePAQ" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@malo:localhost" + } + }, + "user_signing_keys": {} + }); + + ruma_response_from_json(&data) + } +} From aa94ad846bc4c7840b98aae89288f0dba224ce42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:50:40 +0000 Subject: [PATCH 018/979] build(deps): bump quinn-proto from 0.11.3 to 0.11.8 Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.3 to 0.11.8. - [Release notes](https://github.com/quinn-rs/quinn/releases) - [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.3...quinn-proto-0.11.8) --- updated-dependencies: - dependency-name: quinn-proto dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d45dd6200d0..c720ec24ac0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4584,7 +4584,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 1.1.0", "rustls", "thiserror", "tokio", @@ -4593,14 +4593,14 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.3" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", "rand", "ring", - "rustc-hash", + "rustc-hash 2.0.0", "rustls", "slab", "thiserror", @@ -5168,6 +5168,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.4.0" From b0e81213474e98d3077f48f7c21ac9d473849b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 4 Sep 2024 10:23:14 +0200 Subject: [PATCH 019/979] sqlite: Bump sqlite crates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- Cargo.lock | 31 +++++++++++++++++++---------- crates/matrix-sdk-sqlite/Cargo.toml | 4 ++-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c720ec24ac0..fe91dfd0c26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,6 +1310,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "deadpool" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + [[package]] name = "deadpool-runtime" version = "0.1.4" @@ -1321,11 +1332,11 @@ dependencies = [ [[package]] name = "deadpool-sqlite" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8010e36e12f3be22543a5e478b4af20aeead9a700dd69581a5e050a070fc22c" +checksum = "2f9cc6210316f8b7ced394e2a5d2833ce7097fb28afb5881299c61bc18e8e0e9" dependencies = [ - "deadpool", + "deadpool 0.12.1", "deadpool-sync", "rusqlite", ] @@ -2206,9 +2217,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ "hashbrown 0.14.5", ] @@ -2841,9 +2852,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ "cc", "pkg-config", @@ -5144,9 +5155,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ "bitflags 2.6.0", "fallible-iterator", @@ -7089,7 +7100,7 @@ dependencies = [ "assert-json-diff", "async-trait", "base64 0.21.7", - "deadpool", + "deadpool 0.10.0", "futures", "http", "http-body-util", diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index b20f211b060..38ac860f89f 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -18,14 +18,14 @@ state-store = ["dep:matrix-sdk-base"] [dependencies] async-trait = { workspace = true } -deadpool-sqlite = "0.7.0" +deadpool-sqlite = "0.8.1" itertools = { workspace = true } matrix-sdk-base = { workspace = true, optional = true } matrix-sdk-crypto = { workspace = true, optional = true } matrix-sdk-store-encryption = { workspace = true } rmp-serde = "1.1.1" ruma = { workspace = true } -rusqlite = { version = "0.30.0", features = ["limits"] } +rusqlite = { version = "0.31.0", features = ["limits"] } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } From 5b0ad01bab377dfda2bc90b75538945188bdc5ef Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 2 Sep 2024 17:16:38 +0200 Subject: [PATCH 020/979] event cache: don't return a useless `Option` --- crates/matrix-sdk/src/event_cache/mod.rs | 26 ++++++------------- .../matrix-sdk/src/event_cache/pagination.rs | 3 --- crates/matrix-sdk/src/room/mod.rs | 8 +----- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index bbedf546fee..b7e122c6388 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -282,7 +282,7 @@ impl EventCache { pub(crate) async fn for_room( &self, room_id: &RoomId, - ) -> Result<(Option, Arc)> { + ) -> Result<(RoomEventCache, Arc)> { let Some(drop_handles) = self.inner.drop_handles.get().cloned() else { return Err(EventCacheError::NotSubscribedYet); }; @@ -303,10 +303,7 @@ impl EventCache { events: Vec, prev_batch: Option, ) -> Result<()> { - let Some(room_cache) = self.inner.for_room(room_id).await? else { - warn!("unknown room, skipping"); - return Ok(()); - }; + let room_cache = self.inner.for_room(room_id).await?; // We could have received events during a previous sync; remove them all, since // we can't know where to insert the "initial events" with respect to @@ -399,10 +396,7 @@ impl EventCacheInner { // Left rooms. for (room_id, left_room_update) in updates.leave { - let Some(room) = self.for_room(&room_id).await? else { - warn!(%room_id, "missing left room"); - continue; - }; + let room = self.for_room(&room_id).await?; if let Err(err) = room.inner.handle_left_room_update(left_room_update).await { // Non-fatal error, try to continue to the next room. @@ -412,10 +406,7 @@ impl EventCacheInner { // Joined rooms. for (room_id, joined_room_update) in updates.join { - let Some(room) = self.for_room(&room_id).await? else { - warn!(%room_id, "missing joined room"); - continue; - }; + let room = self.for_room(&room_id).await?; if let Err(err) = room.inner.handle_joined_room_update(joined_room_update).await { // Non-fatal error, try to continue to the next room. @@ -433,13 +424,13 @@ impl EventCacheInner { /// /// It may not be found, if the room isn't known to the client, in which /// case it'll return None. - async fn for_room(&self, room_id: &RoomId) -> Result> { + async fn for_room(&self, room_id: &RoomId) -> Result { // Fast path: the entry exists; let's acquire a read lock, it's cheaper than a // write lock. let by_room_guard = self.by_room.read().await; match by_room_guard.get(room_id) { - Some(room) => Ok(Some(room.clone())), + Some(room) => Ok(room.clone()), None => { // Slow-path: the entry doesn't exist; let's acquire a write lock. @@ -449,7 +440,7 @@ impl EventCacheInner { // In the meanwhile, some other caller might have obtained write access and done // the same, so check for existence again. if let Some(room) = by_room_guard.get(room_id) { - return Ok(Some(room.clone())); + return Ok(room.clone()); } let room_event_cache = RoomEventCache::new( @@ -460,7 +451,7 @@ impl EventCacheInner { by_room_guard.insert(room_id.to_owned(), room_event_cache.clone()); - Ok(Some(room_event_cache)) + Ok(room_event_cache) } } } @@ -1032,7 +1023,6 @@ mod tests { event_cache.subscribe().unwrap(); let (room_event_cache, _drop_handles) = event_cache.for_room(room_id).await.unwrap(); - let room_event_cache = room_event_cache.unwrap(); let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index e81fdad8879..f334f470794 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -352,7 +352,6 @@ mod tests { event_cache.subscribe().unwrap(); let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap(); - let room_event_cache = room_event_cache.unwrap(); // When I only have events in a room, { @@ -405,7 +404,6 @@ mod tests { event_cache.subscribe().unwrap(); let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap(); - let room_event_cache = room_event_cache.unwrap(); let expected_token = "old".to_owned(); @@ -460,7 +458,6 @@ mod tests { event_cache.subscribe().unwrap(); let (room_event_cache, _drop_handles) = event_cache.for_room(room_id).await.unwrap(); - let room_event_cache = room_event_cache.unwrap(); let expected_token = "old".to_owned(); diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 38e999f0caf..a68bc366a92 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2769,13 +2769,7 @@ impl Room { pub async fn event_cache( &self, ) -> event_cache::Result<(RoomEventCache, Arc)> { - let global_event_cache = self.client.event_cache(); - - global_event_cache.for_room(self.room_id()).await.map(|(maybe_room, drop_handles)| { - // SAFETY: the `RoomEventCache` must always been found, since we're constructing - // from a `Room`. - (maybe_room.unwrap(), drop_handles) - }) + self.client.event_cache().for_room(self.room_id()).await } /// This will only send a call notification event if appropriate. From 3f7909641fb79d2dd356506445b16189d74f5f7a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Sep 2024 11:31:25 +0200 Subject: [PATCH 021/979] client builder(nit): avoid unnecessary clone --- crates/matrix-sdk/src/client/builder/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 11442d16c13..cb67511d64a 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -443,7 +443,9 @@ impl ClientBuilder { let mut client = BaseClient::with_store_config(build_store_config(self.store_config).await?); #[cfg(feature = "e2e-encryption")] - (client.room_key_recipient_strategy = self.room_key_recipient_strategy.clone()); + { + client.room_key_recipient_strategy = self.room_key_recipient_strategy; + } client }; From 0db0ea0977f19f7aed12e019328a0ef54c584a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 8 Aug 2024 18:46:17 +0200 Subject: [PATCH 022/979] docs: Add PR review guidelines. --- CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23e7a692a8e..0e09da0c6f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,6 +67,71 @@ Having good commit messages and PR titles also helps with reviews, scanning the the project, and writing the [*This week in Matrix*](https://matrix.org/category/this-week-in-matrix/) updates for the SDK. +## Review process + +To streamline the review process and make it easier for maintainers to review +your contributions, follow these basic rules: + +1. Do not force push after a review has started. This helps maintainers track + incremental changes without confusion and makes it easier to follow the + evolution of the code. + +2. Do not mix moves and refactoring with functional changes. Keep these in + separate commits for clarity. This ensures that the purpose of each commit is + clear and easy to review. + +3. Each commit must compile. If commits don’t compile, git bisect becomes + unusable, which hampers the debugging process and makes it harder to identify + the source of issues. + +4. Commits should only introduce test failures if they are proving that a bug + exists. New features should never introduce test failures. Test failures + should only be used to demonstrate existing bugs, not as part of adding new + functionality. + +5. Keep PRs on topic and small. Large PRs are harder to review and more prone to + delays. Create small, focused commits that address a single topic. Use a + combination of [git add] -p or git checkout -p to split changes into logical + units. This makes your work easier to review and reduces the chance of + introducing unrelated changes. + +[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch +[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch + +### Addressing review comments using fixup commits + +So you posted a PR and the maintainers aren't quite happy with it. Here are some +guidelines to make the maintainers life easier and increase the chances that +your PR will be reviewed swiftly. + +1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup +commits. These commits mark your changes as corrections of specific previous +commits in the PR. + +Example: + +```bash +git commit --fixup= +``` + +This command creates a new commit that refers to an existing one, making it +easier to rebase and squash later while showing reviewers the history of fixes. +For extra points, link to the fixup commit in the thread where the change was +requested. + +2. After all requested changes were addressed, feel free to re-request a review. + People might not notice that all changes were addressed. + +3. Once the PR has been approved, rebase your PR to squash all the fixup + commits, the [autosquash] option can help with this. + +```bash +git rebase main --autosquash +``` + +[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt +[autosquash]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash + ## Sign off In order to have a concrete record that your contribution is intentional From 6c704352a919379d243d62e60d92c9adbb0006a7 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 29 Aug 2024 10:13:27 +0300 Subject: [PATCH 023/979] send_queue: add mechanism for unwedging and resending a request based on its transaction identifier --- crates/matrix-sdk/src/send_queue.rs | 27 ++++++ .../tests/integration/send_queue.rs | 88 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index ba77183a4a8..b48a08ee109 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -556,6 +556,20 @@ impl RoomSendQueue { self.inner.notifier.notify_one(); } } + + /// Unwedge a local echo identified by its transaction identifier and try to + /// resend it. + pub async fn unwedge(&self, transaction_id: &TransactionId) -> Result<(), RoomSendQueueError> { + self.inner + .queue + .mark_as_unwedged(transaction_id) + .await + .map_err(RoomSendQueueError::StorageError)?; + + self.inner.notifier.notify_one(); + + Ok(()) + } } struct RoomSendQueueInner { @@ -673,6 +687,19 @@ impl QueueStorage { .await?) } + /// Marks an event identified with the given transaction id as being now + /// unwedged and adds it back to the queue + async fn mark_as_unwedged( + &self, + transaction_id: &TransactionId, + ) -> Result<(), RoomSendQueueStorageError> { + Ok(self + .client()? + .store() + .update_send_queue_event_status(&self.room_id, transaction_id, false) + .await?) + } + /// Marks an event pushed with [`Self::push`] and identified with the given /// transaction id as sent by removing it from the local queue. async fn mark_as_sent( diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 53494791f11..822e88dda76 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1398,6 +1398,94 @@ async fn test_unrecoverable_errors() { assert!(client.send_queue().is_enabled()); } +#[async_test] +async fn test_unwedge_unrecoverable_errors() { + let (client, server) = logged_in_client_with_server().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + + let room = mock_sync_with_new_room( + |builder| { + builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + }, + &client, + &server, + room_id, + ) + .await; + + let mut errors = client.send_queue().subscribe_errors(); + + assert!(errors.is_empty()); + + // Start with an enabled sending queue. + client.send_queue().set_enabled(true).await; + + assert!(client.send_queue().is_enabled()); + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + + assert!(local_echoes.is_empty()); + assert!(watch.is_empty()); + + server.reset().await; + + mock_encryption_state(&server, false).await; + + let respond_with_unrecoverable = AtomicBool::new(true); + + // Respond to the first /send with an unrecoverable error. + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(move |_req: &Request| { + // The first message gets M_TOO_LARGE, subsequent messages will encounter a + // great success. + if respond_with_unrecoverable.swap(false, Ordering::SeqCst) { + ResponseTemplate::new(413).set_body_json(json!({ + // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response + "errcode": "M_TOO_LARGE", + })) + } else { + ResponseTemplate::new(200).set_body_json(json!({ + "event_id": "$42", + })) + } + }) + .expect(2) + .mount(&server) + .await; + + // Queue the unrecoverable message. + q.send(RoomMessageEventContent::text_plain("i'm too big for ya").into()).await.unwrap(); + + // Message is seen as a local echo. + let (txn1, _) = assert_update!(watch => local echo { body = "i'm too big for ya" }); + + // There will be an error report for the first message, indicating that the + // error is unrecoverable. + let report = errors.recv().await.unwrap(); + assert_eq!(report.room_id, room.room_id()); + assert!(!report.is_recoverable); + + // The room updates will report the error for the first message as unrecoverable + // too. + assert_update!(watch => error { recoverable=false, txn=txn1 }); + + // No queue is being disabled, because the error was unrecoverable. + assert!(room.send_queue().is_enabled()); + assert!(client.send_queue().is_enabled()); + + // Unwedge the previously failed message and try sending it again + let _ = q.unwedge(&txn1).await; + + // The message will be sent and a remote echo received + assert_update!(watch => sent { txn=txn1, event_id=event_id!("$42") }); +} + #[async_test] async fn test_no_network_access_error_is_recoverable() { // This is subtle, but for the `drop(server)` below to be effectful, it needs to From f3d3924bb6bce77760f2adcf67161e56711fe7aa Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 29 Aug 2024 13:43:12 +0300 Subject: [PATCH 024/979] send_queue: publish retry updates when unwedging an event; have the timeline update the corresponding item in response. --- .../matrix-sdk-ui/src/timeline/controller/mod.rs | 4 ++++ crates/matrix-sdk/src/send_queue.rs | 11 +++++++++++ crates/matrix-sdk/tests/integration/send_queue.rs | 15 ++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index d723f703110..86463ed3b14 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1331,6 +1331,10 @@ impl TimelineController

{ .await; } + RoomSendQueueUpdate::RetryEvent { transaction_id } => { + self.update_event_send_state(&transaction_id, EventSendState::NotSentYet).await; + } + RoomSendQueueUpdate::SentEvent { transaction_id, event_id } => { self.update_event_send_state(&transaction_id, EventSendState::Sent { event_id }) .await; diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index b48a08ee109..ee82dd3adee 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -568,6 +568,11 @@ impl RoomSendQueue { self.inner.notifier.notify_one(); + let _ = self + .inner + .updates + .send(RoomSendQueueUpdate::RetryEvent { transaction_id: transaction_id.to_owned() }); + Ok(()) } } @@ -1194,6 +1199,12 @@ pub enum RoomSendQueueUpdate { is_recoverable: bool, }, + /// The event has been unwedged and sending is now being retried. + RetryEvent { + /// Transaction id used to identify this event. + transaction_id: OwnedTransactionId, + }, + /// The event has been sent to the server, and the query returned /// successfully. SentEvent { diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 822e88dda76..6ab33821613 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -122,6 +122,16 @@ macro_rules! assert_update { assert_eq!(txn, $transaction_id); }}; + // Check the next stream event is a retry event, with optional checks on txn=$txn + ($watch:ident => retry { $(txn=$txn:expr)? }) => { + assert_let!( + Ok(Ok(RoomSendQueueUpdate::RetryEvent { transaction_id: _txn })) = + timeout(Duration::from_secs(1), $watch.recv()).await + ); + + $(assert_eq!(_txn, $txn);)? + }; + // Check the next stream event is a sent event, with optional checks on txn=$txn and // event_id=$event_id. ($watch:ident => sent { $(txn=$txn:expr,)? $(event_id=$event_id:expr)? }) => { @@ -1482,7 +1492,10 @@ async fn test_unwedge_unrecoverable_errors() { // Unwedge the previously failed message and try sending it again let _ = q.unwedge(&txn1).await; - // The message will be sent and a remote echo received + // The message should be retried + assert_update!(watch => retry { txn=txn1 }); + + // Then eventually sent and a remote echo received assert_update!(watch => sent { txn=txn1, event_id=event_id!("$42") }); } From 14ee78c54de1dbe5609f2620fcf398620bd1a91c Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 29 Aug 2024 10:15:27 +0300 Subject: [PATCH 025/979] ffi: expose methods for manually withdrawing certain users' verification or trusting their devices and resending failed messages --- bindings/matrix-sdk-ffi/src/error.rs | 8 +- bindings/matrix-sdk-ffi/src/room.rs | 74 ++++++++++++++++++- .../src/encryption/identities/users.rs | 11 ++- crates/matrix-sdk/src/send_queue.rs | 3 +- .../tests/integration/send_queue.rs | 2 +- 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index 7e2d2799e18..ba0198b4f0c 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -134,14 +134,14 @@ impl From for ClientError { } } -impl From for ClientError { - fn from(e: RoomSendQueueError) -> Self { +impl From for ClientError { + fn from(e: EditError) -> Self { Self::new(e) } } -impl From for ClientError { - fn from(e: EditError) -> Self { +impl From for ClientError { + fn from(e: RoomSendQueueError) -> Self { Self::new(e) } } diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index b54e73bb027..0de3e7f5559 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -1,7 +1,8 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use anyhow::{Context, Result}; use matrix_sdk::{ + crypto::LocalTrust, event_cache::paginator::PaginatorError, room::{ edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole, @@ -23,7 +24,7 @@ use ruma::{ }, TimelineEventType, }, - EventId, Int, RoomAliasId, UserId, + EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, UserId, }; use tokio::sync::RwLock; use tracing::error; @@ -737,6 +738,75 @@ impl Room { self.inner.send_queue().send(replacement_event).await?; Ok(()) } + + /// Remove verification requirements for the given users and + /// resend messages that failed to send because their identities were no + /// longer verified (in response to + /// `SessionRecipientCollectionError::VerifiedUserChangedIdentity`) + /// + /// # Arguments + /// + /// * `user_ids` - The list of users identifiers received in the error + /// * `transaction_id` - The send queue transaction identifier of the local + /// echo the send error applies to + pub async fn withdraw_verification_and_resend( + &self, + user_ids: Vec, + transaction_id: String, + ) -> Result<(), ClientError> { + let transaction_id: OwnedTransactionId = transaction_id.into(); + + let user_ids: Vec = + user_ids.iter().map(UserId::parse).collect::>()?; + + let encryption = self.inner.client().encryption(); + + for user_id in user_ids { + if let Some(user_identity) = encryption.get_user_identity(&user_id).await? { + user_identity.withdraw_verification().await?; + } + } + + self.inner.send_queue().unwedge(&transaction_id).await?; + + Ok(()) + } + + /// Set the local trust for the given devices to `LocalTrust::Ignored` + /// and resend messages that failed to send because said devices are + /// unverified (in response to + /// `SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice`). + /// # Arguments + /// + /// * `devices` - The map of users identifiers to device identifiers + /// received in the error + /// * `transaction_id` - The send queue transaction identifier of the local + /// echo the send error applies to + pub async fn ignore_device_trust_and_resend( + &self, + devices: HashMap>, + transaction_id: String, + ) -> Result<(), ClientError> { + let transaction_id: OwnedTransactionId = transaction_id.into(); + + let encryption = self.inner.client().encryption(); + + for (user_id, device_ids) in devices.iter() { + let user_id = UserId::parse(user_id)?; + + for device_id in device_ids { + let device_id: OwnedDeviceId = device_id.as_str().into(); + + if let Some(device) = encryption.get_device(&user_id, &device_id).await? { + device.set_local_trust(LocalTrust::Ignored).await?; + } + } + } + + self.inner.send_queue().unwedge(&transaction_id).await?; + + Ok(()) + } } /// Generates a `matrix.to` permalink to the given room alias. diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index 25653c381d3..ad47bc4c356 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -15,7 +15,7 @@ use std::collections::BTreeMap; use matrix_sdk_base::{ - crypto::{types::MasterPubkey, UserIdentities as CryptoUserIdentities}, + crypto::{types::MasterPubkey, CryptoStoreError, UserIdentities as CryptoUserIdentities}, RoomMemberships, }; use ruma::{ @@ -353,6 +353,15 @@ impl UserIdentity { self.inner.identity.is_verified() } + /// Remove the requirement for this identity to be verified. + /// + /// If an identity was previously verified and is not any more it will be + /// reported to the user. In order to remove this notice users have to + /// verify again or to withdraw the verification requirement. + pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> { + self.inner.identity.withdraw_verification().await + } + /// Get the public part of the Master key of this user identity. /// /// The public part of the Master key is usually used to uniquely identify diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index ee82dd3adee..6b0cc3704c3 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -566,6 +566,7 @@ impl RoomSendQueue { .await .map_err(RoomSendQueueError::StorageError)?; + // Wake up the queue, in case the room was asleep before unwedging the event. self.inner.notifier.notify_one(); let _ = self @@ -693,7 +694,7 @@ impl QueueStorage { } /// Marks an event identified with the given transaction id as being now - /// unwedged and adds it back to the queue + /// unwedged and adds it back to the queue. async fn mark_as_unwedged( &self, transaction_id: &TransactionId, diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 6ab33821613..0837a36fdf6 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1490,7 +1490,7 @@ async fn test_unwedge_unrecoverable_errors() { assert!(client.send_queue().is_enabled()); // Unwedge the previously failed message and try sending it again - let _ = q.unwedge(&txn1).await; + q.unwedge(&txn1).await.unwrap(); // The message should be retried assert_update!(watch => retry { txn=txn1 }); From 31b4d0a2d1bf17759f3dc8df1633cf2803f16c43 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Sep 2024 23:10:47 -0400 Subject: [PATCH 026/979] crypto: add setting for checking sender device trust on decryption --- crates/matrix-sdk-crypto/src/lib.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index c712adb6aba..41c110a3b60 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -91,6 +91,7 @@ pub use requests::{ IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest, }; +use serde::{Deserialize, Serialize}; pub use session_manager::CollectStrategy; pub use store::{ CrossSigningKeyExport, CryptoStoreError, SecretImportError, SecretInfo, TrackedUser, @@ -112,3 +113,25 @@ matrix_sdk_test::init_tracing_for_tests!(); #[cfg(feature = "uniffi")] uniffi::setup_scaffolding!(); + +/// The trust level in the sender's device that is required to decrypt an +/// event. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum TrustRequirement { + /// Decrypt events from everyone regardless of trust. + Untrusted, + /// Only decrypt events from cross-signed devices or legacy sessions (Megolm + /// sessions created before we started collecting trust information). + CrossSignedOrLegacy, + /// Only decrypt events from cross-signed devices. + CrossSigned, +} + +/// Settings for decrypting messages +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DecryptionSettings { + /// The trust level in the sender's device that is required to decrypt the + /// event. If the sender's device is not sufficiently trusted, + /// [`MegolmError::SenderIdentityNotTrusted`] will be returned. + pub sender_device_trust_requirement: TrustRequirement, +} From 7b71d3ca1b8a242457448369d83457ef84430f62 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Sep 2024 23:12:43 -0400 Subject: [PATCH 027/979] crypto: add error code for sender device not sufficiently trusted on decryption --- .../src/deserialized_responses.rs | 16 ++++++++++++++++ crates/matrix-sdk-crypto/src/error.rs | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 1f7501e2047..928a8eb561f 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -189,6 +189,22 @@ pub enum VerificationLevel { None(DeviceLinkProblem), } +impl fmt::Display for VerificationLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let display = match self { + VerificationLevel::UnverifiedIdentity => "The sender's identity was not verified", + VerificationLevel::PreviouslyVerified => { + "The sender's identity was previously verified but has changed" + } + VerificationLevel::UnsignedDevice => { + "The sending device was not signed by the user's identity" + } + VerificationLevel::None(..) => "The sending device is not known", + }; + write!(f, "{}", display) + } +} + /// The sub-enum containing detailed information on why we were not able to link /// a message back to a device. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 0fcead5a609..849538b94c5 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -14,6 +14,7 @@ use std::collections::BTreeMap; +use matrix_sdk_common::deserialized_responses::VerificationLevel; use ruma::{CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId}; use serde::{ser::SerializeMap, Serializer}; use serde_json::Error as SerdeError; @@ -115,6 +116,12 @@ pub enum MegolmError { /// The storage layer returned an error. #[error(transparent)] Store(#[from] CryptoStoreError), + + /// An encrypted message wasn't decrypted, because the sender's + /// cross-signing identity did not satisfy the requested + /// [`crate::TrustRequirement`]. + #[error("decryption failed because trust requirement not satisfied: {0}")] + SenderIdentityNotTrusted(VerificationLevel), } /// Decryption failed because of a mismatch between the identity keys of the From 62d4abd454ff8ea05b9d146f8b1b8beb6fe9b8d0 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Sep 2024 23:17:20 -0400 Subject: [PATCH 028/979] crypto: add DecryptionSettings parameter to functions --- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 11 +++- crates/matrix-sdk-base/src/client.rs | 8 +-- crates/matrix-sdk-crypto/src/machine/mod.rs | 30 +++++++---- .../tests/decryption_verification_state.rs | 13 +++-- .../src/machine/tests/mod.rs | 52 ++++++++++++++----- crates/matrix-sdk-ui/src/timeline/traits.rs | 7 ++- crates/matrix-sdk/src/room/mod.rs | 30 ++++++----- 7 files changed, 108 insertions(+), 43 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 89e0695ec39..b8920f6dd90 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -17,7 +17,8 @@ use matrix_sdk_crypto::{ decrypt_room_key_export, encrypt_room_key_export, olm::ExportedRoomKey, store::{BackupDecryptionKey, Changes}, - LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentities, + DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, TrustRequirement, + UserIdentities, }; use ruma::{ api::{ @@ -881,7 +882,13 @@ impl OlmMachine { let event: Raw<_> = serde_json::from_str(&event)?; let room_id = RoomId::parse(room_id)?; - let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decrypted = self.runtime.block_on(self.inner.decrypt_room_event( + &event, + &room_id, + &decryption_settings, + ))?; if handle_verification_events { if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() { diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index be8df36bb5d..d3838d8a13f 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -28,8 +28,8 @@ use eyeball_im::{Vector, VectorDiff}; use futures_util::Stream; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::{ - store::DynCryptoStore, CollectStrategy, EncryptionSettings, EncryptionSyncChanges, OlmError, - OlmMachine, ToDeviceRequest, + store::DynCryptoStore, CollectStrategy, DecryptionSettings, EncryptionSettings, + EncryptionSyncChanges, OlmError, OlmMachine, ToDeviceRequest, TrustRequirement, }; #[cfg(feature = "e2e-encryption")] use ruma::events::{ @@ -324,8 +324,10 @@ impl BaseClient { let olm = self.olm_machine().await; let Some(olm) = olm.as_ref() else { return Ok(None) }; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; let event: SyncTimelineEvent = - olm.decrypt_room_event(event.cast_ref(), room_id).await?.into(); + olm.decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await?.into(); if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.event.deserialize() { match &e { diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 7e951615669..47e88040bc1 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -93,8 +93,8 @@ use crate::{ }, utilities::timestamp_to_iso8601, verification::{Verification, VerificationMachine, VerificationRequest}, - CrossSigningKeyExport, CryptoStoreError, DeviceData, KeysQueryRequest, LocalTrust, - SignatureError, ToDeviceRequest, + CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, KeysQueryRequest, + LocalTrust, SignatureError, ToDeviceRequest, TrustRequirement, }; /// State machine implementation of the Olm/Megolm encryption protocol used for @@ -1597,6 +1597,7 @@ impl OlmMachine { room_id: &RoomId, event: &EncryptedEvent, content: &SupportedEventEncryptionSchemes<'_>, + decryption_settings: &DecryptionSettings, ) -> MegolmResult<(JsonObject, EncryptionInfo)> { let session = self.get_inbound_group_session_or_error(room_id, content.session_id()).await?; @@ -1670,8 +1671,9 @@ impl OlmMachine { &self, event: &Raw, room_id: &RoomId, + decryption_settings: &DecryptionSettings, ) -> MegolmResult { - self.decrypt_room_event_inner(event, room_id, true).await + self.decrypt_room_event_inner(event, room_id, true, decryption_settings).await } #[instrument(name = "decrypt_room_event", skip_all, fields(?room_id, event_id, origin_server_ts, sender, algorithm, session_id, sender_key))] @@ -1680,6 +1682,7 @@ impl OlmMachine { event: &Raw, room_id: &RoomId, decrypt_unsigned: bool, + decryption_settings: &DecryptionSettings, ) -> MegolmResult { let event = event.deserialize()?; @@ -1707,7 +1710,8 @@ impl OlmMachine { }; Span::current().record("session_id", content.session_id()); - let result = self.decrypt_megolm_events(room_id, &event, &content).await; + let result = + self.decrypt_megolm_events(room_id, &event, &content, decryption_settings).await; if let Err(e) = &result { #[cfg(feature = "automatic-room-key-forwarding")] @@ -1732,8 +1736,9 @@ impl OlmMachine { let mut unsigned_encryption_info = None; if decrypt_unsigned { // Try to decrypt encrypted unsigned events. - unsigned_encryption_info = - self.decrypt_unsigned_events(&mut decrypted_event, room_id).await; + unsigned_encryption_info = self + .decrypt_unsigned_events(&mut decrypted_event, room_id, decryption_settings) + .await; } let event = serde_json::from_value::>(decrypted_event.into())?; @@ -1759,6 +1764,7 @@ impl OlmMachine { &self, main_event: &mut JsonObject, room_id: &RoomId, + decryption_settings: &DecryptionSettings, ) -> Option> { let unsigned = main_event.get_mut("unsigned")?.as_object_mut()?; let mut unsigned_encryption_info: Option< @@ -1768,7 +1774,9 @@ impl OlmMachine { // Search for an encrypted event in `m.replace`, an edit. let location = UnsignedEventLocation::RelationsReplace; let replace = location.find_mut(unsigned); - if let Some(decryption_result) = self.decrypt_unsigned_event(replace, room_id).await { + if let Some(decryption_result) = + self.decrypt_unsigned_event(replace, room_id, decryption_settings).await + { unsigned_encryption_info .get_or_insert_with(Default::default) .insert(location, decryption_result); @@ -1779,7 +1787,7 @@ impl OlmMachine { let location = UnsignedEventLocation::RelationsThreadLatestEvent; let thread_latest_event = location.find_mut(unsigned); if let Some(decryption_result) = - self.decrypt_unsigned_event(thread_latest_event, room_id).await + self.decrypt_unsigned_event(thread_latest_event, room_id, decryption_settings).await { unsigned_encryption_info .get_or_insert_with(Default::default) @@ -1800,6 +1808,7 @@ impl OlmMachine { &'a self, event: Option<&'a mut Value>, room_id: &'a RoomId, + decryption_settings: &'a DecryptionSettings, ) -> BoxFuture<'a, Option> { Box::pin(async move { let event = event?; @@ -1813,7 +1822,10 @@ impl OlmMachine { } let raw_event = serde_json::from_value(event.clone()).ok()?; - match self.decrypt_room_event_inner(&raw_event, room_id, false).await { + match self + .decrypt_room_event_inner(&raw_event, room_id, false, decryption_settings) + .await + { Ok(decrypted_event) => { // Replace the encrypted event. *event = serde_json::to_value(decrypted_event.event).ok()?; diff --git a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs index ad3e8ec9520..03333ac6dd1 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs @@ -45,7 +45,8 @@ use crate::{ CrossSigningKey, EventEncryptionAlgorithm, }, utilities::json_convert, - CryptoStoreError, EncryptionSettings, LocalTrust, OlmMachine, UserIdentities, + CryptoStoreError, DecryptionSettings, EncryptionSettings, LocalTrust, OlmMachine, + TrustRequirement, UserIdentities, }; #[async_test] @@ -110,8 +111,14 @@ async fn test_decryption_verification_state() { let event = json_convert(&event).unwrap(); - let encryption_info = - bob.decrypt_room_event(&event, room_id).await.unwrap().encryption_info.unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let encryption_info = bob + .decrypt_room_event(&event, room_id, &decryption_settings) + .await + .unwrap() + .encryption_info + .unwrap(); assert_eq!( VerificationState::Unverified(VerificationLevel::UnsignedDevice), diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index a9351ed84a7..3d355fd603a 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -70,8 +70,8 @@ use crate::{ }, utilities::json_convert, verification::tests::bob_id, - Account, DeviceData, EncryptionSettings, MegolmError, OlmError, OutgoingRequests, - ToDeviceRequest, + Account, DecryptionSettings, DeviceData, EncryptionSettings, MegolmError, OlmError, + OutgoingRequests, ToDeviceRequest, TrustRequirement, }; mod decryption_verification_state; @@ -553,8 +553,15 @@ async fn test_megolm_encryption() { let event = json_convert(&event).unwrap(); - let decrypted_event = - bob.decrypt_room_event(&event, room_id).await.unwrap().event.deserialize().unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decrypted_event = bob + .decrypt_room_event(&event, room_id, &decryption_settings) + .await + .unwrap() + .event + .deserialize() + .unwrap(); if let AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage( MessageLikeEvent::Original(OriginalMessageLikeEvent { sender, content, .. }), @@ -663,7 +670,9 @@ async fn test_withheld_unverified() { }); let room_event = json_convert(&room_event).unwrap(); - let decrypt_result = bob.decrypt_room_event(&room_event, room_id).await; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decrypt_result = bob.decrypt_room_event(&room_event, room_id, &decryption_settings).await; assert_matches!(&decrypt_result, Err(MegolmError::MissingRoomKey(Some(_)))); @@ -690,7 +699,12 @@ async fn test_decrypt_unencrypted_event() { let event = json_convert(&event).unwrap(); // decrypt_room_event should return an error - assert_matches!(bob.decrypt_room_event(&event, room_id).await, Err(MegolmError::JsonError(..))); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + assert_matches!( + bob.decrypt_room_event(&event, room_id, &decryption_settings).await, + Err(MegolmError::JsonError(..)) + ); // so should get_room_event_encryption_info assert_matches!( @@ -871,7 +885,10 @@ async fn test_query_ratcheted_key() { let room_event = json_convert(&room_event).unwrap(); - let decrypt_error = bob.decrypt_room_event(&room_event, room_id).await.unwrap_err(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decrypt_error = + bob.decrypt_room_event(&room_event, room_id, &decryption_settings).await.unwrap_err(); if let MegolmError::Decryption(vodo_error) = decrypt_error { if let vodozemac::megolm::DecryptionError::UnknownMessageIndex(_, _) = vodo_error { @@ -1000,8 +1017,10 @@ async fn test_room_key_with_fake_identity_keys() { }); let event = json_convert(&event).unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; assert_matches!( - alice.decrypt_room_event(&event, room_id).await, + alice.decrypt_room_event(&event, room_id, &decryption_settings).await, Err(MegolmError::MismatchedIdentityKeys { .. }) ); } @@ -1266,7 +1285,10 @@ async fn test_unsigned_decryption() { let raw_encrypted_event = json_convert(&first_message_encrypted_event).unwrap(); // Bob has the room key, so first message should be decrypted successfully. - let raw_decrypted_event = bob.decrypt_room_event(&raw_encrypted_event, room_id).await.unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let raw_decrypted_event = + bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); assert_matches!( @@ -1318,7 +1340,8 @@ async fn test_unsigned_decryption() { // Bob does not have the second room key, so second message should fail to // decrypt. - let raw_decrypted_event = bob.decrypt_room_event(&raw_encrypted_event, room_id).await.unwrap(); + let raw_decrypted_event = + bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); assert_matches!( @@ -1361,7 +1384,8 @@ async fn test_unsigned_decryption() { bob.store().save_inbound_group_sessions(&[group_session]).await.unwrap(); // Second message should decrypt now. - let raw_decrypted_event = bob.decrypt_room_event(&raw_encrypted_event, room_id).await.unwrap(); + let raw_decrypted_event = + bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); assert_matches!( @@ -1425,7 +1449,8 @@ async fn test_unsigned_decryption() { // Bob does not have the third room key, so third message should fail to // decrypt. - let raw_decrypted_event = bob.decrypt_room_event(&raw_encrypted_event, room_id).await.unwrap(); + let raw_decrypted_event = + bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); assert_matches!( @@ -1472,7 +1497,8 @@ async fn test_unsigned_decryption() { bob.store().save_inbound_group_sessions(&[group_session]).await.unwrap(); // Third message should decrypt now. - let raw_decrypted_event = bob.decrypt_room_event(&raw_encrypted_event, room_id).await.unwrap(); + let raw_decrypted_event = + bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); assert_matches!( diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index c55973d1c6e..706b1a742fe 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -16,6 +16,8 @@ use std::future::Future; use futures_util::FutureExt as _; use indexmap::IndexMap; +#[cfg(all(test, feature = "e2e-encryption"))] +use matrix_sdk::crypto::{DecryptionSettings, TrustRequirement}; #[cfg(feature = "e2e-encryption")] use matrix_sdk::{deserialized_responses::TimelineEvent, Result}; use matrix_sdk::{event_cache::paginator::PaginableRoom, BoxFuture, Room}; @@ -292,7 +294,10 @@ impl Decryptor for Room { impl Decryptor for (matrix_sdk_base::crypto::OlmMachine, ruma::OwnedRoomId) { async fn decrypt_event_impl(&self, raw: &Raw) -> Result { let (olm_machine, room_id) = self; - let event = olm_machine.decrypt_room_event(raw.cast_ref(), room_id).await?; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let event = + olm_machine.decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings).await?; Ok(event) } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index a68bc366a92..6a389b1b57b 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -14,6 +14,8 @@ use futures_util::{ future::{try_join, try_join_all}, stream::FuturesUnordered, }; +#[cfg(feature = "e2e-encryption")] +use matrix_sdk_base::crypto::{DecryptionSettings, TrustRequirement}; use matrix_sdk_base::{ deserialized_responses::{ RawAnySyncOrStrippedState, RawSyncOrStrippedState, SyncOrStrippedState, TimelineEvent, @@ -1235,18 +1237,22 @@ impl Room { let machine = self.client.olm_machine().await; let machine = machine.as_ref().ok_or(Error::NoOlmMachine)?; - let mut event = - match machine.decrypt_room_event(event.cast_ref(), self.inner.room_id()).await { - Ok(event) => event, - Err(e) => { - self.client - .encryption() - .backups() - .maybe_download_room_key(self.room_id().to_owned(), event.clone()); - - return Err(e.into()); - } - }; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let mut event = match machine + .decrypt_room_event(event.cast_ref(), self.inner.room_id(), &decryption_settings) + .await + { + Ok(event) => event, + Err(e) => { + self.client + .encryption() + .backups() + .maybe_download_room_key(self.room_id().to_owned(), event.clone()); + + return Err(e.into()); + } + }; event.push_actions = self.event_push_actions(&event.event).await?; From 98a79de8119520c80372a08b464aa26d80a50fc2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Sep 2024 23:17:55 -0400 Subject: [PATCH 029/979] crypto: check trust requirement when decrypting --- crates/matrix-sdk-crypto/src/machine/mod.rs | 60 ++++- .../tests/decryption_verification_state.rs | 234 +++++++++++++++++- 2 files changed, 286 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 47e88040bc1..5a2214566f5 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -934,8 +934,6 @@ impl OlmMachine { &self, room_id: &RoomId, ) -> OlmResult<()> { - use crate::olm::SenderData; - let (_, session) = self .inner .group_session_manager @@ -957,8 +955,6 @@ impl OlmMachine { &self, room_id: &RoomId, ) -> OlmResult { - use crate::olm::SenderData; - let (_, session) = self .inner .group_session_manager @@ -1613,6 +1609,13 @@ impl OlmMachine { match result { Ok((decrypted_event, _)) => { let encryption_info = self.get_encryption_info(&session, &event.sender).await?; + + self.check_sender_trust_requirement( + &session, + &encryption_info, + &decryption_settings.sender_device_trust_requirement, + )?; + Ok((decrypted_event, encryption_info)) } Err(error) => Err( @@ -1637,6 +1640,55 @@ impl OlmMachine { } } + /// Check that the sender of a Megolm session satisfies the trust + /// requirement from the decryption settings. + fn check_sender_trust_requirement( + &self, + session: &InboundGroupSession, + encryption_info: &EncryptionInfo, + trust_requirement: &TrustRequirement, + ) -> MegolmResult<()> { + /// Get the error from the encryption information. + fn encryption_info_to_error(encryption_info: &EncryptionInfo) -> MegolmResult<()> { + // When this is called, the verification state *must* be unverified, + // otherwise the sender_data would have been SenderVerified + let VerificationState::Unverified(verification_level) = + &encryption_info.verification_state + else { + unreachable!("inconsistent verification state"); + }; + Err(MegolmError::SenderIdentityNotTrusted(verification_level.clone())) + } + + match trust_requirement { + TrustRequirement::Untrusted => Ok(()), + + TrustRequirement::CrossSignedOrLegacy => match &session.sender_data { + // Reject if the sender was previously verified, but changed + // their identity and is not verified any more. + SenderData::SenderUnverifiedButPreviouslyVerified(..) => Err( + MegolmError::SenderIdentityNotTrusted(VerificationLevel::PreviouslyVerified), + ), + SenderData::SenderUnverified(..) => Ok(()), + SenderData::SenderVerified(..) => Ok(()), + SenderData::DeviceInfo { legacy_session: true, .. } => Ok(()), + SenderData::UnknownDevice { legacy_session: true, .. } => Ok(()), + _ => encryption_info_to_error(encryption_info), + }, + + TrustRequirement::CrossSigned => match &session.sender_data { + // Reject if the sender was previously verified, but changed + // their identity and is not verified any more. + SenderData::SenderUnverifiedButPreviouslyVerified(..) => Err( + MegolmError::SenderIdentityNotTrusted(VerificationLevel::PreviouslyVerified), + ), + SenderData::SenderUnverified(..) => Ok(()), + SenderData::SenderVerified(..) => Ok(()), + _ => encryption_info_to_error(encryption_info), + }, + } + } + /// Attempt to retrieve an inbound group session from the store. /// /// If the session is not found, checks for withheld reports, and returns a diff --git a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs index 03333ac6dd1..8ce5cd12075 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs @@ -36,17 +36,17 @@ use crate::{ tests, }, olm::{InboundGroupSession, OutboundGroupSession, SenderData}, - store::Changes, + store::{Changes, IdentityChanges}, types::{ events::{ room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme}, ToDeviceEvent, }, - CrossSigningKey, EventEncryptionAlgorithm, + CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, SelfSigningPubkey, }, utilities::json_convert, - CryptoStoreError, DecryptionSettings, EncryptionSettings, LocalTrust, OlmMachine, - TrustRequirement, UserIdentities, + CryptoStoreError, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, OlmMachine, + OtherUserIdentityData, OutgoingRequests, TrustRequirement, UserIdentities, }; #[async_test] @@ -380,3 +380,229 @@ async fn test_verification_states_multiple_device() { assert_eq!(VerificationState::Unverified(VerificationLevel::UnverifiedIdentity), state); } + +/// Test that the trust requirement is checked when decrypting an event. +/// +/// Set the sender data to various values, and test that we can or can't +/// decrypt, depending on what the trust requirement is. +#[async_test] +async fn test_decryption_trust_requirement() { + let (alice, bob) = get_machine_pair_with_setup_sessions_test_helper( + tests::alice_id(), + tests::user_id(), + false, + ) + .await; + let room_id = room_id!("!test:example.org"); + let (event, session_id) = encrypt_message(&alice, room_id, &bob, "Secret message").await; + + // Set the SenderData on the megolm session used to encrypt `event` to + // `DeviceInfo` (ie, we have the device keys but no cross-signing + // information). Events sent on such a session should be decryptable only + // when the trust requirement allows untrusted or legacy sessions. + let mut session = + bob.store().get_inbound_group_session(room_id, &session_id).await.unwrap().unwrap(); + session.sender_data = SenderData::DeviceInfo { + device_keys: alice + .get_device(alice.user_id(), alice.device_id(), None) + .await + .unwrap() + .unwrap() + .as_device_keys() + .clone(), + legacy_session: false, + }; + bob.store().save_inbound_group_sessions(&[session.clone()]).await.unwrap(); + + check_decryption_trust_requirement( + &bob, + &event, + room_id, + &[ + (TrustRequirement::Untrusted, true), + (TrustRequirement::CrossSignedOrLegacy, false), + (TrustRequirement::CrossSigned, false), + ], + ) + .await; + + // Bob later gets Alice's identity and cross-signed device keys + bob.bootstrap_cross_signing(false).await.unwrap(); + set_up_alice_cross_signing(&alice, &bob).await; + + // Since the sending device is now cross-signed by Alice, it should be + // decryptable in all modes + check_decryption_trust_requirement( + &bob, + &event, + room_id, + &[ + (TrustRequirement::Untrusted, true), + (TrustRequirement::CrossSignedOrLegacy, true), + (TrustRequirement::CrossSigned, true), + ], + ) + .await; +} + +/// Test that the trust requirement is correctly handled when a user's +/// cross-signing identity changes. +#[async_test] +async fn test_decryption_trust_with_identity_change() { + let (alice, bob) = get_machine_pair_with_setup_sessions_test_helper( + tests::alice_id(), + tests::user_id(), + false, + ) + .await; + bob.bootstrap_cross_signing(false).await.unwrap(); + let room_id = room_id!("!test:example.org"); + let (event, session_id) = encrypt_message(&alice, room_id, &bob, "Secret message").await; + set_up_alice_cross_signing(&alice, &bob).await; + + // Simulate Alice's cross-signing key changing after having been verified by + // setting the `previously_verified` flag + let alice_identity = + bob.store().get_identity(alice.user_id()).await.unwrap().unwrap().other().unwrap(); + alice_identity.mark_as_previously_verified().await.unwrap(); + + // Bob receives the Megolm session and message + let mut session = + bob.store().get_inbound_group_session(room_id, &session_id).await.unwrap().unwrap(); + session.sender_data = + SenderData::UnknownDevice { legacy_session: false, owner_check_failed: false }; + bob.store().save_inbound_group_sessions(&[session.clone()]).await.unwrap(); + + // In this case, the message should not be decryptable except for when we + // accept untrusted devices + check_decryption_trust_requirement( + &bob, + &event, + room_id, + &[ + (TrustRequirement::Untrusted, true), + (TrustRequirement::CrossSignedOrLegacy, false), + (TrustRequirement::CrossSigned, false), + ], + ) + .await; +} + +/// Helper function to set up Alice's cross-signing, and save her keys in Bob's +/// storage. +async fn set_up_alice_cross_signing(alice: &OlmMachine, bob: &OlmMachine) { + let cross_signing_requests = alice.bootstrap_cross_signing(false).await.unwrap(); + let upload_signing_keys_req = cross_signing_requests.upload_signing_keys_req; + let alice_msk: MasterPubkey = upload_signing_keys_req.master_key.unwrap().try_into().unwrap(); + let alice_ssk: SelfSigningPubkey = + upload_signing_keys_req.self_signing_key.unwrap().try_into().unwrap(); + let upload_keys_req = cross_signing_requests.upload_keys_req.unwrap().clone(); + assert_let!( + OutgoingRequests::KeysUpload(device_upload_request) = upload_keys_req.request.as_ref() + ); + bob.store() + .save_device_data(&[DeviceData::try_from( + &device_upload_request + .device_keys + .as_ref() + .unwrap() + .deserialize_as::() + .unwrap(), + ) + .unwrap()]) + .await + .unwrap(); + bob.store() + .save_changes(Changes { + identities: IdentityChanges { + new: vec![OtherUserIdentityData::new(alice_msk.clone(), alice_ssk.clone()) + .unwrap() + .into()], + ..Default::default() + }, + ..Default::default() + }) + .await + .unwrap(); +} + +/// Helper function that encrypts a message and shares the Megolm session +/// with a recipient. +async fn encrypt_message( + sender: &OlmMachine, + room_id: &RoomId, + recipient: &OlmMachine, + plaintext: &str, +) -> (Raw, String) { + let to_device_requests = sender + .share_room_key(room_id, iter::once(recipient.user_id()), EncryptionSettings::default()) + .await + .unwrap(); + + let event = ToDeviceEvent::new( + sender.user_id().to_owned(), + tests::to_device_requests_to_content(to_device_requests), + ); + + let group_session = recipient + .store() + .with_transaction(|mut tr| async { + let res = + recipient.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + Ok((tr, res)) + }) + .await + .unwrap() + .inbound_group_session + .unwrap(); + recipient.store().save_inbound_group_sessions(&[group_session.clone()]).await.unwrap(); + + let content = RoomMessageEventContent::text_plain(plaintext); + + let encrypted_content = sender + .encrypt_room_event(room_id, AnyMessageLikeEventContent::RoomMessage(content.clone())) + .await + .unwrap(); + + let event = json!({ + "event_id": "$xxxxx:example.org", + "origin_server_ts": MilliSecondsSinceUnixEpoch::now(), + "sender": sender.user_id(), + "type": "m.room.encrypted", + "content": encrypted_content, + }); + let event = json_convert(&event).unwrap(); + + (event, group_session.session_id().to_owned()) +} + +/// Helper function that checks whether a message is decryptable under different +/// trust requirements. +/// +/// `tests` is a list of tuples, where the first element of the tuple is the +/// trust requirement to check, and the second element indicates whether +/// decryption should succeed (`true`) or fail (`false`). +async fn check_decryption_trust_requirement( + bob: &OlmMachine, + event: &Raw, + room_id: &RoomId, + tests: &[(TrustRequirement, bool)], +) { + for (trust_requirement, is_ok) in tests { + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: *trust_requirement }; + if *is_ok { + assert!( + bob.decrypt_room_event(event, room_id, &decryption_settings).await.is_ok(), + "Decryption did not succeed with {:?}", + trust_requirement, + ); + } else { + assert!( + bob.decrypt_room_event(event, room_id, &decryption_settings).await.is_err(), + "Decryption succeeded with {:?}", + trust_requirement, + ); + } + } +} From dfb67c88e69ad63d52ef0ad689a43a61763e9a69 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Sep 2024 23:18:13 -0400 Subject: [PATCH 030/979] crypto: add changelog --- crates/matrix-sdk-crypto/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 96804cef64f..c07c91b4071 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -36,6 +36,14 @@ Breaking changes: the CryptoStore, meaning that, once upgraded, it will not be possible to roll back applications to earlier versions without breaking user sessions. +- `OlmMachine::decrypt_room_event` now takes a `DecryptionSettings` argument, + which includes a `TrustRequirement` indicating the required trust level for + the sending device. When it is called with `TrustRequirement` other than + `TrustRequirement::Unverified`, it may return the new + `MegolmError::SenderIdentityNotTrusted` variant if the sending device does not + satisfy the required trust level. + ([#3899](https://github.com/matrix-org/matrix-rust-sdk/pull/3899)) + - Change the structure of the `SenderData` enum to separate variants for previously-verified, unverified and verified. ([#3877](https://github.com/matrix-org/matrix-rust-sdk/pull/3877)) From 30d3d9d26cce6e41108a532ab2866db1c29bbf4e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 14:53:28 +0100 Subject: [PATCH 031/979] crypto: expose `InboundGroupSession.sender_data` We need write access to this in the integration tests --- crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index cf17841a093..ac4d269fed1 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -127,7 +127,7 @@ pub struct InboundGroupSession { /// the session, or, if we can use that device information to find the /// sender's cross-signing identity, holds the user ID and cross-signing /// key. - pub(crate) sender_data: SenderData, + pub sender_data: SenderData, /// The Room this GroupSession belongs to pub room_id: OwnedRoomId, From 6bc98873140aa3440d43f42b2a1319e52c9ad219 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 10:38:25 +0100 Subject: [PATCH 032/979] crypto: fix memorystore groupsession batch query If the previous session is removed from the list, we should still be able to continue iterating through the *rest* of the list. --- .../matrix-sdk-crypto/src/store/integration_tests.rs | 5 +++++ crates/matrix-sdk-crypto/src/store/memorystore.rs | 11 +++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index d696751bd5d..24098de3f0e 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -648,6 +648,11 @@ macro_rules! cryptostore_integration_tests { // Check that there are exactly two results in the batch assert_eq!(sessions_1_k_batch.len(), 2); + // Modify one of the results, to check that that doesn't break iteration + let mut last_session = last_session.clone(); + last_session.sender_data = SenderData::unknown(); + store.save_inbound_group_sessions(vec![last_session], None).await.unwrap(); + previous_last_session_id = Some(last_session.session_id().to_owned()); sessions_1_k.append(&mut sessions_1_k_batch); } diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 1a8b00ffca1..b773d819d94 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -408,13 +408,12 @@ impl CryptoStore for MemoryStore { match after_session_id { None => 0, Some(id) => { - let idx = sessions + // We're looking for the first session with a session ID strictly after `id`; if + // there are none, the end of the array. + sessions .iter() - .position(|session| session.session_id() == id) - .map(|idx| idx + 1); - - // If `after_session_id` was not found in the array, go to the end of the array - idx.unwrap_or(sessions.len()) + .position(|session| session.session_id() > id.as_str()) + .unwrap_or(sessions.len()) } } }; From 385c2b8e7167086453840850088d5320926c3661 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 30 Aug 2024 16:19:51 +0100 Subject: [PATCH 033/979] crypto: Expose sender_data_finder module as pub(crate) This module has a number of useful types (in particular, error types). Rather than addding even more types to the top level module, let's export the `sender_data_finder` module as a whole. --- crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs | 3 +-- crates/matrix-sdk-crypto/src/olm/mod.rs | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs index 55eee23d74b..54f6b661b03 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs @@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize}; mod inbound; mod outbound; mod sender_data; -mod sender_data_finder; +pub(crate) mod sender_data_finder; pub use inbound::{InboundGroupSession, PickledInboundGroupSession}; pub(crate) use outbound::ShareState; @@ -26,7 +26,6 @@ pub use outbound::{ EncryptionSettings, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo, }; pub use sender_data::{KnownSenderData, SenderData, SenderDataType}; -pub(crate) use sender_data_finder::SenderDataFinder; use thiserror::Error; pub use vodozemac::megolm::{ExportedSessionKey, SessionKey}; use vodozemac::{megolm::SessionKeyDecodeError, Curve25519PublicKey}; diff --git a/crates/matrix-sdk-crypto/src/olm/mod.rs b/crates/matrix-sdk-crypto/src/olm/mod.rs index 2f47d65eea6..c4c01d3d430 100644 --- a/crates/matrix-sdk-crypto/src/olm/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/mod.rs @@ -25,12 +25,15 @@ mod utility; pub use account::{Account, OlmMessageHash, PickledAccount, StaticAccountData}; pub(crate) use account::{OlmDecryptionInfo, SessionType}; +pub(crate) use group_sessions::{ + sender_data_finder::{self, SenderDataFinder}, + ShareState, +}; pub use group_sessions::{ BackedUpRoomKey, EncryptionSettings, ExportedRoomKey, InboundGroupSession, KnownSenderData, OutboundGroupSession, PickledInboundGroupSession, PickledOutboundGroupSession, SenderData, SenderDataType, SessionCreationError, SessionExportError, SessionKey, ShareInfo, }; -pub(crate) use group_sessions::{SenderDataFinder, ShareState}; pub use session::{PickledSession, Session}; pub use signing::{CrossSigningStatus, PickledCrossSigningIdentity, PrivateCrossSigningIdentity}; pub(crate) use utility::{SignedJsonObject, VerifyJson}; From 3c27f83857fda410d762dae593c7027d1f316629 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 30 Aug 2024 16:50:28 +0100 Subject: [PATCH 034/979] crypto: update sender data on `/keys/query` responses When we receive an `/keys/query` response, look for existing inboundgroupsessions created by updated devices, and see if we can update any of their senderdata settings. --- .../src/identities/manager.rs | 218 +++++++++++++++++- .../src/store/integration_tests.rs | 3 +- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index ba701ed0288..f3e7c40a9e8 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -32,7 +32,7 @@ use tracing::{debug, enabled, info, instrument, trace, warn, Level}; use crate::{ error::OlmResult, identities::{DeviceData, OtherUserIdentityData, OwnUserIdentityData, UserIdentityData}, - olm::PrivateCrossSigningIdentity, + olm::{InboundGroupSession, PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType}, requests::KeysQueryRequest, store::{ caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager, @@ -162,6 +162,24 @@ impl IdentityManager { self.store.save_changes(changes).await?; + // Update the sender data on any existing inbound group sessions based on the + // changes in this response. + // + // `update_sender_data_from_device_changes` relies on being able to look up the + // user identities from the store, so this has to happen *after* the + // changes from `handle_cross_signing_keys` are saved. + // + // Note: it might be possible for this to race against session creation. If a + // new session is received at the same time as a `/keys/query` response is being + // processed, it could be saved without up-to-date sender data, but it might be + // saved too late for it to be picked up by + // `update_sender_data_from_device_changes`. However, this should be rare, + // since, in general, /sync responses which might create a new session + // are not processed at the same time as /keys/query responses (assuming + // that the application does not call `OlmMachine::receive_sync_changes` + // at the same time as `OlmMachine::mark_request_as_sent`). + self.update_sender_data_from_device_changes(&devices).await?; + // if this request is one of those we expected to be in flight, pass the // sequence number back to the store so that it can mark devices up to // date @@ -1019,6 +1037,113 @@ impl IdentityManager { _ => Ok(None), } } + + /// Given a list of changed devices, update any [`InboundGroupSession`]s + /// which were sent from those devices and which do not have complete + /// sender data. + async fn update_sender_data_from_device_changes( + &self, + device_changes: &DeviceChanges, + ) -> Result<(), CryptoStoreError> { + for device in device_changes.new.iter().chain(device_changes.changed.iter()) { + // 1. Look for InboundGroupSessions from the device whose sender_data is + // UnknownDevice. For such sessions, we now have the device, and can update + // the sender_data accordingly. + // + // In theory, we only need to do this for new devices. In practice, I'm a bit + // worried about races leading us to getting stuck in the + // UnknownDevice state, so we'll paper over that by doing this check + // on device updates too. + self.update_sender_data_for_sessions_for_device(device, SenderDataType::UnknownDevice) + .await?; + + // 2. If, and only if, the device is now correctly cross-signed (ie, + // device.is_cross_signed_by_owner() is true, and we have the master + // cross-signing key for the owner), look for InboundGroupSessions from the + // device whose sender_data is DeviceInfo. We can also update the sender_data + // for these sessions. + // + // In theory, we can skip a couple of steps of the SenderDataFinder algorithm, + // because we're doing the cross-signing check here. In practice, + // it's *way* easier just to use the same logic. + let device_owner_identity = self.store.get_user_identity(device.user_id()).await?; + if device_owner_identity.is_some_and(|id| device.is_cross_signed_by_owner(&id)) { + self.update_sender_data_for_sessions_for_device(device, SenderDataType::DeviceInfo) + .await?; + } + } + + Ok(()) + } + + /// Given a device, look for [`InboundGroupSession`]s whose sender data is + /// in the given state, and update it. + #[instrument(skip(self))] + async fn update_sender_data_for_sessions_for_device( + &self, + device: &DeviceData, + sender_data_type: SenderDataType, + ) -> Result<(), CryptoStoreError> { + const IGS_BATCH_SIZE: usize = 50; + + let Some(curve_key) = device.curve25519_key() else { return Ok(()) }; + + let mut last_session_id: Option = None; + loop { + let mut sessions = self + .store + .get_inbound_group_sessions_for_device_batch( + curve_key, + sender_data_type, + last_session_id, + IGS_BATCH_SIZE, + ) + .await?; + + if sessions.is_empty() { + // end of the session list + return Ok(()); + } + + last_session_id = None; + for session in &mut sessions { + last_session_id = Some(session.session_id().to_owned()); + self.update_sender_data_for_session(session, device).await?; + } + self.store.save_inbound_group_sessions(&sessions).await?; + } + } + + /// Update the sender data on the given inbound group session, using the + /// given device data. + #[instrument(skip(self, device, session), fields(session_id = session.session_id()))] + async fn update_sender_data_for_session( + &self, + session: &mut InboundGroupSession, + device: &DeviceData, + ) -> Result<(), CryptoStoreError> { + debug!("Updating existing InboundGroupSession with new SenderData"); + use crate::olm::sender_data_finder::SessionDeviceCheckError::*; + + match SenderDataFinder::find_using_device_data(&self.store, device.clone(), session).await { + Ok(sender_data) => { + session.sender_data = sender_data; + } + Err(CryptoStoreError(e)) => { + return Err(e); + } + Err(MismatchedIdentityKeys(e)) => { + warn!( + ?session, + ?device, + "cannot update existing InboundGroupSession due to ownership error: {}", + e + ); + } + }; + + Ok(()) + } } /// Log information about what changed after processing a /keys/query response. @@ -2286,4 +2411,95 @@ pub(crate) mod tests { // The latch should be set now assert!(bob_identity.was_previously_verified()); } + + mod update_sender_data { + use assert_matches::assert_matches; + use matrix_sdk_test::async_test; + use ruma::room_id; + + use super::{device_id, manager_test_helper}; + use crate::{ + identities::manager::testing::{other_user_id, user_id}, + olm::{InboundGroupSession, SenderData}, + store::{Changes, DeviceChanges}, + Account, DeviceData, EncryptionSettings, + }; + + #[async_test] + async fn test_adds_device_info_to_existing_sessions() { + let manager = manager_test_helper(user_id(), device_id()).await; + + // Given that we have lots of sessions in the store, from each of two devices + let account1 = Account::new(user_id()); + let account2 = Account::new(other_user_id()); + + let mut account1_sessions = Vec::new(); + for _ in 0..60 { + account1_sessions.push(create_inbound_group_session(&account1).await); + } + let mut account2_sessions = Vec::new(); + for _ in 0..60 { + account2_sessions.push(create_inbound_group_session(&account2).await); + } + manager + .store + .save_changes(Changes { + inbound_group_sessions: [account1_sessions.clone(), account2_sessions.clone()] + .concat(), + ..Default::default() + }) + .await + .unwrap(); + + // When we get an update for one device + let device_data = DeviceData::from_account(&account1); + manager + .update_sender_data_from_device_changes(&DeviceChanges { + changed: vec![device_data], + ..Default::default() + }) + .await + .unwrap(); + + // Then those sessions should be updated + for session in account1_sessions { + let updated = manager + .store + .get_inbound_group_session(session.room_id(), session.session_id()) + .await + .unwrap() + .expect("Could not find session after update"); + assert_matches!( + updated.sender_data, + SenderData::DeviceInfo { .. }, + "incorrect sender data for session {}", + session.session_id() + ); + } + + // ... and those from the other account should not + for session in account2_sessions { + let updated = manager + .store + .get_inbound_group_session(session.room_id(), session.session_id()) + .await + .unwrap() + .expect("Could not find session after update"); + assert_matches!(updated.sender_data, SenderData::UnknownDevice { .. }); + } + } + + /// Create an InboundGroupSession sent from the given account + async fn create_inbound_group_session(account: &Account) -> InboundGroupSession { + let (_, igs) = account + .create_group_session_pair( + room_id!("!test:room"), + EncryptionSettings::default(), + SenderData::unknown(), + ) + .await + .unwrap(); + igs + } + } } diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 24098de3f0e..5cbb61bfb17 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -648,12 +648,13 @@ macro_rules! cryptostore_integration_tests { // Check that there are exactly two results in the batch assert_eq!(sessions_1_k_batch.len(), 2); + previous_last_session_id = Some(last_session.session_id().to_owned()); + // Modify one of the results, to check that that doesn't break iteration let mut last_session = last_session.clone(); last_session.sender_data = SenderData::unknown(); store.save_inbound_group_sessions(vec![last_session], None).await.unwrap(); - previous_last_session_id = Some(last_session.session_id().to_owned()); sessions_1_k.append(&mut sessions_1_k_batch); } From 73486b2b7bd2f6812051847da38dd0c4e95d6823 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 30 Aug 2024 17:03:26 +0100 Subject: [PATCH 035/979] crypto: update senderdata integration tests Extend the integration tests for megolm sender data to check that we update existing inbound group sessions when we get a `/keys/query` response. --- .../src/identities/manager.rs | 5 +- .../src/machine/test_helpers.rs | 44 +++++++- .../src/machine/tests/megolm_sender_data.rs | 106 +++++++++++++++++- 3 files changed, 150 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index f3e7c40a9e8..28ab62eba87 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -1122,11 +1122,14 @@ impl IdentityManager { session: &mut InboundGroupSession, device: &DeviceData, ) -> Result<(), CryptoStoreError> { - debug!("Updating existing InboundGroupSession with new SenderData"); use crate::olm::sender_data_finder::SessionDeviceCheckError::*; match SenderDataFinder::find_using_device_data(&self.store, device.clone(), session).await { Ok(sender_data) => { + debug!( + "Updating existing InboundGroupSession with new SenderData {:?}", + sender_data + ); session.sender_data = sender_data; } Err(CryptoStoreError(e)) => { diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 4d0c9059809..c1ace327dea 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -17,17 +17,24 @@ use std::collections::BTreeMap; +use as_variant::as_variant; use matrix_sdk_test::{ruma_response_from_json, test_json}; use ruma::{ - api::client::keys::{claim_keys, get_keys, upload_keys}, + api::client::keys::{ + claim_keys, get_keys, get_keys::v3::Response as KeysQueryResponse, upload_keys, + }, device_id, encryption::OneTimeKey, events::dummy::ToDeviceDummyEventContent, serde::Raw, user_id, DeviceId, OwnedDeviceKeyId, TransactionId, UserId, }; +use serde_json::json; -use crate::{store::Changes, types::events::ToDeviceEvent, DeviceData, OlmMachine}; +use crate::{ + store::Changes, types::events::ToDeviceEvent, CrossSigningBootstrapRequests, DeviceData, + OlmMachine, OutgoingRequests, +}; /// These keys need to be periodically uploaded to the server. type OneTimeKeys = BTreeMap>; @@ -182,3 +189,36 @@ pub async fn create_session( let response = claim_keys::v3::Response::new(one_time_keys); machine.inner.session_manager.create_sessions(&response).await.unwrap(); } + +/// Given a set of requests returned by `bootstrap_cross_signing` for one user, +/// return a `/keys/query` response which might be returned to another user/ +pub fn bootstrap_requests_to_keys_query_response( + bootstrap_requests: CrossSigningBootstrapRequests, +) -> KeysQueryResponse { + let mut kq_response = json!({}); + + // If we have a master key, add that to the response + if let Some(key) = bootstrap_requests.upload_signing_keys_req.master_key { + let user_id = key.user_id.clone(); + kq_response["master_keys"] = json!({user_id: key}); + } + + // If we have a self-signing key, add that + if let Some(key) = bootstrap_requests.upload_signing_keys_req.self_signing_key { + let user_id = key.user_id.clone(); + kq_response["self_signing_keys"] = json!({user_id: key}); + } + + // And if we have a device, add that + if let Some(dk) = bootstrap_requests + .upload_keys_req + .and_then(|req| as_variant!(req.request.as_ref(), OutgoingRequests::KeysUpload).cloned()) + .and_then(|keys_upload_request| keys_upload_request.device_keys) + { + let user_id: String = dk.get_field("user_id").unwrap().unwrap(); + let device_id: String = dk.get_field("device_id").unwrap().unwrap(); + kq_response["device_keys"] = json!({user_id: { device_id: dk }}); + } + + ruma_response_from_json(&kq_response) +} diff --git a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs index 2ac6602e4e2..d4a12a998ae 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs @@ -26,13 +26,16 @@ use serde_json::json; use crate::{ machine::{ - test_helpers::get_machine_pair_with_setup_sessions_test_helper, + test_helpers::{ + bootstrap_requests_to_keys_query_response, + get_machine_pair_with_setup_sessions_test_helper, + }, tests::to_device_requests_to_content, }, olm::{InboundGroupSession, SenderData}, store::RoomKeyInfo, types::events::{room::encrypted::ToDeviceEncryptedEventContent, EventType, ToDeviceEvent}, - EncryptionSettings, EncryptionSyncChanges, OlmMachine, Session, + DeviceData, EncryptionSettings, EncryptionSyncChanges, OlmMachine, Session, }; /// Test the behaviour when a megolm session is received from an unknown device, @@ -105,6 +108,105 @@ async fn test_receive_megolm_session_from_known_device() { ); } +/// If we have a megolm session from an unknown device, test what happens when +/// we get a /keys/query response that includes that device. +#[async_test] +async fn test_update_unknown_device_senderdata_on_keys_query() { + // Given we have a megolm session from an unknown device + + let (alice, bob) = get_machine_pair().await; + let mut bob_room_keys_received_stream = Box::pin(bob.store().room_keys_received_stream()); + + // `get_machine_pair_with_setup_sessions_test_helper` tells Bob about Alice's + // device keys, so to run this test, we need to make him forget them. + forget_devices_for_user(&bob, alice.user_id()).await; + + // Alice starts a megolm session and shares the key with Bob, *without* sending + // the sender data. + let room_id = room_id!("!test:example.org"); + let event = create_and_share_session_without_sender_data(&alice, &bob, room_id).await; + + // Bob receives the to-device message + receive_to_device_event(&bob, &event).await; + + // and now Bob should know about the session. + let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); + let session = get_inbound_group_session_or_panic(&bob, &room_key_info).await; + + // Double-check that it is, in fact, an unknown device session. + assert_matches!(session.sender_data, SenderData::UnknownDevice { .. }); + + // When Bob gets a /keys/query response for Alice, that includes the + // sending device... + + let alice_device = DeviceData::from_machine_test_helper(&alice).await.unwrap(); + let kq_response = json!({ + "device_keys": { alice.user_id() : { alice.device_id(): alice_device.as_device_keys()}} + }); + bob.receive_keys_query_response( + &TransactionId::new(), + &matrix_sdk_test::ruma_response_from_json(&kq_response), + ) + .await + .unwrap(); + + // Then Bob should have received an update about the session, and it should now + // be `SenderData::DeviceInfo` + let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); + let session = get_inbound_group_session_or_panic(&bob, &room_key_info).await; + + assert_matches!( + session.sender_data, + SenderData::DeviceInfo {legacy_session, ..} => { + assert!(legacy_session); // TODO: change when https://github.com/matrix-org/matrix-rust-sdk/pull/3785 lands + } + ); +} + +/// If we have a megolm session from an unsigned device, test what happens when +/// we get a /keys/query response that includes that device. +#[async_test] +async fn test_update_device_info_senderdata_on_keys_query() { + // Given we have a megolm session from an unsigned device + + let (alice, bob) = get_machine_pair().await; + let mut bob_room_keys_received_stream = Box::pin(bob.store().room_keys_received_stream()); + + // Alice starts a megolm session and shares the key with Bob + let room_id = room_id!("!test:example.org"); + + let to_device_requests = alice + .share_room_key(room_id, iter::once(bob.user_id()), EncryptionSettings::default()) + .await + .unwrap(); + let event = ToDeviceEvent::new( + alice.user_id().to_owned(), + to_device_requests_to_content(to_device_requests), + ); + // Bob receives the to-device message + receive_to_device_event(&bob, &event).await; + + // and now Bob should know about the session. + let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); + let session = get_inbound_group_session_or_panic(&bob, &room_key_info).await; + + // Double-check that it is, in fact, an unverified device session. + assert_matches!(session.sender_data, SenderData::DeviceInfo { .. }); + + // When Bob receives a /keys/query response for Alice that includes a verifiable + // signature for her device + let bootstrap_requests = alice.bootstrap_cross_signing(false).await.unwrap(); + let kq_response = bootstrap_requests_to_keys_query_response(bootstrap_requests); + bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + + // Then Bob should have received an update about the session, and it should now + // be `SenderData::SenderUnverified` + let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); + let session = get_inbound_group_session_or_panic(&bob, &room_key_info).await; + + assert_matches!(session.sender_data, SenderData::SenderUnverified(_)); +} + /// Convenience wrapper for [`get_machine_pair_with_setup_sessions_test_helper`] /// using standard user ids. async fn get_machine_pair() -> (OlmMachine, OlmMachine) { From f7ee6434757442c59f691adecde263b6383276c0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 30 Aug 2024 17:06:29 +0100 Subject: [PATCH 036/979] crypto: update changelog --- crates/matrix-sdk-crypto/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index c07c91b4071..279c6a8c0b7 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,10 @@ Changes: +- Update `SenderData` on existing inbound group sessions when we receive + updates via `/keys/query`. + ([#3849](https://github.com/matrix-org/matrix-rust-sdk/pull/3849)) + - Add message IDs to all outgoing to-device messages encrypted by `matrix-sdk-crypto`. The `message-ids` feature of `matrix-sdk-crypto` and `matrix-sdk-base` is now a no-op. From 12f36d59726fcb5688125fee60ba7b7a64ddc4bb Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Sep 2024 17:54:36 +0200 Subject: [PATCH 037/979] timeline: document and rename some concepts around pending poll events --- .../src/timeline/controller/state.rs | 8 ++-- .../src/timeline/event_handler.rs | 6 +-- crates/matrix-sdk-ui/src/timeline/polls.rs | 42 +++++++++++-------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 5735dac6086..39ae407bd3f 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -36,7 +36,7 @@ use crate::{ TimelineItemPosition, }, event_item::RemoteEventOrigin, - polls::PollPendingEvents, + polls::PendingPollEvents, reactions::Reactions, read_receipts::ReadReceipts, traits::RoomDataProvider, @@ -701,7 +701,7 @@ pub(in crate::timeline) struct TimelineMetadata { pub all_events: VecDeque, pub reactions: Reactions, - pub poll_pending_events: PollPendingEvents, + pub pending_poll_events: PendingPollEvents, pub fully_read_event: Option, /// Whether we have a fully read-marker item in the timeline, that's up to @@ -726,7 +726,7 @@ impl TimelineMetadata { all_events: Default::default(), next_internal_id: Default::default(), reactions: Default::default(), - poll_pending_events: Default::default(), + pending_poll_events: Default::default(), fully_read_event: Default::default(), // It doesn't make sense to set this to false until we fill the `fully_read_event` // field, otherwise we'll keep on exiting early in `Self::update_read_marker`. @@ -745,7 +745,7 @@ impl TimelineMetadata { } self.all_events.clear(); self.reactions.clear(); - self.poll_pending_events.clear(); + self.pending_poll_events.clear(); self.fully_read_event = None; // We forgot about the fully read marker right above, so wait for a new one // before attempting to update it for each new timeline item. diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index eafb5ced76f..9735c27f263 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -639,7 +639,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { if let Flow::Remote { event_id, .. } = &self.ctx.flow { // Applying the cache to remote events only because local echoes // don't have an event ID that could be referenced by responses yet. - self.meta.poll_pending_events.apply(event_id, &mut poll_state); + self.meta.pending_poll_events.apply_pending(event_id, &mut poll_state); } if should_add { @@ -649,7 +649,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { fn handle_poll_response(&mut self, c: UnstablePollResponseEventContent) { let Some((item_pos, item)) = rfind_event_by_id(self.items, &c.relates_to.event_id) else { - self.meta.poll_pending_events.add_response( + self.meta.pending_poll_events.add_response( &c.relates_to.event_id, &self.ctx.sender, self.ctx.timestamp, @@ -678,7 +678,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { fn handle_poll_end(&mut self, c: UnstablePollEndEventContent) { let Some((item_pos, item)) = rfind_event_by_id(self.items, &c.relates_to.event_id) else { - self.meta.poll_pending_events.add_end(&c.relates_to.event_id, self.ctx.timestamp); + self.meta.pending_poll_events.mark_as_ended(&c.relates_to.event_id, self.ctx.timestamp); return; }; diff --git a/crates/matrix-sdk-ui/src/timeline/polls.rs b/crates/matrix-sdk-ui/src/timeline/polls.rs index df95133de6d..5f3eb867ed1 100644 --- a/crates/matrix-sdk-ui/src/timeline/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/polls.rs @@ -139,23 +139,26 @@ impl From for NewUnstablePollStartEventContent { } } -/// Acts as a cache for poll response and poll end events handled before their -/// start event has been handled. +/// Cache holding poll response and end events handled before their poll start +/// event has been handled. #[derive(Clone, Debug, Default)] -pub(super) struct PollPendingEvents { - pub(super) pending_poll_responses: HashMap>, - pub(super) pending_poll_ends: HashMap, +pub(super) struct PendingPollEvents { + /// Responses to a poll (identified by the poll's start event id). + responses: HashMap>, + + /// Mapping of a poll (identified by its start event's id) to its end date. + end_dates: HashMap, } -impl PollPendingEvents { +impl PendingPollEvents { pub(super) fn add_response( &mut self, - start_id: &EventId, + start_event_id: &EventId, sender: &UserId, timestamp: MilliSecondsSinceUnixEpoch, content: &UnstablePollResponseEventContent, ) { - self.pending_poll_responses.entry(start_id.to_owned()).or_default().push(ResponseData { + self.responses.entry(start_event_id.to_owned()).or_default().push(ResponseData { sender: sender.to_owned(), timestamp, answers: content.poll_response.answers.clone(), @@ -163,22 +166,27 @@ impl PollPendingEvents { } pub(super) fn clear(&mut self) { - self.pending_poll_ends.clear(); - self.pending_poll_responses.clear(); + self.end_dates.clear(); + self.responses.clear(); } - pub(super) fn add_end(&mut self, start_id: &EventId, timestamp: MilliSecondsSinceUnixEpoch) { - self.pending_poll_ends.insert(start_id.to_owned(), timestamp); + /// Mark a poll as finished by inserting its poll date. + pub(super) fn mark_as_ended( + &mut self, + start_event_id: &EventId, + timestamp: MilliSecondsSinceUnixEpoch, + ) { + self.end_dates.insert(start_event_id.to_owned(), timestamp); } /// Dumps all response and end events present in the cache that belong to /// the given start_event_id into the given poll_state. - pub(super) fn apply(&mut self, start_event_id: &EventId, poll_state: &mut PollState) { - if let Some(pending_responses) = self.pending_poll_responses.get_mut(start_event_id) { - poll_state.response_data.append(pending_responses); + pub(super) fn apply_pending(&mut self, start_event_id: &EventId, poll_state: &mut PollState) { + if let Some(pending_responses) = self.responses.remove(start_event_id) { + poll_state.response_data.extend(pending_responses); } - if let Some(pending_end) = self.pending_poll_ends.get(start_event_id) { - poll_state.end_event_timestamp = Some(*pending_end) + if let Some(pending_end) = self.end_dates.remove(start_event_id) { + poll_state.end_event_timestamp = Some(pending_end); } } } From 552df0e4c610fc5d5c745ac3a3f577c00af0a837 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Sep 2024 18:00:23 +0200 Subject: [PATCH 038/979] timeline(tests): use the event factory in a few more places --- .../tests/integration/timeline/edit.rs | 114 ++++++++---------- 1 file changed, 48 insertions(+), 66 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index c9a0bf75fd4..dd3992e44e7 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -18,26 +18,27 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::{FutureExt, StreamExt}; -use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; +use matrix_sdk::{ + config::SyncSettings, + test_utils::{events::EventFactory, logged_in_client_with_server}, +}; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, - ALICE, BOB, + async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, }; use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineDetails, TimelineItemContent}; use ruma::{ - assign, event_id, + event_id, events::{ poll::unstable_start::{ NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers, UnstablePollStartContentBlock, }, - relation::InReplyTo, room::message::{ - MessageType, Relation, ReplacementMetadata, RoomMessageEventContent, - RoomMessageEventContentWithoutRelation, TextMessageEventContent, + MessageType, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, + TextMessageEventContent, }, }, - room_id, user_id, + room_id, }; use serde_json::json; use stream_assert::assert_next_matches; @@ -53,9 +54,10 @@ use crate::mock_sync; async fn test_edit() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; - let event_builder = EventBuilder::new(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let f = EventFactory::new(); + let mut sync_builder = SyncResponseBuilder::new(); sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); @@ -70,13 +72,10 @@ async fn test_edit() { let (_, mut timeline_stream) = timeline.subscribe().await; let event_id = event_id!("$msda7m:localhost"); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( - event_builder.make_sync_message_event_with_id( - &ALICE, - event_id, - RoomMessageEventContent::text_plain("hello"), - ), - )); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("hello").sender(&ALICE).event_id(event_id)), + ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; let _response = client.sync_once(sync_settings.clone()).await.unwrap(); @@ -95,18 +94,11 @@ async fn test_edit() { sync_builder.add_joined_room( JoinedRoomBuilder::new(room_id) - .add_timeline_event(event_builder.make_sync_message_event( - &BOB, - RoomMessageEventContent::text_html("Test", "Test"), - )) + .add_timeline_event(f.text_html("Test", "Test").sender(&BOB)) .add_timeline_event( - event_builder.make_sync_message_event( - &ALICE, - RoomMessageEventContent::text_plain("hi").make_replacement( - ReplacementMetadata::new(event_id.to_owned(), None), - None, - ), - ), + f.text_msg("* hi") + .sender(&ALICE) + .edit(event_id, RoomMessageEventContent::text_plain("hi").into()), ), ); @@ -271,7 +263,6 @@ async fn test_edit_local_echo() { async fn test_send_edit() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; - let event_builder = EventBuilder::new(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); let mut sync_builder = SyncResponseBuilder::new(); @@ -288,14 +279,14 @@ async fn test_send_edit() { let (_, mut timeline_stream) = timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( - event_builder.make_sync_message_event_with_id( - // Same user as the logged_in_client - user_id!("@example:localhost"), - event_id!("$original_event"), - RoomMessageEventContent::text_plain("Hello, World!"), + let f = EventFactory::new(); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.text_msg("Hello, World!") + .sender(client.user_id().unwrap()) + .event_id(event_id!("$original_event")), ), - )); + ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; let _response = client.sync_once(sync_settings.clone()).await.unwrap(); @@ -347,7 +338,6 @@ async fn test_send_edit() { async fn test_send_reply_edit() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; - let event_builder = EventBuilder::new(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); let mut sync_builder = SyncResponseBuilder::new(); @@ -364,23 +354,16 @@ async fn test_send_reply_edit() { let (_, mut timeline_stream) = timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; + let f = EventFactory::new(); let fst_event_id = event_id!("$original_event"); sync_builder.add_joined_room( JoinedRoomBuilder::new(room_id) - .add_timeline_event(event_builder.make_sync_message_event_with_id( - &ALICE, - fst_event_id, - RoomMessageEventContent::text_plain("Hello, World!"), - )) - .add_timeline_event(event_builder.make_sync_message_event( - // Same user as the logged_in_client - user_id!("@example:localhost"), - assign!(RoomMessageEventContent::text_plain("Hello, Alice!"), { - relates_to: Some(Relation::Reply { - in_reply_to: InReplyTo::new(fst_event_id.to_owned()), - }) - }), - )), + .add_timeline_event(f.text_msg("Hello, World!").sender(&ALICE).event_id(fst_event_id)) + .add_timeline_event( + f.text_msg("Hello, Alice!") + .reply_to(fst_event_id) + .sender(client.user_id().unwrap()), + ), ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; @@ -442,7 +425,6 @@ async fn test_send_reply_edit() { async fn test_send_edit_poll() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; - let event_builder = EventBuilder::new(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); let mut sync_builder = SyncResponseBuilder::new(); @@ -464,17 +446,18 @@ async fn test_send_edit_poll() { UnstablePollAnswer::new("1", "no"), ]) .unwrap(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( - event_builder.make_sync_message_event_with_id( - // Same user as the logged_in_client - user_id!("@example:localhost"), - event_id!("$original_event"), - NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new( + + let f = EventFactory::new(); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.event(NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new( "Test", poll_answers, - )), + ))) + .sender(client.user_id().unwrap()) + .event_id(event_id!("$original_event")), ), - )); + ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; let _response = client.sync_once(sync_settings.clone()).await.unwrap(); @@ -535,7 +518,6 @@ async fn test_send_edit_poll() { async fn test_send_edit_when_timeline_is_clear() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; - let event_builder = EventBuilder::new(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); let mut sync_builder = SyncResponseBuilder::new(); @@ -552,12 +534,12 @@ async fn test_send_edit_when_timeline_is_clear() { let (_, mut timeline_stream) = timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; - let raw_original_event = event_builder.make_sync_message_event_with_id( - // Same user as the logged_in_client - user_id!("@example:localhost"), - event_id!("$original_event"), - RoomMessageEventContent::text_plain("Hello, World!"), - ); + let f = EventFactory::new(); + let raw_original_event = f + .text_msg("Hello, World!") + .sender(client.user_id().unwrap()) + .event_id(event_id!("$original_event")) + .into_raw_sync(); sync_builder.add_joined_room( JoinedRoomBuilder::new(room_id).add_timeline_event(raw_original_event.clone()), ); From b9b8de7ff15589b2d9174f949f0ceed932769ac4 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 31 Jul 2024 15:27:16 +0100 Subject: [PATCH 039/979] crypto: Mark all new SenderData info as non-legacy Since we now have a clear idea of the structure, and anything we create now should be usable in future. --- .../src/machine/tests/megolm_sender_data.rs | 8 +-- .../src/olm/group_sessions/inbound.rs | 2 +- .../src/olm/group_sessions/sender_data.rs | 23 ++------- .../olm/group_sessions/sender_data_finder.rs | 50 ++++--------------- 4 files changed, 18 insertions(+), 65 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs index d4a12a998ae..0c91b97471a 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs @@ -66,7 +66,7 @@ async fn test_receive_megolm_session_from_unknown_device() { assert_matches!( session.sender_data, SenderData::UnknownDevice {legacy_session, owner_check_failed} => { - assert!(legacy_session); // TODO: change when https://github.com/matrix-org/matrix-rust-sdk/pull/3785 lands + assert!(!legacy_session); assert!(!owner_check_failed); } ); @@ -102,8 +102,8 @@ async fn test_receive_megolm_session_from_known_device() { assert_matches!( session.sender_data, - SenderData::DeviceInfo {legacy_session, ..} => { - assert!(legacy_session); // TODO: change when https://github.com/matrix-org/matrix-rust-sdk/pull/3785 lands + SenderData::DeviceInfo { legacy_session, .. } => { + assert!(!legacy_session); } ); } @@ -158,7 +158,7 @@ async fn test_update_unknown_device_senderdata_on_keys_query() { assert_matches!( session.sender_data, SenderData::DeviceInfo {legacy_session, ..} => { - assert!(legacy_session); // TODO: change when https://github.com/matrix-org/matrix-rust-sdk/pull/3785 lands + assert!(!legacy_session); } ); } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index ac4d269fed1..f43e013e442 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -771,7 +771,7 @@ mod tests { "signing_key":{"ed25519":"wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww"}, "sender_data":{ "UnknownDevice":{ - "legacy_session":true + "legacy_session":false } }, "room_id":"!test:localhost", diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index ad509f924bd..a9cdbb1235b 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -95,26 +95,12 @@ pub enum SenderData { impl SenderData { /// Create a [`SenderData`] which contains no device info. pub fn unknown() -> Self { - Self::UnknownDevice { - // TODO: when we have implemented all of SenderDataFinder, - // legacy_session should be set to false, but for now we leave - // it as true because we might lose device info while - // this code is still in transition. - legacy_session: true, - owner_check_failed: false, - } + Self::UnknownDevice { legacy_session: false, owner_check_failed: false } } /// Create a [`SenderData`] which contains device info. pub fn device_info(device_keys: DeviceKeys) -> Self { - Self::DeviceInfo { - device_keys, - // TODO: when we have implemented all of SenderDataFinder, - // legacy_session should be set to false, but for now we leave - // it as true because we might lose device info while - // this code is still in transition. - legacy_session: true, - } + Self::DeviceInfo { device_keys, legacy_session: false } } /// Create a [`SenderData`] with a known but unverified sender, where the @@ -209,9 +195,8 @@ impl SenderData { /// Used when deserialising and the sender_data property is missing. /// If we are deserialising an InboundGroupSession session with missing /// sender_data, this must be a legacy session (i.e. it was created before we -/// started tracking sender data). We set its legacy flag to true, and set it up -/// to be retried soon, so we can populate it with trust information if it is -/// available. +/// started tracking sender data). We set its legacy flag to true, so we can +/// populate it with trust information if it is available later. impl Default for SenderData { fn default() -> Self { Self::legacy() diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs index 807f0fb042a..8ef51d6d9be 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs @@ -204,10 +204,7 @@ impl<'a> SenderDataFinder<'a> { let sender_data = SenderData::UnknownDevice { // This is not a legacy session since we did attempt to look // up its sender data at the time of reception. - // legacy_session: false, - // TODO: we set legacy to true for now, since our implementation is incomplete, so - // we may not have had a proper chance to look up the sender data. - legacy_session: true, + legacy_session: false, owner_check_failed: false, }; Ok(sender_data) @@ -238,18 +235,12 @@ impl<'a> SenderDataFinder<'a> { (true, true) => self.device_is_cross_signed_by_sender(sender_device), (true, false) => { // F (we have device keys, but they are not signed by the sender) - SenderData::DeviceInfo { - device_keys: sender_device.as_device_keys().clone(), - legacy_session: true, // TODO: change to false when we have all the retry code - } + SenderData::device_info(sender_device.as_device_keys().clone()) } (false, _) => { // Step E (the device does not own the session) // Give up: something is wrong with the session. - SenderData::UnknownDevice { - legacy_session: true, // TODO: change to false when all SenderData work is done - owner_check_failed: true, - } + SenderData::UnknownDevice { legacy_session: false, owner_check_failed: true } } }) } @@ -285,11 +276,7 @@ impl<'a> SenderDataFinder<'a> { // treat it as if the device was not signed by this master key. // error!("MasterPubkey for user {user_id} does not contain any keys!",); - - SenderData::DeviceInfo { - device_keys: sender_device.as_device_keys().clone(), - legacy_session: true, // TODO: change to false when retries etc. are done - } + SenderData::device_info(sender_device.as_device_keys().clone()) } } } @@ -429,10 +416,7 @@ mod tests { // Then we get back no useful information at all assert_let!(SenderData::UnknownDevice { legacy_session, owner_check_failed } = sender_data); - // TODO: This should not be marked as a legacy session, but for now it is - // because we haven't finished implementing the whole sender_data and - // retry mechanism. - assert!(legacy_session); + assert!(!legacy_session); assert!(!owner_check_failed); } @@ -453,11 +437,7 @@ mod tests { // Then we get back the device keys that were in the event assert_let!(SenderData::DeviceInfo { device_keys, legacy_session } = sender_data); assert_eq!(&device_keys, setup.sender_device.as_device_keys()); - - // TODO: This should not be marked as a legacy session, but for now it is - // because we haven't finished implementing the whole sender_data and - // retry mechanism. - assert!(legacy_session); + assert!(!legacy_session); } #[async_test] @@ -477,11 +457,7 @@ mod tests { // Then we get back the device keys that were in the store assert_let!(SenderData::DeviceInfo { device_keys, legacy_session } = sender_data); assert_eq!(&device_keys, setup.sender_device.as_device_keys()); - - // TODO: This should not be marked as a legacy session, but for now it is - // because we haven't finished implementing the whole sender_data and - // retry mechanism. - assert!(legacy_session); + assert!(!legacy_session); } #[async_test] @@ -501,11 +477,7 @@ mod tests { // check it matches up later. assert_let!(SenderData::DeviceInfo { device_keys, legacy_session } = sender_data); assert_eq!(&device_keys, setup.sender_device.as_device_keys()); - - // TODO: This should not be marked as a legacy session, but for now it is - // because we haven't finished implementing the whole sender_data and - // retry mechanism. - assert!(legacy_session); + assert!(!legacy_session); } #[async_test] @@ -684,11 +656,7 @@ mod tests { // Then we fail to find useful sender data assert_let!(SenderData::UnknownDevice { legacy_session, owner_check_failed } = sender_data); - - // TODO: This should not be marked as a legacy session, but for now it is - // because we haven't finished implementing the whole sender_data and - // retry mechanism. - assert!(legacy_session); + assert!(!legacy_session); // And report that the owner_check_failed assert!(owner_check_failed); From 2c2d8e9ff00146346b3348bb031c042bf13c4a47 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 22:45:10 +0100 Subject: [PATCH 040/979] crypto: log details of our public identity when we update it For debugging, it's useful to have a record of what we believe our own public cross-signing keys to be. Currently, we log the keys at startup if we restore them from the database, but if we subsequently create, or download, a set of keys, they aren't logged. --- .../src/store/crypto_store_wrapper.rs | 20 +++++++++++++++++++ .../src/types/cross_signing/self_signing.rs | 9 +++++++++ .../src/types/cross_signing/user_signing.rs | 9 +++++++++ 3 files changed, 38 insertions(+) diff --git a/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs b/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs index 00965e216d4..a2d1f8fb4e4 100644 --- a/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs +++ b/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs @@ -126,6 +126,26 @@ impl CryptoStoreWrapper { self.store.save_changes(changes).await?; + // If we updated our own public identity, log it for debugging purposes + if tracing::level_enabled!(tracing::Level::DEBUG) { + for updated_identity in + identities.new.iter().chain(identities.changed.iter()).filter_map(|id| id.own()) + { + let master_key = updated_identity.master_key().get_first_key(); + let user_signing_key = updated_identity.user_signing_key().get_first_key(); + let self_signing_key = updated_identity.self_signing_key().get_first_key(); + + debug!( + ?master_key, + ?user_signing_key, + ?self_signing_key, + previously_verified = updated_identity.was_previously_verified(), + verified = updated_identity.is_verified(), + "Stored our own identity" + ); + } + } + if !room_key_updates.is_empty() { // Ignore the result. It can only fail if there are no listeners. let _ = self.room_keys_received_sender.send(room_key_updates); diff --git a/crates/matrix-sdk-crypto/src/types/cross_signing/self_signing.rs b/crates/matrix-sdk-crypto/src/types/cross_signing/self_signing.rs index 5419f54386e..88661cc1051 100644 --- a/crates/matrix-sdk-crypto/src/types/cross_signing/self_signing.rs +++ b/crates/matrix-sdk-crypto/src/types/cross_signing/self_signing.rs @@ -2,6 +2,7 @@ use std::collections::btree_map::Iter; use ruma::{encryption::KeyUsage, OwnedDeviceKeyId, UserId}; use serde::{Deserialize, Serialize}; +use vodozemac::Ed25519PublicKey; use super::{CrossSigningKey, SigningKey}; use crate::{ @@ -33,6 +34,14 @@ impl SelfSigningPubkey { &self.0.usage } + /// Get the first available self signing key. + /// + /// There's usually only a single key so this will usually fetch the + /// only key. + pub fn get_first_key(&self) -> Option { + self.0.get_first_key_and_id().map(|(_, k)| k) + } + /// Verify that the [`DeviceKeys`] have a valid signature from this /// self-signing key. pub fn verify_device_keys(&self, device_keys: &DeviceKeys) -> Result<(), SignatureError> { diff --git a/crates/matrix-sdk-crypto/src/types/cross_signing/user_signing.rs b/crates/matrix-sdk-crypto/src/types/cross_signing/user_signing.rs index a4f17be55f5..393ce863c84 100644 --- a/crates/matrix-sdk-crypto/src/types/cross_signing/user_signing.rs +++ b/crates/matrix-sdk-crypto/src/types/cross_signing/user_signing.rs @@ -2,6 +2,7 @@ use std::collections::btree_map::Iter; use ruma::{encryption::KeyUsage, OwnedDeviceKeyId, UserId}; use serde::{Deserialize, Serialize}; +use vodozemac::Ed25519PublicKey; use super::{CrossSigningKey, MasterPubkey, SigningKey}; use crate::{olm::VerifyJson, types::SigningKeys, SignatureError}; @@ -29,6 +30,14 @@ impl UserSigningPubkey { &self.0.keys } + /// Get the first available user-signing key. + /// + /// There's usually only a single key so this will usually fetch the + /// only key. + pub fn get_first_key(&self) -> Option { + self.0.get_first_key_and_id().map(|(_, k)| k) + } + /// Check if the given master key is signed by this user signing key. /// /// # Arguments From fed418d9a84e8159a20fa7ce37af076f436a0207 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 22:58:27 +0100 Subject: [PATCH 041/979] crypto: log when we show a QR code Take the logging that happens when a QR code verification is added to the `verification cache`, and push it down to the `VerificationCache` itself. Doing so means that we will log when we *show* a QR code as well as when we scan it. I would have found this helpful when trying to debug a verification flow this week. --- crates/matrix-sdk-crypto/src/verification/cache.rs | 12 ++++++++++++ .../matrix-sdk-crypto/src/verification/requests.rs | 10 ---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index bbe01266f11..c847abbad3a 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -19,6 +19,8 @@ use std::{ use as_variant::as_variant; use ruma::{DeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId}; +#[cfg(feature = "qrcode")] +use tracing::debug; use tracing::{trace, warn}; use super::{event_enums::OutgoingContent, FlowId, Sas, Verification}; @@ -105,11 +107,21 @@ impl VerificationCache { #[cfg(feature = "qrcode")] pub fn insert_qr(&self, qr: QrVerification) { + debug!( + user_id = ?qr.other_user_id(), + flow_id = qr.flow_id().as_str(), + "Inserting new QR verification" + ); self.insert(qr) } #[cfg(feature = "qrcode")] pub fn replace_qr(&self, qr: QrVerification) { + debug!( + user_id = ?qr.other_user_id(), + flow_id = qr.flow_id().as_str(), + "Replacing existing QR verification" + ); let verification: Verification = qr.into(); self.replace(verification); } diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index a7bd265a0f2..018db26b856 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -449,18 +449,8 @@ impl VerificationRequest { .get_qr(qr_verification.other_user_id(), qr_verification.flow_id().as_str()) .is_some() { - debug!( - user_id = ?self.other_user(), - flow_id = self.flow_id().as_str(), - "Replacing existing QR verification" - ); self.verification_cache.replace_qr(qr_verification.clone()); } else { - debug!( - user_id = ?self.other_user(), - flow_id = self.flow_id().as_str(), - "Inserting new QR verification" - ); self.verification_cache.insert_qr(qr_verification.clone()); } From b1a533a071648e26ba0fda1b762ace3cf246fd23 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 23:01:14 +0100 Subject: [PATCH 042/979] crypto: log flow_id when processing verification requests Attach the flow_id (the transaction ID or message ID from the `request` message) to the span, so that it is displayed alongside loglines that happen when processing the request. --- crates/matrix-sdk-crypto/src/verification/machine.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index 89ecd4f3635..c82d3b7867f 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -27,7 +27,7 @@ use ruma::{ SecondsSinceUnixEpoch, TransactionId, UInt, UserId, }; use tokio::sync::Mutex; -use tracing::{debug, info, instrument, trace, warn}; +use tracing::{debug, info, instrument, trace, warn, Span}; use super::{ cache::{RequestInfo, VerificationCache}, @@ -306,7 +306,7 @@ impl VerificationMachine { Ok(()) } - #[instrument(skip_all)] + #[instrument(skip_all, fields(flow_id))] pub async fn receive_any_event( &self, event: impl Into>, @@ -317,6 +317,7 @@ impl VerificationMachine { // This isn't a verification event, return early. return Ok(()); }; + Span::current().record("flow_id", flow_id.as_str()); let flow_id_mismatch = || { warn!( From a2bfc07eccf89bc55a11c6946813f45e89212150 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 23:03:06 +0100 Subject: [PATCH 043/979] crypto: log the method on an `m.verification.start` message This is the message that tells us whether the other side wants to do QR code or SAS (emoji) verification. Knowing which they have chosen is really helpful for following the flow! --- crates/matrix-sdk-crypto/src/verification/requests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 018db26b856..6fae90ec919 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -1337,6 +1337,7 @@ async fn receive_start( info!( ?sender, device = ?content.from_device(), + method = ?content.method(), "Received a new verification start event", ); From c761a84acd6a48bb073613ab9115972e00761f9a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 23:05:16 +0100 Subject: [PATCH 044/979] crypto: logging during QR code verifications * Upgrade the log when we get the "reciprocate" message (which tells us the other side has scanned our QR code) to debug, instead of trace. * Warn if we get a reciprocate we don't understand * Log when the user confirms that the other side has scanned successfully. --- crates/matrix-sdk-crypto/src/verification/qrcode.rs | 3 ++- crates/matrix-sdk-crypto/src/verification/requests.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/qrcode.rs b/crates/matrix-sdk-crypto/src/verification/qrcode.rs index 76992314267..a5411b6e3e7 100644 --- a/crates/matrix-sdk-crypto/src/verification/qrcode.rs +++ b/crates/matrix-sdk-crypto/src/verification/qrcode.rs @@ -41,7 +41,7 @@ use ruma::{ DeviceId, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId, }; use thiserror::Error; -use tracing::trace; +use tracing::{debug, trace}; use vodozemac::Ed25519PublicKey; use super::{ @@ -328,6 +328,7 @@ impl QrVerification { /// Confirm that the other side has scanned our QR code. pub fn confirm_scanning(&self) -> Option { + debug!("User confirmed other side scanned our QR code"); let mut state = self.state.write(); match &*state { diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 6fae90ec919..94c2b599d59 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -1438,7 +1438,7 @@ async fn receive_start( if let Some(request) = qr_verification.receive_reciprocation(content) { request_state.verification_cache.add_request(request.into()) } - trace!( + debug!( sender = ?identities.device_being_verified.user_id(), device_id = ?identities.device_being_verified.device_id(), verification = ?qr_verification, @@ -1447,6 +1447,7 @@ async fn receive_start( Ok(None) } else { + warn!("Received a QR code reciprocation for an unknown flow"); Ok(None) } } From 88b005ace3d4a80a3c42a895813a2fb05db74a58 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 23:08:05 +0100 Subject: [PATCH 045/979] crypto: clarify logging on conclusion of verification requests * Not verifying the remote device/user is normal: log it at debug rather than info. * On the other hand, if we do verify something, let's log that at info rather than trace. Also fix a comment, while we're here. --- .../matrix-sdk-crypto/src/verification/mod.rs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 3b1b10fdc97..6d48d38e684 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -47,7 +47,7 @@ use ruma::{ }; pub use sas::{AcceptSettings, AcceptedProtocols, EmojiShortAuthString, Sas, SasState}; use tokio::sync::Mutex; -use tracing::{error, info, trace, warn}; +use tracing::{debug, error, info, warn}; use crate::{ error::SignatureError, @@ -502,7 +502,7 @@ impl IdentitiesBeingVerified { self.mark_identity_as_verified(verified_identities).await?; if device.is_none() && identity.is_none() { - // Something wen't wrong if nothing was verified, we use key + // Something went wrong if nothing was verified. We use key // mismatch here, since it's the closest to nothing was verified return Ok(VerificationResult::Cancel(CancelCode::KeyMismatch)); } @@ -634,9 +634,10 @@ impl IdentitiesBeingVerified { if verified_identities.is_some_and(|i| { i.iter().any(|verified| verified.user_id() == identity.user_id()) }) { - trace!( + info!( user_id = ?self.other_user_id(), - "Marking the user identity of as verified." + "The interactive verification process verified the identity of \ + the remote user: marking as verified." ); let should_request_secrets = if let UserIdentityData::Own(i) = &identity { @@ -648,7 +649,10 @@ impl IdentitiesBeingVerified { (Some(identity), should_request_secrets) } else { - info!( + // Note, this is normal. For example, if we're an existing device in a device + // verification, we don't need to verify our identity: instead the verification + // process should verify the new device. + debug!( user_id = ?self.other_user_id(), "The interactive verification process didn't verify \ the user identity of the user that participated in \ @@ -703,20 +707,24 @@ impl IdentitiesBeingVerified { } if verified_devices.is_some_and(|v| v.contains(&device)) { - trace!( + info!( user_id = ?device.user_id(), device_id = ?device.device_id(), - "Marking device as verified.", + "The interactive verification process verified the remote device: marking as verified.", ); device.set_trust_state(LocalTrust::Verified); Ok(Some(device)) } else { - info!( + // Note, this is normal. For example, if we're a new device in a QR code device + // verification, we'll verify the master key but not (directly) the + // remote device. Likewise, in a QR code identity verification, we'll verify the + // master key of the remote user but not (directly) their device. + debug!( user_id = ?device.user_id(), device_id = ?device.device_id(), - "The interactive verification process didn't verify the device", + "The interactive verification process didn't verify the remote device", ); Ok(None) From 32049537389c99eaa8d7062030a862bff48041fb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 4 Sep 2024 23:15:48 +0100 Subject: [PATCH 046/979] crypto: update changelog --- crates/matrix-sdk-crypto/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 279c6a8c0b7..7ec36d90b54 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,10 @@ Changes: +- Miscellaneous improvements to logging for verification and `OwnUserIdentity` + updates. + ([#3949](https://github.com/matrix-org/matrix-rust-sdk/pull/3949)) + - Update `SenderData` on existing inbound group sessions when we receive updates via `/keys/query`. ([#3849](https://github.com/matrix-org/matrix-rust-sdk/pull/3849)) From 3f93324a8577674dde027a48d658e75f62d7ee4c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Sep 2024 15:56:18 +0200 Subject: [PATCH 047/979] timeline(style): gather common code under the same arm branches --- .../matrix-sdk-ui/src/timeline/controller/mod.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 86463ed3b14..fb388b3c963 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -370,14 +370,13 @@ impl TimelineController

{ num_events: u16, ) -> Result { let pagination = match &*self.focus.read().await { - TimelineFocusData::Live => return Err(PaginationError::NotEventFocusMode), + TimelineFocusData::Live | TimelineFocusData::PinnedEvents { .. } => { + return Err(PaginationError::NotEventFocusMode) + } TimelineFocusData::Event { paginator, .. } => paginator .paginate_backward(num_events.into()) .await .map_err(PaginationError::Paginator)?, - TimelineFocusData::PinnedEvents { .. } => { - return Err(PaginationError::NotEventFocusMode) - } }; self.add_events_at(pagination.events, TimelineEnd::Front, RemoteEventOrigin::Pagination) @@ -395,14 +394,13 @@ impl TimelineController

{ num_events: u16, ) -> Result { let pagination = match &*self.focus.read().await { - TimelineFocusData::Live => return Err(PaginationError::NotEventFocusMode), + TimelineFocusData::Live | TimelineFocusData::PinnedEvents { .. } => { + return Err(PaginationError::NotEventFocusMode) + } TimelineFocusData::Event { paginator, .. } => paginator .paginate_forward(num_events.into()) .await .map_err(PaginationError::Paginator)?, - TimelineFocusData::PinnedEvents { .. } => { - return Err(PaginationError::NotEventFocusMode) - } }; self.add_events_at(pagination.events, TimelineEnd::Back, RemoteEventOrigin::Pagination) @@ -799,7 +797,6 @@ impl TimelineController

{ // The event was already marked as sent, that's a broken state, let's // emit an error but also override to the given sent state. if let EventSendState::Sent { event_id: existing_event_id } = &local_item.send_state { - let new_event_id = new_event_id.map(debug); error!(?existing_event_id, ?new_event_id, "Local echo already marked as sent"); } From f978960d302208ad65cbd81ef0652f27315e8f6e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Sep 2024 17:37:12 +0200 Subject: [PATCH 048/979] timeline: don't insert a read marker when all subsequent events have been inserted by ourselves --- .../src/timeline/controller/mod.rs | 1 + .../src/timeline/controller/state.rs | 40 ++++++++++- .../src/timeline/day_dividers.rs | 19 ++--- .../matrix-sdk-ui/src/timeline/tests/echo.rs | 7 +- .../matrix-sdk-ui/src/timeline/tests/virt.rs | 72 ++++++++++++++----- 5 files changed, 110 insertions(+), 29 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index fb388b3c963..e9351f8f323 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -272,6 +272,7 @@ impl TimelineController

{ let state = TimelineState::new( focus_kind, + room_data_provider.own_user_id().to_owned(), room_data_provider.room_version(), internal_id_prefix, unable_to_decrypt_hook, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 39ae407bd3f..16034ea892b 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -70,6 +70,7 @@ pub(in crate::timeline) struct TimelineState { impl TimelineState { pub(super) fn new( timeline_focus: TimelineFocusKind, + own_user_id: OwnedUserId, room_version: RoomVersionId, internal_id_prefix: Option, unable_to_decrypt_hook: Option>, @@ -81,6 +82,7 @@ impl TimelineState { // small enough. items: ObservableVector::with_capacity(32), meta: TimelineMetadata::new( + own_user_id, room_version, internal_id_prefix, unable_to_decrypt_hook, @@ -691,6 +693,9 @@ pub(in crate::timeline) struct TimelineMetadata { /// This value is constant over the lifetime of the metadata. pub room_version: RoomVersionId, + /// The own [`OwnedUserId`] of the client who opened the timeline. + own_user_id: OwnedUserId, + // **** DYNAMIC FIELDS **** /// The next internal identifier for timeline items, used for both local and /// remote echoes. @@ -717,12 +722,14 @@ pub(in crate::timeline) struct TimelineMetadata { impl TimelineMetadata { pub(crate) fn new( + own_user_id: OwnedUserId, room_version: RoomVersionId, internal_id_prefix: Option, unable_to_decrypt_hook: Option>, is_room_encrypted: bool, ) -> Self { Self { + own_user_id, all_events: Default::default(), next_internal_id: Default::default(), reactions: Default::default(), @@ -805,7 +812,38 @@ impl TimelineMetadata { trace!(?fully_read_event, "Updating read marker"); let read_marker_idx = items.iter().rposition(|item| item.is_read_marker()); - let fully_read_event_idx = rfind_event_by_id(items, fully_read_event).map(|(idx, _)| idx); + + let mut fully_read_event_idx = + rfind_event_by_id(items, fully_read_event).map(|(idx, _)| idx); + + if let Some(i) = &mut fully_read_event_idx { + // The item at position `i` is the first item that's fully read, we're about to + // insert a read marker just after it. + // + // Do another forward pass to skip all the events we've sent too. + + // Find the position of the first element… + let next = items + .iter() + .enumerate() + // …strictly *after* the fully read event… + .skip(*i + 1) + // …that's not virtual and not sent by us… + .find(|(_, item)| { + item.as_event().map_or(false, |event| event.sender() != self.own_user_id) + }) + .map(|(i, _)| i); + + if let Some(next) = next { + // `next` point to the first item that's not sent by us, so the *previous* of + // next is the right place where to insert the fully read marker. + *i = next.wrapping_sub(1); + } else { + // There's no event after the read marker that's not sent by us, i.e. the full + // timeline has been read: the fully read marker goes to the end. + *i = items.len().wrapping_sub(1); + } + } match (read_marker_idx, fully_read_event_idx) { (None, None) => { diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index f29fed837ed..ae873e68862 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -642,12 +642,16 @@ mod tests { ) } + fn test_metadata() -> TimelineMetadata { + TimelineMetadata::new(owned_user_id!("@a:b.c"), ruma::RoomVersionId::V11, None, None, false) + } + #[test] fn test_no_trailing_day_divider() { let mut items = ObservableVector::new(); let mut txn = items.transaction(); - let mut meta = TimelineMetadata::new(ruma::RoomVersionId::V11, None, None, false); + let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); let timestamp_next_day = @@ -681,7 +685,7 @@ mod tests { let mut items = ObservableVector::new(); let mut txn = items.transaction(); - let mut meta = TimelineMetadata::new(ruma::RoomVersionId::V11, None, None, false); + let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); let timestamp_next_day = @@ -713,7 +717,7 @@ mod tests { let mut items = ObservableVector::new(); let mut txn = items.transaction(); - let mut meta = TimelineMetadata::new(ruma::RoomVersionId::V11, None, None, false); + let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); let timestamp_next_day = @@ -747,7 +751,7 @@ mod tests { let mut items = ObservableVector::new(); let mut txn = items.transaction(); - let mut meta = TimelineMetadata::new(ruma::RoomVersionId::V11, None, None, false); + let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); let timestamp_next_day = @@ -777,7 +781,7 @@ mod tests { let mut items = ObservableVector::new(); let mut txn = items.transaction(); - let mut meta = TimelineMetadata::new(ruma::RoomVersionId::V11, None, None, false); + let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); @@ -803,7 +807,7 @@ mod tests { let mut items = ObservableVector::new(); let mut txn = items.transaction(); - let mut meta = TimelineMetadata::new(ruma::RoomVersionId::V11, None, None, false); + let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); @@ -827,8 +831,7 @@ mod tests { let mut items = ObservableVector::new(); let mut txn = items.transaction(); - let mut meta = TimelineMetadata::new(ruma::RoomVersionId::V11, None, None, false); - + let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker)); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index f98c8dc8581..ea3a3f77399 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -267,7 +267,7 @@ async fn test_day_divider_removed_after_local_echo_disappeared() { } #[async_test] -async fn test_read_marker_removed_after_local_echo_disappeared() { +async fn test_no_read_marker_with_local_echo() { let event_id = event_id!("$1"); let timeline = TestTimeline::with_room_data_provider( @@ -306,11 +306,10 @@ async fn test_read_marker_removed_after_local_echo_disappeared() { let items = timeline.controller.items().await; - assert_eq!(items.len(), 4); + assert_eq!(items.len(), 3); assert!(items[0].is_day_divider()); assert!(items[1].is_remote_event()); - assert!(items[2].is_read_marker()); - assert!(items[3].is_local_echo()); + assert!(items[2].is_local_echo()); // Cancel the local echo. timeline diff --git a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs index 7a94843c947..1e788a84100 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs @@ -16,6 +16,7 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use chrono::{Datelike, Local, TimeZone}; use eyeball_im::VectorDiff; +use futures_util::{FutureExt, StreamExt as _}; use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::{ event_id, @@ -24,7 +25,7 @@ use ruma::{ use stream_assert::assert_next_matches; use super::TestTimeline; -use crate::timeline::{TimelineItemKind, VirtualTimelineItem}; +use crate::timeline::{traits::RoomDataProvider as _, TimelineItemKind, VirtualTimelineItem}; #[async_test] async fn test_day_divider() { @@ -91,60 +92,99 @@ async fn test_update_read_marker() { let timeline = TestTimeline::new(); let mut stream = timeline.subscribe().await; - let f = &timeline.factory; + let own_user = timeline.controller.room_data_provider.own_user_id().to_owned(); - timeline.handle_live_event(f.text_msg("A").sender(&ALICE)).await; + let f = &timeline.factory; + timeline.handle_live_event(f.text_msg("A").sender(&own_user)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); - let first_event_id = item.as_event().unwrap().event_id().unwrap().to_owned(); + let event_id1 = item.as_event().unwrap().event_id().unwrap().to_owned(); let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); assert!(day_divider.is_day_divider()); - timeline.controller.handle_fully_read_marker(first_event_id.clone()).await; + timeline.controller.handle_fully_read_marker(event_id1.to_owned()).await; // Nothing should happen, the marker cannot be added at the end. timeline.handle_live_event(f.text_msg("B").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); - let second_event_id = item.as_event().unwrap().event_id().unwrap().to_owned(); + let event_id2 = item.as_event().unwrap().event_id().unwrap().to_owned(); // Now the read marker appears after the first event. let item = assert_next_matches!(stream, VectorDiff::Insert { index: 2, value } => value); assert_matches!(item.as_virtual(), Some(VirtualTimelineItem::ReadMarker)); - timeline.controller.handle_fully_read_marker(second_event_id.clone()).await; + timeline.controller.handle_fully_read_marker(event_id2.clone()).await; // The read marker is removed but not reinserted, because it cannot be added at // the end. assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); - timeline.handle_live_event(f.text_msg("C").sender(&ALICE)).await; + timeline.handle_live_event(f.text_msg("C").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); - let third_event_id = item.as_event().unwrap().event_id().unwrap().to_owned(); + let event_id3 = item.as_event().unwrap().event_id().unwrap().to_owned(); // Now the read marker is reinserted after the second event. let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 3, value } => value); assert_matches!(marker.kind, TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)); - // Nothing should happen if the fully read event is set back to an older event. - timeline.controller.handle_fully_read_marker(first_event_id).await; + // Nothing should happen if the fully read event is set back to an older event + // sent by another user. + timeline.controller.handle_fully_read_marker(event_id1.to_owned()).await; + assert!(stream.next().now_or_never().is_none()); // Nothing should happen if the fully read event isn't found. timeline.controller.handle_fully_read_marker(event_id!("$fake_event_id").to_owned()).await; + assert!(stream.next().now_or_never().is_none()); // Nothing should happen if the fully read event is referring to an event // that has already been marked as fully read. - timeline.controller.handle_fully_read_marker(second_event_id).await; + timeline.controller.handle_fully_read_marker(event_id2).await; + assert!(stream.next().now_or_never().is_none()); - timeline.handle_live_event(f.text_msg("D").sender(&ALICE)).await; + timeline.handle_live_event(f.text_msg("D").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); - item.as_event().unwrap(); + let event_id4 = item.as_event().unwrap().event_id().unwrap().to_owned(); - timeline.controller.handle_fully_read_marker(third_event_id).await; + timeline.controller.handle_fully_read_marker(event_id3).await; - // The read marker is moved after the third event. + // The read marker is moved after the third event (sent by another user). assert_next_matches!(stream, VectorDiff::Remove { index: 3 }); let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 4, value } => value); assert_matches!(marker.kind, TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)); + + // If the current user sends an event afterwards, the read marker doesn't move. + timeline.handle_live_event(f.text_msg("E").sender(&own_user)).await; + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + item.as_event().unwrap(); + + assert!(stream.next().now_or_never().is_none()); + + // If the marker moved forward to another user's event, and there's no other + // event sent from another user, then it will be removed. + timeline.controller.handle_fully_read_marker(event_id4).await; + assert_next_matches!(stream, VectorDiff::Remove { index: 4 }); + + assert!(stream.next().now_or_never().is_none()); + + // When a last event is inserted by ourselves, still no read marker. + timeline.handle_live_event(f.text_msg("F").sender(&own_user)).await; + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + item.as_event().unwrap(); + + timeline.handle_live_event(f.text_msg("G").sender(&own_user)).await; + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + item.as_event().unwrap(); + + assert!(stream.next().now_or_never().is_none()); + + // But when it's another user who sent the event, then we get a read marker for + // their message. + timeline.handle_live_event(f.text_msg("H").sender(&BOB)).await; + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + item.as_event().unwrap(); + + let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 8, value } => value); + assert_matches!(marker.kind, TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)); } From 977a9995fe59559361a99b7a923dd67da86cdf6c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Sep 2024 17:38:33 +0200 Subject: [PATCH 049/979] timeline(tests): simplify matching a day divider or a read marker using public APIs --- crates/matrix-sdk-ui/src/timeline/tests/virt.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs index 1e788a84100..42e299ad7b2 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs @@ -25,7 +25,7 @@ use ruma::{ use stream_assert::assert_next_matches; use super::TestTimeline; -use crate::timeline::{traits::RoomDataProvider as _, TimelineItemKind, VirtualTimelineItem}; +use crate::timeline::{traits::RoomDataProvider as _, VirtualTimelineItem}; #[async_test] async fn test_day_divider() { @@ -84,7 +84,7 @@ async fn test_day_divider() { // The other events are in the past so a local event always creates a new day // divider. let day_divider = assert_next_matches!(stream, VectorDiff::Insert { index: 5, value } => value); - assert_matches!(day_divider.as_virtual().unwrap(), VirtualTimelineItem::DayDivider { .. }); + assert!(day_divider.is_day_divider()); } #[async_test] @@ -127,7 +127,7 @@ async fn test_update_read_marker() { // Now the read marker is reinserted after the second event. let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 3, value } => value); - assert_matches!(marker.kind, TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)); + assert!(marker.is_read_marker()); // Nothing should happen if the fully read event is set back to an older event // sent by another user. @@ -152,7 +152,7 @@ async fn test_update_read_marker() { // The read marker is moved after the third event (sent by another user). assert_next_matches!(stream, VectorDiff::Remove { index: 3 }); let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 4, value } => value); - assert_matches!(marker.kind, TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)); + assert!(marker.is_read_marker()); // If the current user sends an event afterwards, the read marker doesn't move. timeline.handle_live_event(f.text_msg("E").sender(&own_user)).await; @@ -186,5 +186,5 @@ async fn test_update_read_marker() { item.as_event().unwrap(); let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 8, value } => value); - assert_matches!(marker.kind, TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)); + assert!(marker.is_read_marker()); } From 9df1c480795c42afcee39e4c7e553d5a927a2680 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 5 Sep 2024 14:33:26 +0200 Subject: [PATCH 050/979] =?UTF-8?q?timeline(tests):=20ASCII=20art=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matrix-sdk-ui/src/timeline/tests/virt.rs | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs index 42e299ad7b2..14c230ca0fd 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs @@ -97,21 +97,30 @@ async fn test_update_read_marker() { let f = &timeline.factory; timeline.handle_live_event(f.text_msg("A").sender(&own_user)).await; + // Timeline: [A]. + // No read marker. let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let event_id1 = item.as_event().unwrap().event_id().unwrap().to_owned(); + // Timeline: [day-divider, A]. let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); assert!(day_divider.is_day_divider()); - timeline.controller.handle_fully_read_marker(event_id1.to_owned()).await; + timeline.controller.handle_fully_read_marker(event_id1.clone()).await; // Nothing should happen, the marker cannot be added at the end. + // Timeline: [A]. + // ^-- fully read + assert!(stream.next().now_or_never().is_none()); + // Timeline: [day-divider, A, B]. timeline.handle_live_event(f.text_msg("B").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let event_id2 = item.as_event().unwrap().event_id().unwrap().to_owned(); // Now the read marker appears after the first event. + // Timeline: [day-divider, A, read-marker, B]. + // fully read --^ let item = assert_next_matches!(stream, VectorDiff::Insert { index: 2, value } => value); assert_matches!(item.as_virtual(), Some(VirtualTimelineItem::ReadMarker)); @@ -119,19 +128,25 @@ async fn test_update_read_marker() { // The read marker is removed but not reinserted, because it cannot be added at // the end. + // Timeline: [day-divider, A, B]. + // ^-- fully read assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); + // Timeline: [day-divider, A, B, C]. + // ^-- fully read timeline.handle_live_event(f.text_msg("C").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let event_id3 = item.as_event().unwrap().event_id().unwrap().to_owned(); // Now the read marker is reinserted after the second event. + // Timeline: [day-divider, A, B, read-marker, C]. + // ^-- fully read let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 3, value } => value); assert!(marker.is_read_marker()); // Nothing should happen if the fully read event is set back to an older event // sent by another user. - timeline.controller.handle_fully_read_marker(event_id1.to_owned()).await; + timeline.controller.handle_fully_read_marker(event_id1).await; assert!(stream.next().now_or_never().is_none()); // Nothing should happen if the fully read event isn't found. @@ -143,6 +158,8 @@ async fn test_update_read_marker() { timeline.controller.handle_fully_read_marker(event_id2).await; assert!(stream.next().now_or_never().is_none()); + // Timeline: [day-divider, A, B, read-marker, C, D]. + // ^-- fully read timeline.handle_live_event(f.text_msg("D").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let event_id4 = item.as_event().unwrap().event_id().unwrap().to_owned(); @@ -150,11 +167,18 @@ async fn test_update_read_marker() { timeline.controller.handle_fully_read_marker(event_id3).await; // The read marker is moved after the third event (sent by another user). + // Timeline: [day-divider, A, B, C, D]. + // fully read --^ assert_next_matches!(stream, VectorDiff::Remove { index: 3 }); + + // Timeline: [day-divider, A, B, C, read-marker, D]. + // fully read --^ let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 4, value } => value); assert!(marker.is_read_marker()); // If the current user sends an event afterwards, the read marker doesn't move. + // Timeline: [day-divider, A, B, C, read-marker, D, E]. + // fully read --^ timeline.handle_live_event(f.text_msg("E").sender(&own_user)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); @@ -163,28 +187,43 @@ async fn test_update_read_marker() { // If the marker moved forward to another user's event, and there's no other // event sent from another user, then it will be removed. + // Timeline: [day-divider, A, B, C, D, E]. + // fully read --^ timeline.controller.handle_fully_read_marker(event_id4).await; assert_next_matches!(stream, VectorDiff::Remove { index: 4 }); assert!(stream.next().now_or_never().is_none()); // When a last event is inserted by ourselves, still no read marker. + // Timeline: [day-divider, A, B, C, D, E, F]. + // fully read --^ timeline.handle_live_event(f.text_msg("F").sender(&own_user)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); + // Timeline: [day-divider, A, B, C, D, E, F, G]. + // fully read --^ timeline.handle_live_event(f.text_msg("G").sender(&own_user)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); assert!(stream.next().now_or_never().is_none()); - // But when it's another user who sent the event, then we get a read marker for - // their message. + // But when it's another user who sent the event, then we get a read marker just + // before their message. It is the first message that's both after the + // fully-read event and not sent by us. + // + // Timeline: [day-divider, A, B, C, D, E, F, G, H]. + // fully read --^ timeline.handle_live_event(f.text_msg("H").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); + // [our own] v-- sent by Bob + // Timeline: [day-divider, A, B, C, D, E, F, G, read-marker, H]. + // fully read --^ let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 8, value } => value); assert!(marker.is_read_marker()); + + assert!(stream.next().now_or_never().is_none()); } From 07aa6d7bc7f8d40809226c01e68f3539c85372c9 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 5 Sep 2024 16:56:34 +0100 Subject: [PATCH 051/979] doc: Fix missing 'o' in the doc comment for the recovery module --- crates/matrix-sdk/src/encryption/recovery/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/encryption/recovery/mod.rs b/crates/matrix-sdk/src/encryption/recovery/mod.rs index 51a4d214c66..c5d818764c2 100644 --- a/crates/matrix-sdk/src/encryption/recovery/mod.rs +++ b/crates/matrix-sdk/src/encryption/recovery/mod.rs @@ -28,7 +28,7 @@ //! this file is the secret storage key. //! //! You should configure your client to bootstrap cross-signing automatically -//! and may chose to let your client automatically create a backup, if it +//! and may choose to let your client automatically create a backup, if it //! doesn't exist, as well: //! //! ```no_run From 98ba714b2071ebd7eb22e90b918129c01ef7bcd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Sep 2024 13:33:47 +0200 Subject: [PATCH 052/979] sdk: Fix a clippy warning --- .../matrix-sdk/src/event_cache/linked_chunk/mod.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs index da4d4c9b213..644ceb09768 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs @@ -888,11 +888,7 @@ impl<'a, const CAP: usize, Item, Gap> Iterator for IterBackward<'a, CAP, Item, G type Item = &'a Chunk; fn next(&mut self) -> Option { - self.chunk.map(|chunk| { - self.chunk = chunk.previous(); - - chunk - }) + self.chunk.inspect(|chunk| self.chunk = chunk.previous()) } } @@ -914,11 +910,7 @@ impl<'a, const CAP: usize, Item, Gap> Iterator for Iter<'a, CAP, Item, Gap> { type Item = &'a Chunk; fn next(&mut self) -> Option { - self.chunk.map(|chunk| { - self.chunk = chunk.next(); - - chunk - }) + self.chunk.inspect(|chunk| self.chunk = chunk.next()) } } From 1eecb2d603edfae43b842010ac13d021411d0332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Sep 2024 13:11:05 +0200 Subject: [PATCH 053/979] ui: Remove the e2e-encryption feature from the matrix-sdk-ui crate It does not make much sense to create an UI client that does not support end-to-end encryption, besides disabling the feature was broken for quite some time. --- bindings/matrix-sdk-ffi/Cargo.toml | 2 +- crates/matrix-sdk-ui/Cargo.toml | 6 ++--- crates/matrix-sdk-ui/src/timeline/builder.rs | 14 +++------- .../src/timeline/controller/mod.rs | 26 +++++++------------ .../src/timeline/controller/state.rs | 2 -- .../src/timeline/event_handler.rs | 5 ---- .../src/timeline/event_item/remote.rs | 1 - crates/matrix-sdk-ui/src/timeline/mod.rs | 3 --- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 1 - .../src/timeline/tests/read_receipts.rs | 1 - crates/matrix-sdk-ui/src/timeline/traits.rs | 18 ++++++------- .../integration/encryption_sync_service.rs | 21 +++++++-------- 12 files changed, 32 insertions(+), 68 deletions(-) diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index d954bfb302f..abab2bff692 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -28,7 +28,7 @@ eyeball-im = { workspace = true } extension-trait = "1.0.1" futures-util = { workspace = true } log-panics = { version = "2", features = ["with-backtrace"] } -matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] } +matrix-sdk-ui = { workspace = true, features = ["uniffi"] } mime = "0.3.16" once_cell = { workspace = true } ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] } diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index 9edaddb9622..db4bac2952b 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -8,9 +8,7 @@ license = "Apache-2.0" rust-version = { workspace = true } [features] -default = ["e2e-encryption", "native-tls"] - -e2e-encryption = ["matrix-sdk/e2e-encryption"] +default = ["native-tls"] native-tls = ["matrix-sdk/native-tls"] rustls-tls = ["matrix-sdk/rustls-tls"] @@ -37,7 +35,7 @@ growable-bloom-filter = { workspace = true } imbl = { workspace = true, features = ["serde"] } indexmap = "2.0.0" itertools = { workspace = true } -matrix-sdk = { workspace = true, features = ["experimental-sliding-sync"] } +matrix-sdk = { workspace = true, features = ["experimental-sliding-sync", "e2e-encryption"] } matrix-sdk-base = { workspace = true } mime = "0.3.16" once_cell = { workspace = true } diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 32e19f82a98..23b14c13199 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -24,10 +24,9 @@ use ruma::{events::AnySyncTimelineEvent, RoomVersionId}; use tokio::sync::broadcast::error::RecvError; use tracing::{info, info_span, trace, warn, Instrument, Span}; -#[cfg(feature = "e2e-encryption")] -use super::to_device::{handle_forwarded_room_key_event, handle_room_key_event}; use super::{ controller::{TimelineController, TimelineSettings}, + to_device::{handle_forwarded_room_key_event, handle_room_key_event}, Error, Timeline, TimelineDropHandle, TimelineFocus, }; use crate::{ @@ -330,23 +329,17 @@ impl TimelineBuilder { // Not using room.add_event_handler here because RoomKey events are // to-device events that are not received in the context of a room. - #[cfg(feature = "e2e-encryption")] let room_key_handle = client.add_event_handler(handle_room_key_event( controller.clone(), room.room_id().to_owned(), )); - #[cfg(feature = "e2e-encryption")] + let forwarded_room_key_handle = client.add_event_handler(handle_forwarded_room_key_event( controller.clone(), room.room_id().to_owned(), )); - let handles = vec![ - #[cfg(feature = "e2e-encryption")] - room_key_handle, - #[cfg(feature = "e2e-encryption")] - forwarded_room_key_handle, - ]; + let handles = vec![room_key_handle, forwarded_room_key_handle]; let room_key_from_backups_join_handle = { let inner = controller.clone(); @@ -391,7 +384,6 @@ impl TimelineBuilder { }), }; - #[cfg(feature = "e2e-encryption")] if has_events { // The events we're injecting might be encrypted events, but we might // have received the room key to decrypt them while nobody was listening to the diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index e9351f8f323..63f5d286e00 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -12,16 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "e2e-encryption")] -use std::collections::BTreeSet; -use std::{fmt, sync::Arc}; +use std::{collections::BTreeSet, fmt, sync::Arc}; use as_variant::as_variant; use eyeball_im::{ObservableVectorEntry, VectorDiff}; use eyeball_im_util::vector::VectorObserverExt; use futures_core::Stream; use imbl::Vector; -#[cfg(all(test, feature = "e2e-encryption"))] +#[cfg(test)] use matrix_sdk::crypto::OlmMachine; use matrix_sdk::{ deserialized_responses::SyncTimelineEvent, @@ -31,10 +29,6 @@ use matrix_sdk::{ }, Result, Room, }; -#[cfg(test)] -use ruma::events::receipt::ReceiptEventContent; -#[cfg(all(test, feature = "e2e-encryption"))] -use ruma::RoomId; use ruma::{ api::client::receipt::create_receipt::v3::ReceiptType as SendReceiptType, events::{ @@ -50,21 +44,21 @@ use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, RoomVersionId, TransactionId, UserId, }; +#[cfg(test)] +use ruma::{events::receipt::ReceiptEventContent, RoomId}; use tokio::sync::{RwLock, RwLockWriteGuard}; -use tracing::{debug, error, field::debug, info, instrument, trace, warn}; -#[cfg(feature = "e2e-encryption")] -use tracing::{field, info_span, Instrument as _}; +use tracing::{ + debug, error, field, field::debug, info, info_span, instrument, trace, warn, Instrument as _, +}; pub(super) use self::state::{ EventMeta, FullEventMeta, TimelineEnd, TimelineMetadata, TimelineState, TimelineStateTransaction, }; -#[cfg(feature = "e2e-encryption")] -use super::traits::Decryptor; use super::{ event_handler::TimelineEventKind, event_item::{ReactionStatus, RemoteEventOrigin}, - traits::RoomDataProvider, + traits::{Decryptor, RoomDataProvider}, util::{rfind_event_by_id, rfind_event_item, RelativePosition}, Error, EventSendState, EventTimelineItem, InReplyToDetails, Message, PaginationError, Profile, ReactionInfo, RepliedToEvent, TimelineDetails, TimelineEventItemId, TimelineFocus, @@ -939,7 +933,6 @@ impl TimelineController

{ true } - #[cfg(feature = "e2e-encryption")] #[instrument(skip(self, room), fields(room_id = ?room.room_id()))] pub(super) async fn retry_event_decryption( &self, @@ -949,7 +942,7 @@ impl TimelineController

{ self.retry_event_decryption_inner(room.to_owned(), session_ids).await } - #[cfg(all(test, feature = "e2e-encryption"))] + #[cfg(test)] pub(super) async fn retry_event_decryption_test( &self, room_id: &RoomId, @@ -959,7 +952,6 @@ impl TimelineController

{ self.retry_event_decryption_inner((olm_machine, room_id.to_owned()), session_ids).await } - #[cfg(feature = "e2e-encryption")] async fn retry_event_decryption_inner( &self, decryptor: impl Decryptor, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 16034ea892b..d1ec4e10790 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -198,7 +198,6 @@ impl TimelineState { txn.commit(); } - #[cfg(feature = "e2e-encryption")] pub(super) async fn retry_event_decryption( &mut self, retry_one: impl Fn(Arc) -> Fut, @@ -635,7 +634,6 @@ impl TimelineStateTransaction<'_> { self.meta.all_events.push_back(event_meta.base_meta()); } - #[cfg(feature = "e2e-encryption")] TimelineItemPosition::Update(_) => { if let Some(event) = self.meta.all_events.iter_mut().find(|e| e.event_id == event_meta.event_id) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 9735c27f263..24caf55307b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -234,7 +234,6 @@ pub(super) enum TimelineItemPosition { /// A single item is updated. /// /// This only happens when a UTD must be replaced with the decrypted event. - #[cfg(feature = "e2e-encryption")] Update(usize), } @@ -249,7 +248,6 @@ pub(super) struct HandleEventResult { /// /// This can happen only if there was a UTD item that has been decrypted /// into an item that was filtered out with the event filter. - #[cfg(feature = "e2e-encryption")] pub(super) item_removed: bool, /// How many items were updated? @@ -433,7 +431,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { if !self.result.item_added { trace!("No new item added"); - #[cfg(feature = "e2e-encryption")] if let Flow::Remote { position: TimelineItemPosition::Update(idx), .. } = self.ctx.flow { // If add was not called, that means the UTD event is one that @@ -821,7 +818,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { | TimelineItemPosition::End { origin } => origin, // For updates, reuse the origin of the encrypted event. - #[cfg(feature = "e2e-encryption")] TimelineItemPosition::Update(idx) => self.items[idx] .as_event() .and_then(|ev| Some(ev.as_remote()?.origin)) @@ -966,7 +962,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } - #[cfg(feature = "e2e-encryption")] Flow::Remote { position: TimelineItemPosition::Update(idx), .. } => { trace!("Updating timeline item at position {idx}"); let id = self.items[*idx].internal_id.clone(); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs b/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs index 4510910c47a..3e162b383a6 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs @@ -89,7 +89,6 @@ pub(in crate::timeline) enum RemoteEventOrigin { /// The event came from pagination. Pagination, /// We don't know. - #[cfg(feature = "e2e-encryption")] Unknown, } diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 43fb967a35c..0c8fdf1dc9c 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -76,7 +76,6 @@ mod reactions; mod read_receipts; #[cfg(test)] mod tests; -#[cfg(feature = "e2e-encryption")] mod to_device; mod traits; mod util; @@ -217,7 +216,6 @@ impl Timeline { /// } /// # anyhow::Ok(()) }; /// ``` - #[cfg(feature = "e2e-encryption")] pub async fn retry_decryption>( &self, session_ids: impl IntoIterator, @@ -230,7 +228,6 @@ impl Timeline { .await; } - #[cfg(feature = "e2e-encryption")] #[tracing::instrument(skip(self))] async fn retry_decryption_for_all_events(&self) { self.controller.retry_event_decryption(self.room(), None).await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 8680a3fbd29..bbc1f0bb6d6 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -68,7 +68,6 @@ use crate::{ mod basic; mod echo; mod edit; -#[cfg(feature = "e2e-encryption")] mod encryption; mod event_filter; mod invalid; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs index 101aa9f172a..574a8bec6a4 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs @@ -329,7 +329,6 @@ async fn test_read_receipts_updates_on_back_paginated_filtered_events() { assert_pending!(stream); } -#[cfg(feature = "e2e-encryption")] #[async_test] async fn test_read_receipts_updates_on_message_decryption() { use std::{io::Cursor, iter}; diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index 706b1a742fe..49fa44163af 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -16,21 +16,21 @@ use std::future::Future; use futures_util::FutureExt as _; use indexmap::IndexMap; -#[cfg(all(test, feature = "e2e-encryption"))] +#[cfg(test)] use matrix_sdk::crypto::{DecryptionSettings, TrustRequirement}; -#[cfg(feature = "e2e-encryption")] -use matrix_sdk::{deserialized_responses::TimelineEvent, Result}; -use matrix_sdk::{event_cache::paginator::PaginableRoom, BoxFuture, Room}; +use matrix_sdk::{ + deserialized_responses::TimelineEvent, event_cache::paginator::PaginableRoom, BoxFuture, + Result, Room, +}; use matrix_sdk_base::latest_event::LatestEvent; -#[cfg(feature = "e2e-encryption")] -use ruma::{events::AnySyncTimelineEvent, serde::Raw}; use ruma::{ events::{ fully_read::FullyReadEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, - AnyMessageLikeEventContent, + AnyMessageLikeEventContent, AnySyncTimelineEvent, }, push::{PushConditionRoomCtx, Ruleset}, + serde::Raw, EventId, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomVersionId, UserId, }; use tracing::{debug, error}; @@ -275,7 +275,6 @@ impl RoomDataProvider for Room { // Internal helper to make most of retry_event_decryption independent of a room // object, which is annoying to create for testing and not really needed -#[cfg(feature = "e2e-encryption")] pub(super) trait Decryptor: Clone + Send + Sync + 'static { fn decrypt_event_impl( &self, @@ -283,14 +282,13 @@ pub(super) trait Decryptor: Clone + Send + Sync + 'static { ) -> impl Future> + Send; } -#[cfg(feature = "e2e-encryption")] impl Decryptor for Room { async fn decrypt_event_impl(&self, raw: &Raw) -> Result { self.decrypt_event(raw.cast_ref()).await } } -#[cfg(all(test, feature = "e2e-encryption"))] +#[cfg(test)] impl Decryptor for (matrix_sdk_base::crypto::OlmMachine, ruma::OwnedRoomId) { async fn decrypt_event_impl(&self, raw: &Raw) -> Result { let (olm_machine, room_id) = self; diff --git a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs index a75b835921f..cc37762c94a 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Mutex}; use futures_util::{pin_mut, StreamExt as _}; use matrix_sdk::test_utils::logged_in_client_with_server; +use matrix_sdk_base::crypto::store::Changes; use matrix_sdk_test::async_test; use matrix_sdk_ui::encryption_sync_service::{ EncryptionSyncPermit, EncryptionSyncService, WithLocking, @@ -291,18 +292,14 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result< // This encryption sync now conceptually goes to sleep, and another encryption // sync starts in another process, runs a sync and changes the to-device // token cached on disk. - #[cfg(feature = "e2e-encryption")] - { - use matrix_sdk_base::crypto::store::Changes; - if let Some(olm_machine) = &*client.olm_machine_for_testing().await { - olm_machine - .store() - .save_changes(Changes { - next_batch_token: Some("nb2".to_owned()), - ..Default::default() - }) - .await?; - } + if let Some(olm_machine) = &*client.olm_machine_for_testing().await { + olm_machine + .store() + .save_changes(Changes { + next_batch_token: Some("nb2".to_owned()), + ..Default::default() + }) + .await?; } // Next iteration must have reloaded the latest to-device token. From 16fd88c419386161b1fcb955c14a52dc08596daf Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Sep 2024 08:49:14 +0200 Subject: [PATCH 054/979] chore(sdk): Improve a doc and format code. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 82488167392..900b8c54f03 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -507,7 +507,9 @@ impl SlidingSync { // Apply sticky parameters, if needs be. self.inner.sticky.write().unwrap().maybe_apply(&mut request, txn_id); - // Set the to-device token if the extension is enabled. + // Extensions are now applied (via sticky parameters). + // + // Override the to-device token if the extension is enabled. if to_device_enabled { request.extensions.to_device.since = restored_fields.and_then(|fields| fields.to_device_token); @@ -1533,11 +1535,11 @@ mod tests { // to-device. let extensions = &sticky.data().extensions; assert_eq!(extensions.e2ee.enabled, None); - assert_eq!(extensions.to_device.enabled, None,); - assert_eq!(extensions.to_device.since, None,); + assert_eq!(extensions.to_device.enabled, None); + assert_eq!(extensions.to_device.since, None); - // What the user explicitly enabled is... enabled. - assert_eq!(extensions.account_data.enabled, Some(true),); + // What the user explicitly enabled is… enabled. + assert_eq!(extensions.account_data.enabled, Some(true)); let txn_id: &TransactionId = "tid123".into(); let mut request = http::Request::default(); @@ -1562,7 +1564,7 @@ mod tests { .await?; // No extensions have been explicitly enabled here. - assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.to_device.enabled, None,); + assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.to_device.enabled, None); assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.e2ee.enabled, None); assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.account_data.enabled, None); From 10a0d590126e36ee24b54a1b8e37a76f22a115d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 23 Aug 2024 14:36:04 +0200 Subject: [PATCH 055/979] sdk-ui: fix max concurrent requests for pinned events timeline. --- benchmarks/benches/room_bench.rs | 5 +- bindings/matrix-sdk-ffi/src/room.rs | 7 +- .../src/timeline/controller/mod.rs | 3 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 2 +- .../src/timeline/pinned_events_loader.rs | 69 ++++---- .../integration/timeline/pinned_event.rs | 155 ++++++++++++------ 6 files changed, 149 insertions(+), 92 deletions(-) diff --git a/benchmarks/benches/room_bench.rs b/benchmarks/benches/room_bench.rs index 7c163552f02..5b010b6ee31 100644 --- a/benchmarks/benches/room_bench.rs +++ b/benchmarks/benches/room_bench.rs @@ -198,7 +198,10 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) { client.event_cache().empty_immutable_cache().await; let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 100 }) + .with_focus(TimelineFocus::PinnedEvents { + max_events_to_load: 100, + max_concurrent_requests: 10, + }) .build() .await .expect("Could not create timeline"); diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 0de3e7f5559..7c6b41833de 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -233,6 +233,7 @@ impl Room { &self, internal_id_prefix: Option, max_events_to_load: u16, + max_concurrent_requests: u16, ) -> Result, ClientError> { let room = &self.inner; @@ -242,8 +243,10 @@ impl Room { builder = builder.with_internal_id_prefix(internal_id_prefix); } - let timeline = - builder.with_focus(TimelineFocus::PinnedEvents { max_events_to_load }).build().await?; + let timeline = builder + .with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests }) + .build() + .await?; Ok(Timeline::new(timeline)) } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 63f5d286e00..069f486839e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -253,11 +253,12 @@ impl TimelineController

{ ) } - TimelineFocus::PinnedEvents { max_events_to_load } => ( + TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => ( TimelineFocusData::PinnedEvents { loader: PinnedEventsLoader::new( Arc::new(room_data_provider.clone()), max_events_to_load as usize, + max_concurrent_requests as usize, ), }, TimelineFocusKind::PinnedEvents, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 0c8fdf1dc9c..584d9aeb826 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -173,7 +173,7 @@ pub enum TimelineFocus { Event { target: OwnedEventId, num_context_events: u16 }, /// Only show pinned events. - PinnedEvents { max_events_to_load: u16 }, + PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 }, } impl Timeline { diff --git a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs index 7cc9a2b07f5..7debb031037 100644 --- a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs +++ b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{fmt::Formatter, num::NonZeroUsize, sync::Arc}; +use std::{fmt::Formatter, sync::Arc}; -use futures_util::{future::join_all, FutureExt as _}; +use futures_util::{stream, FutureExt as _, StreamExt}; use matrix_sdk::{ config::RequestConfig, event_cache::paginator::PaginatorError, BoxFuture, Room, SendOutsideWasm, SyncOutsideWasm, @@ -24,8 +24,6 @@ use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId}; use thiserror::Error; use tracing::{debug, warn}; -const MAX_CONCURRENT_REQUESTS: usize = 10; - /// Utility to load the pinned events in a room. pub struct PinnedEventsLoader { /// Backend to load pinned events. @@ -34,12 +32,21 @@ pub struct PinnedEventsLoader { /// Maximum number of pinned events to load (either from network or the /// cache). max_events_to_load: usize, + + /// Number of requests to load pinned events that can run concurrently. This + /// is used to avoid overwhelming a home server with dozens or hundreds + /// of concurrent requests. + max_concurrent_requests: usize, } impl PinnedEventsLoader { /// Creates a new `PinnedEventsLoader` instance. - pub fn new(room: Arc, max_events_to_load: usize) -> Self { - Self { room, max_events_to_load } + pub fn new( + room: Arc, + max_events_to_load: usize, + max_concurrent_requests: usize, + ) -> Self { + Self { room, max_events_to_load, max_concurrent_requests } } /// Loads the pinned events in this room, using the cache first and then @@ -64,37 +71,33 @@ impl PinnedEventsLoader { return Ok(Vec::new()); } - let request_config = Some( - RequestConfig::default() - .retry_limit(3) - .max_concurrent_requests(NonZeroUsize::new(MAX_CONCURRENT_REQUESTS)), - ); - - let new_events = join_all(pinned_event_ids.into_iter().map(|event_id| { - let provider = self.room.clone(); - async move { - match provider.load_event_with_relations(&event_id, request_config).await { - Ok((event, related_events)) => { - let mut events = vec![event]; - events.extend(related_events); - Some(events) - } - Err(err) => { - warn!("error when loading pinned event: {err}"); - None + let request_config = Some(RequestConfig::default().retry_limit(3)); + + let mut loaded_events: Vec = + stream::iter(pinned_event_ids.into_iter().map(|event_id| { + let provider = self.room.clone(); + async move { + match provider.load_event_with_relations(&event_id, request_config).await { + Ok((event, related_events)) => { + let mut events = vec![event]; + events.extend(related_events); + Some(events) + } + Err(err) => { + warn!("error when loading pinned event: {err}"); + None + } } } - } - })) - .await; - - let mut loaded_events = new_events - .into_iter() + })) + .buffer_unordered(self.max_concurrent_requests) // Get only the `Some>` results - .flatten() + .flat_map(stream::iter) // Flatten the `Vec`s into a single one containing all their items - .flatten() - .collect::>(); + .flat_map(stream::iter) + .collect() + .await; + if loaded_events.is_empty() { return Err(PinnedEventsLoaderError::TimelineReloadFailed); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index 89a4f91409b..a46c6df9177 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -12,7 +12,7 @@ use matrix_sdk::{ use matrix_sdk_base::deserialized_responses::TimelineEvent; use matrix_sdk_test::{async_test, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, BOB}; use matrix_sdk_ui::{ - timeline::{TimelineFocus, TimelineItemContent}, + timeline::{RoomExt, TimelineFocus, TimelineItemContent}, Timeline, }; use ruma::{ @@ -21,7 +21,11 @@ use ruma::{ }; use serde_json::json; use stream_assert::assert_pending; -use wiremock::MockServer; +use tokio::time::sleep; +use wiremock::{ + matchers::{header, method, path_regex}, + Mock, MockServer, ResponseTemplate, +}; use crate::{mock_event, mock_sync}; @@ -46,11 +50,8 @@ async fn test_new_pinned_events_are_added_on_sync() { let _ = test_helper.setup_sync_response(vec![(event_1, false)], Some(vec!["$1", "$2"])).await; let room = test_helper.client.get_room(&room_id).unwrap(); - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 100 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(100)).build().await.unwrap(); test_helper.server.reset().await; assert!( @@ -131,11 +132,8 @@ async fn test_new_pinned_event_ids_reload_the_timeline() { .await; let room = test_helper.client.get_room(&room_id).unwrap(); - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 100 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(100)).build().await.unwrap(); assert!( timeline.live_back_pagination_status().await.is_none(), @@ -203,10 +201,7 @@ async fn test_max_events_to_load_is_honored() { test_helper.setup_sync_response(vec![(pinned_event, false)], Some(vec!["$1", "$2"])).await; let room = test_helper.client.get_room(&room_id).unwrap(); - let ret = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 1 }) - .build() - .await; + let ret = Timeline::builder(&room).with_focus(pinned_events_focus(1)).build().await; // We're only taking the last event id, `$2`, and it's not available so the // timeline fails to initialise. @@ -242,11 +237,8 @@ async fn test_cached_events_are_kept_for_different_room_instances() { let room = test_helper.client.get_room(&room_id).unwrap(); let (room_cache, _drop_handles) = room.event_cache().await.unwrap(); - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 2 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(2)).build().await.unwrap(); assert!( timeline.live_back_pagination_status().await.is_none(), @@ -274,11 +266,8 @@ async fn test_cached_events_are_kept_for_different_room_instances() { let room = test_helper.client.get_room(&room_id).unwrap(); // And a new timeline one - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 2 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(2)).build().await.unwrap(); let (items, _) = timeline.subscribe().await; assert!(!items.is_empty()); // These events came from the cache @@ -298,10 +287,7 @@ async fn test_cached_events_are_kept_for_different_room_instances() { let room = test_helper.client.get_room(&room_id).unwrap(); // And a new timeline one - let ret = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 2 }) - .build() - .await; + let ret = Timeline::builder(&room).with_focus(pinned_events_focus(2)).build().await; // Since the events are no longer in the cache the timeline couldn't load them // and can't be initialised. @@ -324,10 +310,7 @@ async fn test_pinned_timeline_with_pinned_event_ids_and_empty_result_fails() { let _ = test_helper.setup_sync_response(Vec::new(), Some(vec!["$1", "$2"])).await; let room = test_helper.client.get_room(&room_id).unwrap(); - let ret = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 1 }) - .build() - .await; + let ret = Timeline::builder(&room).with_focus(pinned_events_focus(1)).build().await; // The timeline couldn't load any events so it fails to initialise assert!(ret.is_err()); @@ -348,11 +331,8 @@ async fn test_pinned_timeline_with_no_pinned_event_ids_is_just_empty() { let _ = test_helper.setup_sync_response(Vec::new(), Some(Vec::new())).await; let room = test_helper.client.get_room(&room_id).unwrap(); - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 1 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(1)).build().await.unwrap(); // The timeline couldn't load any events, but it expected none, so it just // returns an empty list @@ -383,11 +363,8 @@ async fn test_edited_events_are_reflected_in_sync() { let _ = test_helper.setup_sync_response(vec![(pinned_event, false)], Some(vec!["$1"])).await; let room = test_helper.client.get_room(&room_id).unwrap(); - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 100 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(100)).build().await.unwrap(); test_helper.server.reset().await; assert!( @@ -462,11 +439,8 @@ async fn test_redacted_events_are_reflected_in_sync() { let _ = test_helper.setup_sync_response(vec![(pinned_event, false)], Some(vec!["$1"])).await; let room = test_helper.client.get_room(&room_id).unwrap(); - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 100 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(100)).build().await.unwrap(); test_helper.server.reset().await; assert!( @@ -532,11 +506,8 @@ async fn test_edited_events_survive_pinned_event_ids_change() { let _ = test_helper.setup_sync_response(vec![(pinned_event, false)], Some(vec!["$1"])).await; let room = test_helper.client.get_room(&room_id).unwrap(); - let timeline = Timeline::builder(&room) - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load: 100 }) - .build() - .await - .unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(100)).build().await.unwrap(); test_helper.server.reset().await; assert!( @@ -635,6 +606,78 @@ async fn test_edited_events_survive_pinned_event_ids_change() { assert_pending!(timeline_stream); } +#[async_test] +async fn test_ensure_max_concurrency_is_observed() { + let (client, server) = logged_in_client_with_server().await; + let room_id = owned_room_id!("!a_room:example.org"); + + let pinned_event_ids: Vec = (0..100).map(|idx| format!("${idx}")).collect(); + + let max_concurrent_requests: u16 = 10; + + let joined_room_builder = JoinedRoomBuilder::new(&room_id) + // Set up encryption + .add_state_event(StateTestEvent::Encryption) + // Add 100 pinned events + .add_state_event(StateTestEvent::Custom(json!( + { + "content": { + "pinned": pinned_event_ids + }, + "event_id": "$15139375513VdeRF:localhost", + "origin_server_ts": 151393755, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.pinned_events", + "unsigned": { + "age": 703422 + } + } + ))); + + // Amount of time to delay the response of an /event mock request, in ms. + let request_delay = 50; + let pinned_event = + EventFactory::new().room(&room_id).sender(*BOB).text_msg("A message").into_raw_timeline(); + Mock::given(method("GET")) + .and(path_regex(r"/_matrix/client/r0/rooms/.*/event/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200) + .set_delay(Duration::from_millis(request_delay)) + .set_body_json(pinned_event.json()), + ) + // Verify this endpoint is only called the max concurrent amount of times. + .expect(max_concurrent_requests as u64) + .mount(&server) + .await; + + let mut sync_response_builder = SyncResponseBuilder::new(); + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let json_response = + sync_response_builder.add_joined_room(joined_room_builder).build_json_sync_response(); + mock_sync(&server, json_response, None).await; + let _ = client.sync_once(sync_settings.clone()).await; + + let room = client.get_room(&room_id).unwrap(); + + // Start loading the pinned event timeline asynchronously. + let handle = tokio::spawn({ + let timeline_builder = room.timeline_builder().with_focus(pinned_events_focus(100)); + async { + let _ = timeline_builder.build().await; + } + }); + + // Give it time to load events. As each request takes `request_delay`, we should + // have exactly `MAX_PINNED_EVENTS_CONCURRENT_REQUESTS` if the max + // concurrency setting is honoured. + sleep(Duration::from_millis(request_delay / 2)).await; + + // Abort handle to stop requests from being processed. + handle.abort(); +} + struct TestHelper { pub client: Client, pub server: MockServer, @@ -721,3 +764,7 @@ impl TestHelper { self.client.sync_once(self.sync_settings.clone()).await } } + +fn pinned_events_focus(max_events_to_load: u16) -> TimelineFocus { + TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests: 10 } +} From ea794bb9f2377de4d9e7588e629ea732d1e98601 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Sep 2024 09:50:06 +0200 Subject: [PATCH 056/979] =?UTF-8?q?chore(sdk):=20Replace=20=E2=80=9Csimpli?= =?UTF-8?q?fied=20sliding=20sync=E2=80=9D=20by=20=E2=80=9CMSC4186=E2=80=9D?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified sliding sync finally has an MSC number: 4186. Let's use this name when possible to clarify the code. --- .../matrix-sdk-base/src/sliding_sync/http.rs | 13 ++++---- .../matrix-sdk-base/src/sliding_sync/mod.rs | 30 +++++++++---------- crates/matrix-sdk/src/sliding_sync/client.rs | 6 ++-- crates/matrix-sdk/src/sliding_sync/mod.rs | 20 ++++++------- .../src/helpers.rs | 4 +-- 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync/http.rs b/crates/matrix-sdk-base/src/sliding_sync/http.rs index 82879253265..c91b3ad2a91 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/http.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/http.rs @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! HTTP types for (Simplified) MSC3575. +//! HTTP types for MSC4186 or MSC3585. //! //! This module provides unified namings for types from MSC3575 and -//! Simplified MSC3575, in addition to provide conversion from one -//! format to another. +//! MSC4186. -/// HTTP types from MSC3575, renamed to match the Simplified MSC3575 namings. +/// HTTP types from MSC3575, renamed to match the MSC4186 namings. pub mod msc3575 { use ruma::api::client::sync::sync_events::v4; pub use v4::{Request, Response}; @@ -42,9 +41,9 @@ pub mod msc3575 { } } -/// HTTP types from Simplified MSC3575. -pub mod simplified_msc3575 { +/// HTTP types from MSC4186. +pub mod msc4186 { pub use ruma::api::client::sync::sync_events::v5::*; } -pub use simplified_msc3575::*; +pub use msc4186::*; diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 9ae77aff3ee..8f6f4dada8b 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -119,15 +119,14 @@ impl BaseClient { /// sync. /// * `previous_events_provider` - Timeline events prior to the current /// sync. - /// * `from_simplified_sliding_sync` - Whether the `response` comes from - /// simplified sliding sync (Simplified MSC3575), or sliding sync - /// (MSC3575). + /// * `from_msc4186` - Whether the `response` comes from simplified sliding + /// sync (MSC4186) or sliding sync (MSC3575). #[instrument(skip_all, level = "trace")] pub async fn process_sliding_sync( &self, response: &http::Response, previous_events_provider: &PEP, - from_simplified_sliding_sync: bool, + from_msc4186: bool, ) -> Result { let http::Response { // FIXME not yet supported by sliding sync. see @@ -181,7 +180,7 @@ impl BaseClient { &mut room_info_notable_updates, &mut notifications, &mut ambiguity_cache, - from_simplified_sliding_sync, + from_msc4186, ) .await?; @@ -353,7 +352,7 @@ impl BaseClient { room_info_notable_updates: &mut BTreeMap, notifications: &mut BTreeMap>, ambiguity_cache: &mut AmbiguityCache, - from_simplified_sliding_sync: bool, + from_msc4186: bool, ) -> Result<(RoomInfo, Option, Option, Option)> { // This method may change `room_data` (see the terrible hack describes below) @@ -381,10 +380,10 @@ impl BaseClient { // `timestamp` despites having `m.room.create` in `bump_event_types`. The result // of this is that an invite cannot be sorted. This horrible hack will fix that. // - // The SDK manipulates Simplified MSC3575 `Request` and `Response` though. In - // Simplified MSC3575, `bump_stamp` replaces `timestamp`, which does NOT - // represent a time! This hack must really, only, apply to the proxy, so to - // MSC3575 strictly (hence the `from_simplified_sliding_sync` argument). + // The SDK manipulates MSC4186 `Request` and `Response` though. In MSC4186, + // `bump_stamp` replaces `timestamp`, which does NOT represent a time! This + // hack must really, only, apply to the proxy, so to MSC3575 strictly (hence + // the `from_msc4186` argument). // // The proxy uses the `origin_server_ts` event's value to fill the `timestamp` // room's value (which is a bad idea[^1]). If `timestamp` is `None`, let's find @@ -392,9 +391,8 @@ impl BaseClient { // // [^1]: using `origin_server_ts` for `timestamp` is a bad idea because // this value can be forged by a malicious user. Anyway, that's how it works - // in the proxy. Simplified MSC3575 has another mechanism which fixes the - // problem. - if !from_simplified_sliding_sync && room_data.bump_stamp.is_none() { + // in the proxy. MSC4186 has another mechanism which fixes the problem. + if !from_msc4186 && room_data.bump_stamp.is_none() { if let Some(invite_state) = &room_data.invite_state { room_data.to_mut().bump_stamp = invite_state.iter().rev().find_map(|invite_state| { @@ -1432,7 +1430,7 @@ mod tests { #[async_test] async fn test_invitation_room_receive_a_default_timestamp_on_not_simplified_sliding_sync() { - const NOT_SIMPLIFIED_SLIDING_SYNC: bool = false; + const NOT_MSC4186: bool = false; // Given a logged-in client let client = logged_in_base_client(None).await; @@ -1445,7 +1443,7 @@ mod tests { set_room_invited(&mut room, user_id, user_id); let response = response_with_room(room_id, room); let _sync_resp = client - .process_sliding_sync(&response, &(), NOT_SIMPLIFIED_SLIDING_SYNC) + .process_sliding_sync(&response, &(), NOT_MSC4186) .await .expect("Failed to process sync"); @@ -1491,7 +1489,7 @@ mod tests { let response = response_with_room(room_id, room); let _sync_resp = client - .process_sliding_sync(&response, &(), NOT_SIMPLIFIED_SLIDING_SYNC) + .process_sliding_sync(&response, &(), NOT_MSC4186) .await .expect("Failed to process sync"); diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index b9921d64af3..f6974835839 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -32,7 +32,7 @@ pub enum Version { }, /// Use the version of the sliding sync implementation inside Synapse, i.e. - /// Simplified MSC3575. + /// MSC4186. Native, } @@ -268,7 +268,7 @@ impl<'a> SlidingSyncResponseProcessor<'a> { pub async fn handle_room_response( &mut self, response: &http::Response, - from_simplified_sliding_sync: bool, + from_msc4186: bool, ) -> Result<()> { self.response = Some( self.client @@ -276,7 +276,7 @@ impl<'a> SlidingSyncResponseProcessor<'a> { .process_sliding_sync( response, &SlidingSyncPreviousEventsProvider(self.rooms), - from_simplified_sliding_sync, + from_msc4186, ) .await?, ); diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 900b8c54f03..57ebf911d13 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -533,7 +533,7 @@ impl SlidingSync { /// Send a sliding sync request. /// /// This method contains the sending logic. It takes a generic `Request` - /// because it can be a Simplified MSC3575 or a MSC3575 `Request`. + /// because it can be an MSC4186 or an MSC3575 `Request`. async fn send_sync_request( &self, request: Request, @@ -545,7 +545,7 @@ impl SlidingSync { Request::IncomingResponse: Send + Sync + - // This is required to get back a Simplified MSC3575 `Response` whatever the + // This is required to get back an MSC4186 `Response` whatever the // `Request` type. Into, HttpError: From>, @@ -615,11 +615,10 @@ impl SlidingSync { #[cfg(not(feature = "e2e-encryption"))] let response = request.await?; - // The code manipulates `Request` and `Response` from Simplified MSC3575 because - // it's the future standard. But this function may have received a `Request` - // from Simplified MSC3575 or MSC3575. We need to get back a - // Simplified MSC3575 `Response`. - let response = Into::::into(response); + // The code manipulates `Request` and `Response` from MSC4186 because it's the + // future standard. But this function may have received a `Request` from MSC4186 + // or MSC3575. We need to get back an MSC4186 `Response`. + let response = Into::::into(response); debug!("Received response"); @@ -684,10 +683,9 @@ impl SlidingSync { let (request, request_config, position_guard) = self.generate_sync_request(&mut LazyTransactionId::new()).await?; - // The code manipulates `Request` and `Response` from Simplified MSC3575 - // because it's the future standard. If - // `Client::is_simplified_sliding_sync_enabled` is turned off, the - // Simplified MSC3575 `Request` must be transformed into a MSC3575 `Request`. + // The code manipulates `Request` and `Response` from MSC4186 because it's + // the future standard. Let's check if the generated request must be + // transformed into an MSC3575 `Request`. if !self.inner.version.is_native() { self.send_sync_request( Into::::into(request), diff --git a/testing/matrix-sdk-integration-testing/src/helpers.rs b/testing/matrix-sdk-integration-testing/src/helpers.rs index f18542fff4d..d8fbf1a5ddf 100644 --- a/testing/matrix-sdk-integration-testing/src/helpers.rs +++ b/testing/matrix-sdk-integration-testing/src/helpers.rs @@ -84,8 +84,8 @@ impl TestClientBuilder { let mut client_builder = Client::builder() .user_agent("matrix-sdk-integration-tests") .homeserver_url(homeserver_url) - // Disable Simplified MSC3575 for the integration tests as, at the time of writing - // (2024-07-15), we use a Synapse version that doesn't support Simplified MSC3575. + // Disable MSC4186 for the integration tests as, at the time of writing + // (2024-07-15), we use a Synapse version that doesn't support MSC4186. .sliding_sync_version_builder(VersionBuilder::Proxy { url: Url::parse(&sliding_sync_proxy_url) .expect("Sliding sync proxy URL is invalid"), From 7eea5628d33c3e4dcc8815083b6d1c745af26bf5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Sep 2024 11:03:29 +0200 Subject: [PATCH 057/979] chore: Update Ruma `feat-sss` to its latest commit. The new commits from `feat-sss` are about migrating `unstable-simplified-msc3575` to `unstable-msc4186`. --- Cargo.lock | 31 ++++++++++--------- Cargo.toml | 4 +-- crates/matrix-sdk-base/Cargo.toml | 2 +- .../matrix-sdk-ui/src/notification_client.rs | 3 +- .../src/room_list_service/mod.rs | 4 +-- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe91dfd0c26..40bad7c3807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2286,9 +2286,9 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +checksum = "0ff6858c1f7e2a470c5403091866fa95b36fe0dbac5d771f932c15e5ff1ee501" dependencies = [ "log", "mac", @@ -2972,9 +2972,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +checksum = "d581ff8be69d08a2efa23a959d81aa22b739073f749f067348bd4f4ba4b69195" dependencies = [ "log", "phf", @@ -4522,9 +4522,9 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0" +checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c" dependencies = [ "bitflags 2.6.0", "memchr", @@ -4997,7 +4997,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.10.1" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "assign", "js_int", @@ -5014,7 +5014,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "as_variant", "assign", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "as_variant", "base64 0.22.1", @@ -5069,7 +5069,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "as_variant", "indexmap 2.2.6", @@ -5094,7 +5094,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "http", "js_int", @@ -5108,7 +5108,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.2.0" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "as_variant", "html5ever", @@ -5120,7 +5120,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "js_int", "thiserror", @@ -5129,8 +5129,9 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ + "cfg-if", "once_cell", "proc-macro-crate", "proc-macro2", @@ -5144,7 +5145,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://github.com/matrix-org/ruma?rev=17f6e555528512319e706bb2cfe68a12ec5603b6#17f6e555528512319e706bb2cfe68a12ec5603b6" +source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index 3fd37271931..4fb7a170fbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ once_cell = "1.16.0" pin-project-lite = "0.2.9" rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } -ruma = { git = "https://github.com/matrix-org/ruma", rev = "17f6e555528512319e706bb2cfe68a12ec5603b6", features = [ +ruma = { git = "https://github.com/matrix-org/ruma", rev = "bb6d4c531aebb571fed4b1948df0118244762741", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -61,7 +61,7 @@ ruma = { git = "https://github.com/matrix-org/ruma", rev = "17f6e555528512319e70 "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = { git = "https://github.com/matrix-org/ruma", rev = "17f6e555528512319e706bb2cfe68a12ec5603b6" } +ruma-common = { git = "https://github.com/matrix-org/ruma", rev = "bb6d4c531aebb571fed4b1948df0118244762741" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 7373797a9e3..04aac0d4ec3 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -23,7 +23,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"] automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"] experimental-sliding-sync = [ "ruma/unstable-msc3575", - "ruma/unstable-simplified-msc3575", + "ruma/unstable-msc4186", ] uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"] diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index f82fe12ba1e..6269363daa2 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -27,6 +27,7 @@ use matrix_sdk_base::{ }; use ruma::{ assign, + directory::RoomTypeFilter, events::{ room::{member::StrippedRoomMemberEvent, message::SyncRoomMessageEvent}, AnyFullStateEventContent, AnyStateEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, @@ -358,7 +359,7 @@ impl NotificationClient { .required_state(required_state.clone()) .filters(Some(assign!(http::request::ListFilters::default(), { is_invite: Some(true), - not_room_types: vec!["m.space".to_owned()], + not_room_types: vec![RoomTypeFilter::Space], }))); let sync = self diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 4f07e64352d..24680a12402 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -69,7 +69,7 @@ use matrix_sdk::{ use matrix_sdk_base::sliding_sync::http; pub use room::*; pub use room_list::*; -use ruma::{assign, events::StateEventType, OwnedRoomId, RoomId}; +use ruma::{assign, directory::RoomTypeFilter, events::StateEventType, OwnedRoomId, RoomId}; pub use state::*; use thiserror::Error; use tokio::time::timeout; @@ -158,7 +158,7 @@ impl RoomListService { // If unset, both invited and joined rooms are returned. If false, no invited rooms are // returned. If true, only invited rooms are returned. is_invite: None, - not_room_types: vec!["m.space".to_owned()], + not_room_types: vec![RoomTypeFilter::Space], }))), ) .await From 57352f0154286640837b96c66a138f9cb6ea013a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Sep 2024 11:38:29 +0200 Subject: [PATCH 058/979] =?UTF-8?q?chore(sdk):=20Rename=20a=20variable=20`?= =?UTF-8?q?from=5F=E2=80=A6`=20to=20`with=5F=E2=80=A6`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch renames the variable `from_msc4186` to `with_msc4186` for better clarity. --- crates/matrix-sdk-base/src/sliding_sync/mod.rs | 12 ++++++------ crates/matrix-sdk/src/sliding_sync/client.rs | 4 ++-- crates/matrix-sdk/src/sliding_sync/mod.rs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 8f6f4dada8b..6f46e09a767 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -119,14 +119,14 @@ impl BaseClient { /// sync. /// * `previous_events_provider` - Timeline events prior to the current /// sync. - /// * `from_msc4186` - Whether the `response` comes from simplified sliding + /// * `with_msc4186` - Whether the `response` comes from simplified sliding /// sync (MSC4186) or sliding sync (MSC3575). #[instrument(skip_all, level = "trace")] pub async fn process_sliding_sync( &self, response: &http::Response, previous_events_provider: &PEP, - from_msc4186: bool, + with_msc4186: bool, ) -> Result { let http::Response { // FIXME not yet supported by sliding sync. see @@ -180,7 +180,7 @@ impl BaseClient { &mut room_info_notable_updates, &mut notifications, &mut ambiguity_cache, - from_msc4186, + with_msc4186, ) .await?; @@ -352,7 +352,7 @@ impl BaseClient { room_info_notable_updates: &mut BTreeMap, notifications: &mut BTreeMap>, ambiguity_cache: &mut AmbiguityCache, - from_msc4186: bool, + with_msc4186: bool, ) -> Result<(RoomInfo, Option, Option, Option)> { // This method may change `room_data` (see the terrible hack describes below) @@ -383,7 +383,7 @@ impl BaseClient { // The SDK manipulates MSC4186 `Request` and `Response` though. In MSC4186, // `bump_stamp` replaces `timestamp`, which does NOT represent a time! This // hack must really, only, apply to the proxy, so to MSC3575 strictly (hence - // the `from_msc4186` argument). + // the `with_msc4186` argument). // // The proxy uses the `origin_server_ts` event's value to fill the `timestamp` // room's value (which is a bad idea[^1]). If `timestamp` is `None`, let's find @@ -392,7 +392,7 @@ impl BaseClient { // [^1]: using `origin_server_ts` for `timestamp` is a bad idea because // this value can be forged by a malicious user. Anyway, that's how it works // in the proxy. MSC4186 has another mechanism which fixes the problem. - if !from_msc4186 && room_data.bump_stamp.is_none() { + if !with_msc4186 && room_data.bump_stamp.is_none() { if let Some(invite_state) = &room_data.invite_state { room_data.to_mut().bump_stamp = invite_state.iter().rev().find_map(|invite_state| { diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index f6974835839..d038e75d3f5 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -268,7 +268,7 @@ impl<'a> SlidingSyncResponseProcessor<'a> { pub async fn handle_room_response( &mut self, response: &http::Response, - from_msc4186: bool, + with_msc4186: bool, ) -> Result<()> { self.response = Some( self.client @@ -276,7 +276,7 @@ impl<'a> SlidingSyncResponseProcessor<'a> { .process_sliding_sync( response, &SlidingSyncPreviousEventsProvider(self.rooms), - from_msc4186, + with_msc4186, ) .await?, ); diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 57ebf911d13..de002be7388 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -684,8 +684,8 @@ impl SlidingSync { self.generate_sync_request(&mut LazyTransactionId::new()).await?; // The code manipulates `Request` and `Response` from MSC4186 because it's - // the future standard. Let's check if the generated request must be - // transformed into an MSC3575 `Request`. + // the future standard (at the time of writing: 2024-09-09). Let's check if + // the generated request must be transformed into an MSC3575 `Request`. if !self.inner.version.is_native() { self.send_sync_request( Into::::into(request), From 7d7142add3adfcb6c3ec485f31107cfe4f292084 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 5 Sep 2024 17:22:45 +0200 Subject: [PATCH 059/979] timeline: check that unique IDs are indeed unique And log an error in production builds if that's not the case. --- .../src/timeline/controller/state.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index d1ec4e10790..84ae7c567a3 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -15,6 +15,7 @@ use std::{collections::VecDeque, future::Future, sync::Arc}; use eyeball_im::{ObservableVector, ObservableVectorTransaction, ObservableVectorTransactionEntry}; +use itertools::Itertools as _; use matrix_sdk::{deserialized_responses::SyncTimelineEvent, send_queue::SendHandle}; use matrix_sdk_base::deserialized_responses::TimelineEvent; #[cfg(test)] @@ -343,9 +344,31 @@ impl TimelineStateTransaction<'_> { self.adjust_day_dividers(day_divider_adjuster); + self.check_no_unused_unique_ids(); total } + fn check_no_unused_unique_ids(&self) { + let duplicates = self + .items + .iter() + .duplicates_by(|item| item.unique_id()) + .map(|item| item.unique_id()) + .collect::>(); + + if !duplicates.is_empty() { + #[cfg(any(debug_assertions, test))] + panic!("duplicate unique ids in this timeline:{:?}\n{:?}", duplicates, self.items); + + #[cfg(not(any(debug_assertions, test)))] + tracing::error!( + "duplicate unique ids in this timeline:{:?}\n{:?}", + duplicates, + self.items + ); + } + } + /// Handle a remote event. /// /// Returns the number of timeline updates that were made. From 24d4e60c2bd149b51d6be00dcc9e3c6084e81c27 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 2 Sep 2024 11:41:36 +0200 Subject: [PATCH 060/979] crypto: Bugfix - UTD messages showing unexpected red padlock warning --- crates/matrix-sdk-ui/CHANGELOG.md | 2 + .../src/timeline/event_item/mod.rs | 5 ++ .../src/timeline/tests/shields.rs | 51 ++++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 6945684010a..55f9cb79440 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -12,6 +12,8 @@ Bug fixes: - `UtdHookManager` no longer re-reports UTD events as late decryptions. ([#3480](https://github.com/matrix-org/matrix-rust-sdk/pull/3480)) +- Messages that we were unable to decrypt no longer display a red padlock. + ([#3956](https://github.com/matrix-org/matrix-rust-sdk/issues/3956)) Other changes: 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 3a8e7033d10..d742dc3be0e 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -381,6 +381,11 @@ impl EventTimelineItem { return None; } + // An unable-to-decrypt message has no authenticity shield. + if let TimelineItemContent::UnableToDecrypt(_) = self.content() { + return None; + } + match self.encryption_info() { Some(info) => { if strict { diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index 73e2798f6c6..ad86b21c913 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -4,7 +4,15 @@ use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode}; use matrix_sdk_test::{async_test, sync_timeline_event, ALICE}; use ruma::{ event_id, - events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, + events::{ + room::{ + encrypted::{ + EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent, + }, + message::RoomMessageEventContent, + }, + AnyMessageLikeEventContent, + }, }; use stream_assert::assert_next_matches; @@ -111,3 +119,44 @@ async fn test_local_sent_in_clear_shield() { Some(ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }) ); } + +#[async_test] +/// Test a bug that was causing unable to decrypt messages to have a `message +/// sent in clear` red warning. +async fn test_utd_shield() { + // Given we are in an encrypted room + let timeline = TestTimeline::with_is_room_encrypted(true); + let mut stream = timeline.subscribe().await; + + let f = &timeline.factory; + + // When we receive a message that we can't decrypt + timeline + .handle_live_event( + f.event(RoomEncryptedEventContent::new( + EncryptedEventScheme::MegolmV1AesSha2( + MegolmV1AesSha2ContentInit { + ciphertext: "\ + AwgAEpABNOd7Rxpc/98gaaOanApQ/h40uNyYE/aiFd8PKeQPH65bwuxBy/glodmteryH\ + 4t5d0cKSPjb+996yK90+A8YUevQKBuC+/+4iRF2CSqMNvArdOCnFHJdZBuCyRP6W82DZ\ + sR1w5X/tKGs/A9egJdxomLCzMRZarayTXUlgMT8Kj7E9zKOgyLEZGki6Y9IPybfrU3+S\ + b4VbF7RKY395/lIZFiLvJ5hUT+Ao1k13opeTE9GHtdOK0GzQPVFLnN61pRa3K/vV9Otk\ + D0QbVS/4mE3C29+yIC1lEkwA" + .to_owned(), + sender_key: "peI8cfSKqZvTOAfY0Od2e7doDpJ1cxdBsOhSceTLU3E".to_owned(), + device_id: "KDCTEHOVSS".into(), + session_id: "C25PoE+4MlNidQD0YU5ibZqHawV0zZ/up7R8vYJBYTY".into(), + } + .into(), + ), + None, + )) + .sender(&ALICE), + ) + .await; + + // Then the message is displayed with no shield + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + let shield = item.as_event().unwrap().get_shield(false); + assert!(shield.is_none()); +} From a07be884b75eee35915a4f95bf12b5c5115effbc Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 5 Sep 2024 17:30:06 +0200 Subject: [PATCH 061/979] tests: try to address intermittent failure of test_incremental_upload_of_keys My theory is that the intermittent failure depends on the ordering of the requests, and if the /keys/upload request happened before the key backup request, then after failing the next key backup request wouldn't run. This is likely a small typo that the key upload returns a 404 error instead of a 200, let's see if this improves the situation. --- .../tests/integration/encryption/backups.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/encryption/backups.rs b/crates/matrix-sdk/tests/integration/encryption/backups.rs index 630a7ef01fc..0982ac9106d 100644 --- a/crates/matrix-sdk/tests/integration/encryption/backups.rs +++ b/crates/matrix-sdk/tests/integration/encryption/backups.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{fs::File, io::Write, sync::Arc}; +use std::{fs::File, io::Write, sync::Arc, time::Duration}; use anyhow::Result; use assert_matches::assert_matches; @@ -574,7 +574,7 @@ async fn setup_create_room_and_send_message_mocks(server: &wiremock::MockServer) Mock::given(method("POST")) .and(path("/_matrix/client/r0/keys/upload")) .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(404).set_body_json(json!({ + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "one_time_key_counts": { "curve25519": 50, "signed_curve25519": 50 @@ -716,6 +716,8 @@ async fn test_incremental_upload_of_keys() -> Result<()> { #[async_test] #[cfg(feature = "experimental-sliding-sync")] async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { + use tokio::time::sleep; + let user_id = user_id!("@example:morpheus.localhost"); let session = MatrixSession { @@ -774,8 +776,8 @@ async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { let sliding = client .sliding_sync("main")? .with_all_extensions() - .poll_timeout(std::time::Duration::from_secs(3)) - .network_timeout(std::time::Duration::from_secs(3)) + .poll_timeout(Duration::from_secs(3)) + .network_timeout(Duration::from_secs(3)) .add_list( matrix_sdk::SlidingSyncList::builder("all") .sync_mode(matrix_sdk::SlidingSyncMode::new_selective().add_range(0..=20)), @@ -814,8 +816,8 @@ async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { .mount(&server) .await; - // let the slinding sync loop run for a bit - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + // let the sliding sync loop run for a bit. + sleep(Duration::from_secs(1)).await; server.verify().await; Ok(()) @@ -1384,7 +1386,7 @@ async fn test_enable_from_secret_storage_and_download_after_utd() { // Wait for the key to be downloaded from backup. { - let room_keys = timeout(room_key_stream.next(), std::time::Duration::from_secs(5)) + let room_keys = timeout(room_key_stream.next(), Duration::from_secs(5)) .await .expect("did not get a room key stream update within 5 seconds") .expect("room_key_stream.next() returned None") @@ -1514,7 +1516,7 @@ async fn test_enable_from_secret_storage_and_download_after_utd_from_old_message // Wait for the key to be downloaded from backup. { - let room_keys = timeout(room_key_stream.next(), std::time::Duration::from_secs(5)) + let room_keys = timeout(room_key_stream.next(), Duration::from_secs(5)) .await .expect("did not get a room key stream update within 5 seconds") .expect("room_key_stream.next() returned None") From 19e89bbd6a0df1c4a5636252b02b9e16cc7ab44d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Sep 2024 12:23:15 +0200 Subject: [PATCH 062/979] tests: make `test_incremental_upload_of_keys_sliding_sync` less dependent on timing --- .../tests/integration/encryption/backups.rs | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/encryption/backups.rs b/crates/matrix-sdk/tests/integration/encryption/backups.rs index 0982ac9106d..f510d40b706 100644 --- a/crates/matrix-sdk/tests/integration/encryption/backups.rs +++ b/crates/matrix-sdk/tests/integration/encryption/backups.rs @@ -72,7 +72,7 @@ const ROOM_KEY: &[u8] = b"\ HztoSJUr/2Y\n\ -----END MEGOLM SESSION DATA-----"; -async fn mount_once( +async fn mount_and_assert_called_once( server: &wiremock::MockServer, method_argument: &str, path_argument: &str, @@ -105,7 +105,7 @@ async fn test_create() { client.restore_session(session).await.unwrap(); - mount_once( + mount_and_assert_called_once( &server, "POST", "_matrix/client/unstable/room_keys/version", @@ -174,7 +174,7 @@ async fn test_creation_failure() { let (client, server) = no_retry_test_client_with_server().await; client.restore_session(session).await.unwrap(); - mount_once( + mount_and_assert_called_once( &server, "POST", "_matrix/client/unstable/room_keys/version", @@ -255,7 +255,7 @@ async fn test_disabling() { let (client, server) = no_retry_test_client_with_server().await; client.restore_session(session).await.unwrap(); - mount_once( + mount_and_assert_called_once( &server, "POST", "_matrix/client/unstable/room_keys/version", @@ -263,7 +263,7 @@ async fn test_disabling() { ) .await; - mount_once( + mount_and_assert_called_once( &server, "DELETE", "_matrix/client/r0/room_keys/version/1", @@ -432,7 +432,7 @@ async fn setup_backups(client: &Client, server: &wiremock::MockServer) { client.encryption().import_room_keys(room_key_path, "1234").await.unwrap(); - mount_once( + mount_and_assert_called_once( server, "POST", "_matrix/client/unstable/room_keys/version", @@ -470,7 +470,7 @@ async fn test_steady_state_waiting() { setup_backups(&client, &server).await; - mount_once( + mount_and_assert_called_once( &server, "PUT", "_matrix/client/unstable/room_keys/keys", @@ -656,7 +656,7 @@ async fn test_incremental_upload_of_keys() -> Result<()> { // This is the call we want to check. The newly created outbound session should // be uploaded to backup. - mount_once( + mount_and_assert_called_once( &server, "PUT", "_matrix/client/unstable/room_keys/keys", @@ -716,7 +716,7 @@ async fn test_incremental_upload_of_keys() -> Result<()> { #[async_test] #[cfg(feature = "experimental-sliding-sync")] async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { - use tokio::time::sleep; + use tokio::task::spawn_blocking; let user_id = user_id!("@example:morpheus.localhost"); @@ -738,17 +738,20 @@ async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { // This is the call we want to check. The newly created outbound session should // be uploaded to backup. - mount_once( - &server, - "PUT", - "_matrix/client/unstable/room_keys/keys", - ResponseTemplate::new(200).set_body_json(json!({ - "count": 1, - "etag": "abcdefg", - } - )), - ) - .await; + let (endpoint_called_sender, endpoint_called_receiver) = std::sync::mpsc::channel(); + Mock::given(method("PUT")) + .and(path("_matrix/client/unstable/room_keys/keys")) + .and(header("authorization", "Bearer 1234")) + .respond_with(move |_req: &wiremock::Request| { + let _ = endpoint_called_sender.send(()); + ResponseTemplate::new(200).set_body_json(json!({ + "count": 1, + "etag": "abcdefg", + })) + }) + .expect(1) + .mount(&server) + .await; setup_create_room_and_send_message_mocks(&server).await; @@ -785,9 +788,8 @@ async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { .build() .await?; - let s = sliding.clone(); - spawn(async move { - let stream = s.sync(); + let sync_task = spawn(async move { + let stream = sliding.sync(); pin_mut!(stream); while let Some(up) = stream.next().await { tracing::warn!("received update: {up:?}"); @@ -816,8 +818,19 @@ async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { .mount(&server) .await; - // let the sliding sync loop run for a bit. - sleep(Duration::from_secs(1)).await; + // Wait for the endpoint to be called, at most for 10 seconds. + // + // Don't plain use `recv_timeout()` on the main task, since this would prevent + // forward progress of the wiremock code. + timeout( + spawn_blocking(move || endpoint_called_receiver.recv().unwrap()), + Duration::from_secs(10), + ) + .await + .expect("timeout waiting for the key backup endpoint to be called") + .expect("join error (internal timeout)"); + + sync_task.abort(); server.verify().await; Ok(()) @@ -853,7 +866,7 @@ async fn test_steady_state_waiting_errors() { "The steady state method should tell us that it couldn't reach the homeserver" ); - mount_once( + mount_and_assert_called_once( &server, "PUT", "_matrix/client/unstable/room_keys/keys", From 38ed66c1b1768907c2a62b35e0f688878a3a6d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Sep 2024 16:21:55 +0200 Subject: [PATCH 063/979] test: Test that a timeline decrypts an event if a backup got enabled --- .../src/tests/timeline.rs | 174 +++++++++++++++++- 1 file changed, 170 insertions(+), 4 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 80ebce3b2db..38b96ca3d52 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -19,12 +19,23 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use assign::assign; use eyeball_im::{Vector, VectorDiff}; +use futures::pin_mut; use futures_util::{FutureExt, StreamExt}; -use matrix_sdk::ruma::{ - api::client::room::create_room::v3::Request as CreateRoomRequest, - events::room::message::RoomMessageEventContent, MilliSecondsSinceUnixEpoch, +use matrix_sdk::{ + encryption::{backups::BackupState, EncryptionSettings}, + ruma::{ + api::client::room::create_room::v3::Request as CreateRoomRequest, + events::{ + room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, + InitialStateEvent, + }, + MilliSecondsSinceUnixEpoch, + }, }; -use matrix_sdk_ui::timeline::{EventSendState, ReactionStatus, RoomExt, TimelineItem}; +use matrix_sdk_ui::timeline::{ + EventSendState, ReactionStatus, RoomExt, TimelineItem, TimelineItemContent, +}; +use similar_asserts::assert_eq; use tokio::{ spawn, task::JoinHandle, @@ -294,3 +305,158 @@ async fn test_stale_local_echo_time_abort_edit() { alice_sync.abort(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_enabling_backups_retries_decryption() { + let encryption_settings = EncryptionSettings { + auto_enable_backups: true, + backup_download_strategy: + matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure, + ..Default::default() + }; + let alice = TestClientBuilder::new("alice") + .use_sqlite() + .encryption_settings(encryption_settings) + .build() + .await + .unwrap(); + + alice.encryption().wait_for_e2ee_initialization_tasks().await; + alice + .encryption() + .recovery() + .enable() + .with_passphrase("Bomber's code") + .await + .expect("We should be able to enable recovery"); + + let user_id = alice.user_id().expect("We should have access to the user ID now"); + assert_eq!( + alice.encryption().backups().state(), + BackupState::Enabled, + "The backup state should now be BackupState::Enabled" + ); + + // Set up sync for user Alice, and create a room. + let alice_sync = spawn({ + let alice = alice.clone(); + async move { + alice.sync(Default::default()).await.expect("sync failed!"); + } + }); + + debug!("Creating room…"); + + let initial_state = + vec![InitialStateEvent::new(RoomEncryptionEventContent::with_recommended_defaults()) + .to_raw_any()]; + + let room = alice + .create_room(assign!(CreateRoomRequest::new(), { + is_direct: true, + initial_state, + preset: Some(matrix_sdk::ruma::api::client::room::create_room::v3::RoomPreset::PrivateChat) + })) + .await + .unwrap(); + + assert!(room + .is_encrypted() + .await + .expect("We should be able to check that the room is encrypted")); + + let event_id = room + .send(RoomMessageEventContent::text_plain("It's a secret to everybody!")) + .await + .expect("We should be able to send a message to our new room") + .event_id; + + alice + .encryption() + .backups() + .wait_for_steady_state() + .await + .expect("We should be able to wait for our room keys to be uploaded"); + + // We don't need Alice anymore. + alice_sync.abort(); + + // I know that this is Alice again, but it's the Alice's second device which she + // named Bob. + let bob = TestClientBuilder::with_exact_username(user_id.localpart().to_owned()) + .use_sqlite() + .encryption_settings(encryption_settings) + .build() + .await + .unwrap(); + + bob.encryption().wait_for_e2ee_initialization_tasks().await; + assert!(!bob.encryption().backups().are_enabled().await, "Backups shouldn't be enabled yet"); + + // Sync once to get access to the room. + bob.sync_once(Default::default()).await.expect("Bob should be able to perform an initial sync"); + // Let Bob sync continuously now. + let bob_sync = spawn({ + let bob = bob.clone(); + async move { + bob.sync(Default::default()).await.expect("sync failed!"); + } + }); + + // Load the event into the timeline. + let room = bob.get_room(room.room_id()).expect("We should have access to our room now"); + let timeline = room.timeline().await.expect("We should be able to get a timeline for our room"); + timeline + .paginate_backwards(50) + .await + .expect("We should be able to paginate the timeline to fetch the history"); + + let item = + timeline.item_by_event_id(&event_id).await.expect("The event should be in the timeline"); + + // The event is not decrypted yet. + assert_matches!(item.content(), TimelineItemContent::UnableToDecrypt(_)); + + // We now connect to the backup which will not give us the room key right away, + // we first need to encounter a UTD to attempt the download. + bob.encryption() + .recovery() + .recover("Bomber's code") + .await + .expect("We should be able to recover from Bob's device"); + + // Let's subscribe to our timeline so we don't miss the transition from UTD to + // decrypted event. + let (_, mut stream) = timeline + .subscribe_filter_map(|item| { + item.as_event().cloned().filter(|item| item.event_id() == Some(&event_id)) + }) + .await; + + let room_key_stream = bob.encryption().backups().room_keys_for_room_stream(room.room_id()); + pin_mut!(room_key_stream); + + // Wait for the room key to arrive before continuing. + if let Some(update) = room_key_stream.next().await { + let _update = update.expect( + "We should not miss the update since we're only broadcasting a small amount of updates", + ); + } + + // Alright, we should now receive an update that the event had been decrypted. + let _vector_diff = timeout(Duration::from_secs(5), stream.next()).await.unwrap().unwrap(); + + // Let's fetch the event again. + let item = + timeline.item_by_event_id(&event_id).await.expect("The event should be in the timeline"); + + // Yup it's decrypted great. + assert_let!( + TimelineItemContent::Message(message) = item.content(), + "The event should have been decrypted now" + ); + + assert_eq!(message.body(), "It's a secret to everybody!"); + + bob_sync.abort(); +} From 626b3d152c648d169b4dc3502db1740d668c889d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 9 Sep 2024 14:38:45 +0200 Subject: [PATCH 064/979] test: Check that we don't mark keys as downloaded before backups were enabled --- crates/matrix-sdk/src/encryption/tasks.rs | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/crates/matrix-sdk/src/encryption/tasks.rs b/crates/matrix-sdk/src/encryption/tasks.rs index d4cf23d0feb..295457a7ceb 100644 --- a/crates/matrix-sdk/src/encryption/tasks.rs +++ b/crates/matrix-sdk/src/encryption/tasks.rs @@ -141,6 +141,7 @@ impl Drop for BackupDownloadTask { } impl BackupDownloadTask { + #[cfg(not(test))] const DOWNLOAD_DELAY_MILLIS: u64 = 100; pub(crate) fn new(client: WeakClient) -> Self { @@ -215,6 +216,7 @@ impl BackupDownloadTask { download_request: RoomKeyDownloadRequest, ) { // Wait a bit, perhaps the room key will arrive in the meantime. + #[cfg(not(test))] tokio::time::sleep(Duration::from_millis(Self::DOWNLOAD_DELAY_MILLIS)).await; // Now take the lock, and check that we still want to do a download. If we do, @@ -239,6 +241,7 @@ impl BackupDownloadTask { // Before we drop the lock, indicate to other tasks that may be considering this // session that we're going to go ahead and do a download. state.downloaded_sessions.insert(download_request.to_room_key_info()); + client }; @@ -267,6 +270,7 @@ impl BackupDownloadTask { state.failures_cache.insert(room_key_info); } } + state.active_tasks.remove(&download_request.event_id); } } @@ -371,3 +375,71 @@ impl BackupDownloadTaskListenerState { true } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod test { + use matrix_sdk_test::async_test; + use ruma::{event_id, room_id}; + use serde_json::json; + use wiremock::MockServer; + + use super::*; + use crate::test_utils::logged_in_client; + + // Test that, if backups are not enabled, we don't incorrectly mark a room key + // as downloaded. + #[async_test] + async fn test_disabled_backup_does_not_mark_room_key_as_downloaded() { + let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost"); + let event_id = event_id!("$JbFHtZpEJiH8uaajZjPLz0QUZc1xtBR9rPGBOjF6WFM"); + let session_id = "session_id"; + + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let weak_client = WeakClient::from_client(&client); + + let event_content = json!({ + "event_id": event_id, + "origin_server_ts": 1698579035927u64, + "sender": "@example2:morpheus.localhost", + "type": "m.room.encrypted", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "AwgAEpABhetEzzZzyYrxtEVUtlJnZtJcURBlQUQJ9irVeklCTs06LwgTMQj61PMUS4Vy\ + YOX+PD67+hhU40/8olOww+Ud0m2afjMjC3wFX+4fFfSkoWPVHEmRVucfcdSF1RSB4EmK\ + PIP4eo1X6x8kCIMewBvxl2sI9j4VNvDvAN7M3zkLJfFLOFHbBviI4FN7hSFHFeM739Zg\ + iwxEs3hIkUXEiAfrobzaMEM/zY7SDrTdyffZndgJo7CZOVhoV6vuaOhmAy4X2t4UnbuV\ + JGJjKfV57NAhp8W+9oT7ugwO", + "device_id": "KIUVQQSDTM", + "sender_key": "LvryVyoCjdONdBCi2vvoSbI34yTOx7YrCFACUEKoXnc", + "session_id": "64H7XKokIx0ASkYDHZKlT5zd/Zccz/cQspPNdvnNULA" + } + }); + + let event: Raw = + serde_json::from_value(event_content).expect(""); + + let state = Arc::new(Mutex::new(BackupDownloadTaskListenerState::new(weak_client))); + let download_request = RoomKeyDownloadRequest { + room_id: room_id.into(), + megolm_session_id: session_id.to_owned(), + event, + event_id: event_id.into(), + }; + + assert!( + !client.encryption().backups().are_enabled().await, + "Backups should not be enabled." + ); + + BackupDownloadTask::handle_download_request(state.clone(), download_request).await; + + { + let state = state.lock().await; + assert!( + !state.downloaded_sessions.contains(&(room_id.to_owned(), session_id.to_owned())), + "Backups are not enabled, we should not mark any room keys as downloaded." + ) + } + } +} From e8a920118f1cc96c79238f6fd288daa07b742b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Sep 2024 16:23:08 +0200 Subject: [PATCH 065/979] timeline: Retry decryption if a room key backup gets enabled --- crates/matrix-sdk-ui/src/timeline/builder.rs | 40 ++++++++++++++++++++ crates/matrix-sdk-ui/src/timeline/mod.rs | 4 ++ 2 files changed, 44 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 23b14c13199..04bfcad83fe 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -16,6 +16,7 @@ use std::{collections::BTreeSet, sync::Arc}; use futures_util::{pin_mut, StreamExt}; use matrix_sdk::{ + encryption::backups::BackupState, event_cache::{EventsOrigin, RoomEventCacheUpdate}, executor::spawn, Room, @@ -370,6 +371,44 @@ impl TimelineBuilder { }) }; + let room_key_backup_enabled_join_handle = { + let inner = controller.clone(); + let stream = client.encryption().backups().state_stream(); + + spawn(async move { + pin_mut!(stream); + + while let Some(update) = stream.next().await { + match update { + // If the backup got enabled, or we lagged and thus missed that the backup + // might be enabled, retry to decrypt all the events. Please note, depending + // on the backup download strategy, this might do two things under the + // assumption that the backup contains the relevant room keys: + // + // 1. It will decrypt the events, if `BackupDownloadStrategy` has been set + // to `OneShot`. + // 2. It will fail to decrypt the event, but try to download the room key to + // decrypt it if the `BackupDownloadStrategy` has been set to + // `AfterDecryptionFailure`. + Ok(BackupState::Enabled) | Err(_) => { + let room = inner.room(); + inner.retry_event_decryption(room, None).await; + } + // The other states aren't interesting since they are either still enabling + // the backup or have the backup in the disabled state. + Ok( + BackupState::Unknown + | BackupState::Creating + | BackupState::Resuming + | BackupState::Disabling + | BackupState::Downloading + | BackupState::Enabling, + ) => (), + } + } + }) + }; + let timeline = Timeline { controller, event_cache: room_event_cache, @@ -379,6 +418,7 @@ impl TimelineBuilder { room_update_join_handle, pinned_events_join_handle, room_key_from_backups_join_handle, + room_key_backup_enabled_join_handle, local_echo_listener_handle, _event_cache_drop_handle: event_cache_drop, }), diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 584d9aeb826..2e346bebb27 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -851,6 +851,7 @@ struct TimelineDropHandle { room_update_join_handle: JoinHandle<()>, pinned_events_join_handle: Option>, room_key_from_backups_join_handle: JoinHandle<()>, + room_key_backup_enabled_join_handle: JoinHandle<()>, local_echo_listener_handle: JoinHandle<()>, _event_cache_drop_handle: Arc, } @@ -860,12 +861,15 @@ impl Drop for TimelineDropHandle { for handle in self.event_handler_handles.drain(..) { self.client.remove_event_handler(handle); } + if let Some(handle) = self.pinned_events_join_handle.take() { handle.abort() }; + self.local_echo_listener_handle.abort(); self.room_update_join_handle.abort(); self.room_key_from_backups_join_handle.abort(); + self.room_key_backup_enabled_join_handle.abort(); } } From 26e6a038a145a6731dd3e9c0a0fc2715b12abf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Sep 2024 16:24:21 +0200 Subject: [PATCH 066/979] backups: Don't mark a room key as downloaded if we did not attempt to download it --- crates/matrix-sdk/CHANGELOG.md | 8 ++++++++ .../matrix-sdk/src/encryption/backups/mod.rs | 19 ++++++++++++++++--- crates/matrix-sdk/src/encryption/tasks.rs | 8 +++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 19d82de5184..3705158a858 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -45,6 +45,14 @@ Additions: - WidgetDriver: Support the `"delay"` field in the `send_event` widget actions. This allows to send delayed events, as defined in [MSC4157](https://github.com/matrix-org/matrix-spec-proposals/pull/4157) +Bug fixes: + +- Fix a bug where room keys were considered to be downloaded before backups were + enabled. This bug only affects the + `BackupDownloadStrategy::AfterDecryptionFailure`, where no attempt would be + made to download a room key, if a decryption failure with a given room key + would have been encountered before the backups were enabled. + # 0.7.0 Breaking changes: diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index d863f62a96b..59457221d10 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -445,7 +445,16 @@ impl Backups { } /// Download a single room key from the server-side key backup. - pub async fn download_room_key(&self, room_id: &RoomId, session_id: &str) -> Result<(), Error> { + /// + /// Returns `true` if we managed to download a room key, `false` or an error + /// if we failed to download it. `false` indicates that there was no + /// error, we just don't have backups enabled so we can't download a + /// room key. + pub async fn download_room_key( + &self, + room_id: &RoomId, + session_id: &str, + ) -> Result { let olm_machine = self.client.olm_machine().await; let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?; @@ -471,10 +480,14 @@ impl Backups { self.handle_downloaded_room_keys(response, decryption_key, &version, olm_machine) .await?; + + Ok(true) + } else { + Ok(false) } + } else { + Ok(false) } - - Ok(()) } /// Set the state of the backup. diff --git a/crates/matrix-sdk/src/encryption/tasks.rs b/crates/matrix-sdk/src/encryption/tasks.rs index 295457a7ceb..c5a43b962d5 100644 --- a/crates/matrix-sdk/src/encryption/tasks.rs +++ b/crates/matrix-sdk/src/encryption/tasks.rs @@ -256,12 +256,18 @@ impl BackupDownloadTask { { let mut state = state.lock().await; let room_key_info = download_request.to_room_key_info(); + match result { - Ok(_) => { + Ok(true) => { // We successfully downloaded the session. We can clear any record of previous // backoffs from the failures cache, because we won't be needing them again. state.failures_cache.remove(std::iter::once(&room_key_info)) } + Ok(false) => { + // We did not find a valid backup decryption key or backup version, we did not + // even attempt to download the room key. + state.downloaded_sessions.remove(&room_key_info); + } Err(_) => { // We were unable to download the session. Update the failure cache so that we // back off from more requests, and also remove the entry from the list of From 67a4a322f56bbb493c8b350fa65017972bd55bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Sep 2024 16:24:43 +0200 Subject: [PATCH 067/979] backups: Don't queue up room keys to be downloaded if backups aren't enabled --- crates/matrix-sdk/src/encryption/tasks.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/matrix-sdk/src/encryption/tasks.rs b/crates/matrix-sdk/src/encryption/tasks.rs index c5a43b962d5..e2c84f41761 100644 --- a/crates/matrix-sdk/src/encryption/tasks.rs +++ b/crates/matrix-sdk/src/encryption/tasks.rs @@ -344,6 +344,16 @@ impl BackupDownloadTaskListenerState { return false; }; + // If backups aren't enabled, there's no point in trying to download a room key. + if !client.encryption().backups().are_enabled().await { + debug!( + ?download_request, + "Not performing backup download because backups are not enabled" + ); + + return false; + } + // Check if the keys for this message have arrived in the meantime. // If we get a StoreError doing the lookup, we assume the keys haven't arrived // (though if the store is returning errors, probably something else is From 4e541ad82556cd6ff08927ebe8aea72fe1553c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Sep 2024 16:49:17 +0200 Subject: [PATCH 068/979] backups: Expire downloaded room keys so they get retried if a better one is found --- crates/matrix-sdk/src/encryption/tasks.rs | 27 ++++++++++------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/tasks.rs b/crates/matrix-sdk/src/encryption/tasks.rs index e2c84f41761..43c8acd4636 100644 --- a/crates/matrix-sdk/src/encryption/tasks.rs +++ b/crates/matrix-sdk/src/encryption/tasks.rs @@ -12,11 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::{BTreeMap, HashSet}, - sync::Arc, - time::Duration, -}; +use std::{collections::BTreeMap, sync::Arc, time::Duration}; use matrix_sdk_common::failures_cache::FailuresCache; use ruma::{ @@ -37,6 +33,9 @@ use crate::{ Client, }; +/// A cache of room keys we already downloaded. +type DownloadCache = FailuresCache; + #[derive(Default)] pub(crate) struct ClientTasks { #[cfg(feature = "e2e-encryption")] @@ -266,13 +265,13 @@ impl BackupDownloadTask { Ok(false) => { // We did not find a valid backup decryption key or backup version, we did not // even attempt to download the room key. - state.downloaded_sessions.remove(&room_key_info); + state.downloaded_sessions.remove(std::iter::once(&room_key_info)); } Err(_) => { // We were unable to download the session. Update the failure cache so that we // back off from more requests, and also remove the entry from the list of // sessions that we are downloading. - state.downloaded_sessions.remove(&room_key_info); + state.downloaded_sessions.remove(std::iter::once(&room_key_info)); state.failures_cache.insert(room_key_info); } } @@ -300,12 +299,7 @@ struct BackupDownloadTaskListenerState { /// The idea here is that once we've (successfully) downloaded a session /// from the backup, there's not much point trying again even if we get /// another UTD event that uses the same session. - /// - /// TODO: that's not quite right though. In theory another client could - /// update the backup with an earlier ratchet state, giving us access - /// to earlier messages in the session. In which case, maybe this - /// should expire? - downloaded_sessions: HashSet, + downloaded_sessions: DownloadCache, } impl BackupDownloadTaskListenerState { @@ -320,7 +314,10 @@ impl BackupDownloadTaskListenerState { client, failures_cache: FailuresCache::with_settings(Duration::from_secs(60 * 60 * 24), 60), active_tasks: Default::default(), - downloaded_sessions: Default::default(), + downloaded_sessions: DownloadCache::with_settings( + Duration::from_secs(60 * 60 * 24), + 60, + ), } } @@ -373,7 +370,7 @@ impl BackupDownloadTaskListenerState { if self.downloaded_sessions.contains(&room_key_info) { debug!( ?download_request, - "Not performing backup download because this session has already been downloaded" + "Not performing backup download because this session has already been downloaded recently" ); return false; }; From dcc20b6c96608267d6898a6c3cd427db406f759b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 9 Sep 2024 14:48:14 +0200 Subject: [PATCH 069/979] backups: Rename the term session to room key The term session is usually only used in the crypto crate to reference a Megolm session, the rest of the SDK uses the name from the event and the Matrix spec, this should lower the amount of confusion since the main crate has already a session concept and its unrelated to end-to-end encryption. --- crates/matrix-sdk/src/encryption/tasks.rs | 46 +++++++++++------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/tasks.rs b/crates/matrix-sdk/src/encryption/tasks.rs index 43c8acd4636..5955a1ea961 100644 --- a/crates/matrix-sdk/src/encryption/tasks.rs +++ b/crates/matrix-sdk/src/encryption/tasks.rs @@ -113,7 +113,7 @@ struct RoomKeyDownloadRequest { /// The event we could not decrypt. event: Raw, - /// The megolm session that the event was encrypted with. + /// The unique ID of the room key that the event was encrypted with. megolm_session_id: String, } @@ -206,7 +206,7 @@ impl BackupDownloadTask { } } - /// Handle a request to download a session for a given event. + /// Handle a request to download a room key for a given event. /// /// Sleeps for a while to see if the key turns up; then checks if we still /// want to do a download, and does the download if so. @@ -238,8 +238,8 @@ impl BackupDownloadTask { } // Before we drop the lock, indicate to other tasks that may be considering this - // session that we're going to go ahead and do a download. - state.downloaded_sessions.insert(download_request.to_room_key_info()); + // room key, that we're going to go ahead and do a download. + state.downloaded_room_keys.insert(download_request.to_room_key_info()); client }; @@ -258,20 +258,20 @@ impl BackupDownloadTask { match result { Ok(true) => { - // We successfully downloaded the session. We can clear any record of previous + // We successfully downloaded the room key. We can clear any record of previous // backoffs from the failures cache, because we won't be needing them again. state.failures_cache.remove(std::iter::once(&room_key_info)) } Ok(false) => { // We did not find a valid backup decryption key or backup version, we did not // even attempt to download the room key. - state.downloaded_sessions.remove(std::iter::once(&room_key_info)); + state.downloaded_room_keys.remove(std::iter::once(&room_key_info)); } Err(_) => { - // We were unable to download the session. Update the failure cache so that we + // We were unable to download the room key. Update the failure cache so that we // back off from more requests, and also remove the entry from the list of - // sessions that we are downloading. - state.downloaded_sessions.remove(std::iter::once(&room_key_info)); + // room keys that we are downloading. + state.downloaded_room_keys.remove(std::iter::once(&room_key_info)); state.failures_cache.insert(room_key_info); } } @@ -293,13 +293,13 @@ struct BackupDownloadTaskListenerState { /// Map from event ID to download task active_tasks: BTreeMap>, - /// A list of megolm sessions that we have already downloaded, or are about - /// to download. + /// A list of room keys that we have already downloaded, or are about to + /// download. /// - /// The idea here is that once we've (successfully) downloaded a session + /// The idea here is that once we've (successfully) downloaded a room key /// from the backup, there's not much point trying again even if we get - /// another UTD event that uses the same session. - downloaded_sessions: DownloadCache, + /// another UTD event that uses the same room key. + downloaded_room_keys: DownloadCache, } impl BackupDownloadTaskListenerState { @@ -314,7 +314,7 @@ impl BackupDownloadTaskListenerState { client, failures_cache: FailuresCache::with_settings(Duration::from_secs(60 * 60 * 24), 60), active_tasks: Default::default(), - downloaded_sessions: DownloadCache::with_settings( + downloaded_room_keys: DownloadCache::with_settings( Duration::from_secs(60 * 60 * 24), 60, ), @@ -325,8 +325,8 @@ impl BackupDownloadTaskListenerState { /// /// Checks if: /// * we already have the key, - /// * we have already downloaded this session, or are about to do so, or - /// * we've backed off from trying to download this session. + /// * we have already downloaded this room key, or are about to do so, or + /// * we've backed off from trying to download this room key. /// /// If any of the above are true, returns `false`. Otherwise, returns /// `true`. @@ -364,22 +364,22 @@ impl BackupDownloadTaskListenerState { return false; } - // Check if we already downloaded this session, or another task is in the + // Check if we already downloaded this room key, or another task is in the // process of doing so. let room_key_info = download_request.to_room_key_info(); - if self.downloaded_sessions.contains(&room_key_info) { + if self.downloaded_room_keys.contains(&room_key_info) { debug!( ?download_request, - "Not performing backup download because this session has already been downloaded recently" + "Not performing backup download because this room key has already been downloaded recently" ); return false; }; - // Check if we're backing off from attempts to download this session + // Check if we're backing off from attempts to download this room key if self.failures_cache.contains(&room_key_info) { debug!( ?download_request, - "Not performing backup download because this session failed to download recently" + "Not performing backup download because this room key failed to download recently" ); return false; } @@ -450,7 +450,7 @@ mod test { { let state = state.lock().await; assert!( - !state.downloaded_sessions.contains(&(room_id.to_owned(), session_id.to_owned())), + !state.downloaded_room_keys.contains(&(room_id.to_owned(), session_id.to_owned())), "Backups are not enabled, we should not mark any room keys as downloaded." ) } From 5abff2970cd6a95ddcb12a5120ce03f1bb8be347 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Sep 2024 16:21:35 +0200 Subject: [PATCH 070/979] room: mark encryption state as missing if a room thinks it's not encrypted after requesting it --- crates/matrix-sdk/src/room/mod.rs | 19 +++++++++++ .../tests/integration/room/joined.rs | 34 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 6a389b1b57b..9e4a94de6c1 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1575,6 +1575,25 @@ impl Room { // could be quite useful if someone wants to enable encryption and // send a message right after it's enabled. _ = timeout(self.client.inner.sync_beat.listen(), SYNC_WAIT_TIME).await; + + // If after waiting for a sync, we don't have the encryption state we expect, + // assume the local encryption state is incorrect; this will cause + // the SDK to re-request it later for confirmation, instead of + // assuming it's sync'd and correct (and not encrypted). + let _sync_lock = self.client.base_client().sync_lock().lock().await; + if !self.inner.is_encrypted() { + debug!("still not marked as encrypted, marking encryption state as missing"); + + let mut room_info = self.clone_info(); + room_info.mark_encryption_state_missing(); + let mut changes = StateChanges::default(); + changes.add_room(room_info.clone()); + + self.client.store().save_changes(&changes).await?; + self.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); + } else { + debug!("room successfully marked as encrypted"); + } } Ok(()) diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index d15577cbe8e..96b240a8535 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -761,3 +761,37 @@ async fn test_make_reply_event_doesnt_require_event_cache() { // make_edit_event works, even if the event cache hasn't been enabled. room.make_edit_event(resp_event_id, EditedContent::RoomMessage(new_content)).await.unwrap(); } + +#[async_test] +async fn test_enable_encryption_doesnt_stay_unencrypted() { + let (client, server) = logged_in_client_with_server().await; + + mock_encryption_state(&server, false).await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$1"}))) + .mount(&server) + .await; + + let room_id = room_id!("!a:b.c"); + let room = mock_sync_with_new_room( + |builder| { + builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + }, + &client, + &server, + room_id, + ) + .await; + + assert!(!room.is_encrypted().await.unwrap()); + + room.enable_encryption().await.expect("enabling encryption should work"); + + server.reset().await; + mock_encryption_state(&server, true).await; + + assert!(room.is_encrypted().await.unwrap()); +} From 79f412790fe0931e931287f3b06cdce1aa82d6eb Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Sep 2024 18:36:12 +0200 Subject: [PATCH 071/979] timeline: stash edits around in case they arrive before the related event --- .../src/timeline/controller/state.rs | 22 +++++- .../src/timeline/event_handler.rs | 40 +++++++++- .../tests/integration/timeline/edit.rs | 74 +++++++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 84ae7c567a3..14f2a88a280 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -12,7 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::VecDeque, future::Future, sync::Arc}; +use std::{ + collections::{HashMap, VecDeque}, + future::Future, + sync::Arc, +}; use eyeball_im::{ObservableVector, ObservableVectorTransaction, ObservableVectorTransactionEntry}; use itertools::Itertools as _; @@ -21,9 +25,14 @@ use matrix_sdk_base::deserialized_responses::TimelineEvent; #[cfg(test)] use ruma::events::receipt::ReceiptEventContent; use ruma::{ - events::AnySyncEphemeralRoomEvent, push::Action, serde::Raw, EventId, - MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomVersionId, - UserId, + events::{ + relation::Replacement, room::message::RoomMessageEventContentWithoutRelation, + AnySyncEphemeralRoomEvent, + }, + push::Action, + serde::Raw, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, + RoomVersionId, UserId, }; use tracing::{debug, instrument, trace, warn}; @@ -727,7 +736,10 @@ pub(in crate::timeline) struct TimelineMetadata { pub all_events: VecDeque, pub reactions: Reactions, + pub pending_poll_events: PendingPollEvents, + pub pending_edits: HashMap>, + pub fully_read_event: Option, /// Whether we have a fully read-marker item in the timeline, that's up to @@ -755,6 +767,7 @@ impl TimelineMetadata { next_internal_id: Default::default(), reactions: Default::default(), pending_poll_events: Default::default(), + pending_edits: Default::default(), fully_read_event: Default::default(), // It doesn't make sense to set this to false until we fill the `fully_read_event` // field, otherwise we'll keep on exiting early in `Self::update_read_marker`. @@ -774,6 +787,7 @@ impl TimelineMetadata { self.all_events.clear(); self.reactions.clear(); self.pending_poll_events.clear(); + self.pending_edits.clear(); self.fully_read_event = None; // We forgot about the fully read marker right above, so wait for a new one // before attempting to update it for each new timeline item. diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 24caf55307b..01be9f76d3a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -453,7 +453,39 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: Replacement, ) { let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) else { - debug!("Timeline item not found, discarding edit"); + if let Flow::Remote { position, .. } = &self.ctx.flow { + match position { + TimelineItemPosition::Start { .. } => { + // Only insert the edit if there wasn't any other edit + // before. + if self.meta.pending_edits.get(&replacement.event_id).is_none() { + self.meta + .pending_edits + .insert(replacement.event_id.clone(), replacement); + debug!("Timeline item not found, stashing edit"); + } else { + debug!("Timeline item not found, but there was a previous edit for the event: discarding"); + } + } + + TimelineItemPosition::End { .. } => { + // This is a more recent edit: it's fine to overwrite the previous one, if + // available. + self.meta.pending_edits.insert(replacement.event_id.clone(), replacement); + debug!("Timeline item not found, stashing edit"); + } + + TimelineItemPosition::Update(_) => { + // This is not trivial: we don't really have any recency information about + // the edit. Maybe there was another edit that's more recent and could be + // decrypted, or maybe it's the opposite. Discard. + debug!("Timeline item not found, but discarding as we don't know the relative position of this edit event"); + } + } + } else { + debug!("Local edit for a timeline item not found, discarding"); + } + return; }; @@ -969,6 +1001,12 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } + if let Flow::Remote { event_id, .. } = &self.ctx.flow { + if let Some(edit) = self.meta.pending_edits.remove(event_id) { + self.handle_room_message_edit(edit); + } + } + // If we don't have a read marker item, look if we need to add one now. if !self.meta.has_up_to_date_read_marker_item { self.meta.update_read_marker(self.items); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index dd3992e44e7..f24f303f399 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -601,3 +601,77 @@ async fn test_send_edit_when_timeline_is_clear() { server.verify().await; } + +#[async_test] +async fn test_pending_edit() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let f = EventFactory::new(); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + // When I receive an edit event for an event I don't know about… + let original_event_id = event_id!("$edited"); + let edit_event_id = event_id!("$original"); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.text_msg("* hello") + .sender(&ALICE) + .event_id(edit_event_id) + .edit(original_event_id, RoomMessageEventContent::text_plain("[edit]").into()), + ), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // Nothing happens. + assert!(timeline_stream.next().now_or_never().is_none()); + + // But when I receive the original event after a bit… + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("hi").sender(&ALICE).event_id(&original_event_id)), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // Then I get the content as original first… + assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + let item = value.as_event().unwrap(); + assert!(item.event_id().is_some()); + assert!(!item.is_own()); + assert_eq!(item.content().as_message().unwrap().body(), "hi"); + + // And then the edit. + assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => { + let item = value.as_event().unwrap(); + assert!(item.event_id().is_some()); + assert!(!item.is_own()); + assert_eq!(item.content().as_message().unwrap().body(), "[edit]"); + }); + + // The day divider. + assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { + assert!(value.is_day_divider()); + }); + + // And nothing else. + assert!(timeline_stream.next().now_or_never().is_none()); +} From 8a2929fb519ee22f33f139e9b46d64d6433382e3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 5 Sep 2024 14:46:30 +0200 Subject: [PATCH 072/979] timeline: apply pending edits when adding the new item, not as a separate update --- .../src/timeline/event_handler.rs | 70 ++++++++++++------- .../tests/integration/timeline/edit.rs | 14 ++-- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 01be9f76d3a..f2b98ae3ccf 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -489,12 +489,36 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return; }; + let Some(new_item) = self.apply_msg_edit(&item, replacement) else { + return; + }; + + trace!("Applying edit"); + self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); + self.result.items_updated += 1; + } + + /// If there's a pending edit for an item, applies it immediately, returning + /// an updated [`EventTimelineItem`]. Otherwise, return the original event + /// item. + fn maybe_apply_pending_edit(&mut self, item: EventTimelineItem) -> EventTimelineItem { + let Flow::Remote { event_id, .. } = &self.ctx.flow else { return item }; + let Some(edit) = self.meta.pending_edits.remove(event_id) else { return item }; + self.apply_msg_edit(&item, edit).unwrap_or(item) + } + + /// Applies an edit to an existing [`EventTimelineItem`]. + fn apply_msg_edit( + &self, + item: &EventTimelineItem, + replacement: Replacement, + ) -> Option { if self.ctx.sender != item.sender() { info!( original_sender = ?item.sender(), edit_sender = ?self.ctx.sender, "Edit event applies to another user's timeline item, discarding" ); - return; + return None; } let TimelineItemContent::Message(msg) = item.content() else { @@ -502,7 +526,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { "Edit of message event applies to {:?}, discarding", item.content().debug_string(), ); - return; + return None; }; let mut msgtype = replacement.new_content.msgtype; @@ -532,9 +556,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } - trace!("Applying edit"); - self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); - self.result.items_updated += 1; + Some(new_item) } // Redacted reaction events are no-ops so don't need to be handled @@ -876,7 +898,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let is_room_encrypted = self.meta.is_room_encrypted; - let mut item = EventTimelineItem::new( + let mut event_item = EventTimelineItem::new( sender, sender_profile, timestamp, @@ -890,7 +912,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Local { .. } => { trace!("Adding new local timeline item"); - let item = self.meta.new_timeline_item(item); + let item = self.meta.new_timeline_item(event_item); self.items.push_back(item); } @@ -907,7 +929,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("Adding new remote timeline item at the start"); - let item = self.meta.new_timeline_item(item); + let event_item = self.maybe_apply_pending_edit(event_item); + let item = self.meta.new_timeline_item(event_item); self.items.push_front(item); } @@ -930,19 +953,19 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // normally, but with the sliding- sync proxy, it is actually very // common. // NOTE: SS proxy workaround. - trace!(?item, old_item = ?*old_item, "Received duplicate event"); + trace!(?event_item, old_item = ?*old_item, "Received duplicate event"); - if old_item.content.is_redacted() && !item.content.is_redacted() { + if old_item.content.is_redacted() && !event_item.content.is_redacted() { warn!("Got original form of an event that was previously redacted"); - item.content = item.content.redact(&self.meta.room_version); - item.reactions.clear(); + event_item.content = event_item.content.redact(&self.meta.room_version); + event_item.reactions.clear(); } } // TODO: Check whether anything is different about the // old and new item? - transfer_details(&mut item, &old_item); + transfer_details(&mut event_item, &old_item); let old_item_id = old_item.internal_id; @@ -950,7 +973,9 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // If the old item is the last one and no day divider // changes need to happen, replace and return early. trace!(idx, "Replacing existing event"); - self.items.set(idx, TimelineItem::new(item, old_item_id.to_owned())); + let old_item_id = old_item_id.to_owned(); + let event_item = self.maybe_apply_pending_edit(event_item); + self.items.set(idx, TimelineItem::new(event_item, old_item_id)); return; } @@ -976,12 +1001,13 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); trace!("Adding new remote timeline item after all non-pending events"); + let event_item = self.maybe_apply_pending_edit(event_item); let new_item = match removed_event_item_id { // If a previous version of the same item (usually a local // echo) was removed and we now need to add it again, reuse // the previous item's ID. - Some(id) => TimelineItem::new(item, id), - None => self.meta.new_timeline_item(item), + Some(id) => TimelineItem::new(event_item, id), + None => self.meta.new_timeline_item(event_item), }; // Keep push semantics, if we're inserting at the front or the back. @@ -996,14 +1022,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Remote { position: TimelineItemPosition::Update(idx), .. } => { trace!("Updating timeline item at position {idx}"); - let id = self.items[*idx].internal_id.clone(); - self.items.set(*idx, TimelineItem::new(item, id)); - } - } - - if let Flow::Remote { event_id, .. } = &self.ctx.flow { - if let Some(edit) = self.meta.pending_edits.remove(event_id) { - self.handle_room_message_edit(edit); + let idx = *idx; + let internal_id = self.items[idx].internal_id.clone(); + let event_item = self.maybe_apply_pending_edit(event_item); + self.items.set(idx, TimelineItem::new(event_item, internal_id)); } } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index f24f303f399..d4ec4249349 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -652,20 +652,14 @@ async fn test_pending_edit() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - // Then I get the content as original first… + // Then I get the edited content immediately. assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); let item = value.as_event().unwrap(); assert!(item.event_id().is_some()); assert!(!item.is_own()); - assert_eq!(item.content().as_message().unwrap().body(), "hi"); - - // And then the edit. - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => { - let item = value.as_event().unwrap(); - assert!(item.event_id().is_some()); - assert!(!item.is_own()); - assert_eq!(item.content().as_message().unwrap().body(), "[edit]"); - }); + let msg = item.content().as_message().unwrap(); + assert!(msg.is_edited()); + assert_eq!(msg.body(), "[edit]"); // The day divider. assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { From c9a46173b982c292b0ca180dc2d408e1f962555d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 5 Sep 2024 14:56:37 +0200 Subject: [PATCH 073/979] timeline: some renamings around poll edits --- crates/matrix-sdk-ui/src/timeline/event_handler.rs | 10 +++++----- crates/matrix-sdk-ui/src/timeline/polls.rs | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index f2b98ae3ccf..7650322cc04 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -352,7 +352,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::UnstablePollStart( UnstablePollStartEventContent::Replacement(c), - ) => self.handle_poll_start_edit(c.relates_to), + ) => self.handle_poll_edit(c.relates_to), AnyMessageLikeEventContent::UnstablePollStart( UnstablePollStartEventContent::New(c), @@ -639,7 +639,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } #[instrument(skip_all, fields(replacement_event_id = ?replacement.event_id))] - fn handle_poll_start_edit( + fn handle_poll_edit( &mut self, replacement: Replacement, ) { @@ -662,9 +662,9 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { }; let new_content = match poll_state.edit(&replacement.new_content) { - Ok(edited_poll_state) => TimelineItemContent::Poll(edited_poll_state), - Err(e) => { - info!("Failed to apply poll edit: {e:?}"); + Some(edited_poll_state) => TimelineItemContent::Poll(edited_poll_state), + None => { + info!("Not applying edit to a poll that's already ended"); return; } }; diff --git a/crates/matrix-sdk-ui/src/timeline/polls.rs b/crates/matrix-sdk-ui/src/timeline/polls.rs index 5f3eb867ed1..b878c8652ca 100644 --- a/crates/matrix-sdk-ui/src/timeline/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/polls.rs @@ -46,18 +46,20 @@ impl PollState { } } + /// Applies an edit to a poll, returns `None` if the poll was already marked + /// as finished. pub(super) fn edit( &self, replacement: &NewUnstablePollStartEventContentWithoutRelation, - ) -> Result { + ) -> Option { if self.end_event_timestamp.is_none() { let mut clone = self.clone(); clone.start_event_content.poll_start = replacement.poll_start.clone(); clone.start_event_content.text = replacement.text.clone(); clone.has_been_edited = true; - Ok(clone) + Some(clone) } else { - Err(()) + None } } From d2709c0679b97f6201db2615aac381ae6a485d53 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 5 Sep 2024 15:49:54 +0200 Subject: [PATCH 074/979] timeline: handle pending poll edits too --- .../src/timeline/controller/mod.rs | 2 +- .../src/timeline/controller/state.rs | 19 +- .../src/timeline/event_handler.rs | 170 +++++++++++------- crates/matrix-sdk-ui/src/timeline/polls.rs | 5 + .../tests/integration/timeline/edit.rs | 99 +++++++++- 5 files changed, 225 insertions(+), 70 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 069f486839e..2ce7bebecdd 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -52,7 +52,7 @@ use tracing::{ }; pub(super) use self::state::{ - EventMeta, FullEventMeta, TimelineEnd, TimelineMetadata, TimelineState, + EventMeta, FullEventMeta, PendingEdit, TimelineEnd, TimelineMetadata, TimelineState, TimelineStateTransaction, }; use super::{ diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 14f2a88a280..17bffec826b 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -26,6 +26,7 @@ use matrix_sdk_base::deserialized_responses::TimelineEvent; use ruma::events::receipt::ReceiptEventContent; use ruma::{ events::{ + poll::unstable_start::NewUnstablePollStartEventContentWithoutRelation, relation::Replacement, room::message::RoomMessageEventContentWithoutRelation, AnySyncEphemeralRoomEvent, }, @@ -702,6 +703,22 @@ impl TimelineStateTransaction<'_> { } } +#[derive(Clone)] +pub(in crate::timeline) enum PendingEdit { + RoomMessage(Replacement), + Poll(Replacement), +} + +#[cfg(not(tarpaulin_include))] +impl std::fmt::Debug for PendingEdit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RoomMessage(_) => f.debug_struct("RoomMessage").finish_non_exhaustive(), + Self::Poll(_) => f.debug_struct("Poll").finish_non_exhaustive(), + } + } +} + #[derive(Clone, Debug)] pub(in crate::timeline) struct TimelineMetadata { // **** CONSTANT FIELDS **** @@ -738,7 +755,7 @@ pub(in crate::timeline) struct TimelineMetadata { pub reactions: Reactions, pub pending_poll_events: PendingPollEvents, - pub pending_edits: HashMap>, + pub pending_edits: HashMap, pub fully_read_event: Option, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 7650322cc04..225d8aac16f 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; +use std::{collections::hash_map::Entry, sync::Arc}; +use as_variant::as_variant; use eyeball_im::{ObservableVectorTransaction, ObservableVectorTransactionEntry}; use indexmap::IndexMap; use matrix_sdk::{ @@ -63,6 +64,7 @@ use super::{ use crate::{ events::SyncTimelineEventWithoutContent, timeline::{ + controller::PendingEdit, event_item::{ReactionInfo, ReactionStatus}, reactions::PendingReaction, }, @@ -454,36 +456,11 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { ) { let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) else { if let Flow::Remote { position, .. } = &self.ctx.flow { - match position { - TimelineItemPosition::Start { .. } => { - // Only insert the edit if there wasn't any other edit - // before. - if self.meta.pending_edits.get(&replacement.event_id).is_none() { - self.meta - .pending_edits - .insert(replacement.event_id.clone(), replacement); - debug!("Timeline item not found, stashing edit"); - } else { - debug!("Timeline item not found, but there was a previous edit for the event: discarding"); - } - } - - TimelineItemPosition::End { .. } => { - // This is a more recent edit: it's fine to overwrite the previous one, if - // available. - self.meta.pending_edits.insert(replacement.event_id.clone(), replacement); - debug!("Timeline item not found, stashing edit"); - } - - TimelineItemPosition::Update(_) => { - // This is not trivial: we don't really have any recency information about - // the edit. Maybe there was another edit that's more recent and could be - // decrypted, or maybe it's the opposite. Discard. - debug!("Timeline item not found, but discarding as we don't know the relative position of this edit event"); - } - } + let replaced_event_id = replacement.event_id.clone(); + let replacement = PendingEdit::RoomMessage(replacement); + self.stash_pending_edit(*position, replaced_event_id, replacement); } else { - debug!("Local edit for a timeline item not found, discarding"); + debug!("Local message edit for a timeline item not found, discarding"); } return; @@ -498,13 +475,62 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { self.result.items_updated += 1; } + /// Try to stash a pending edit, if it makes sense to do so. + fn stash_pending_edit( + &mut self, + position: TimelineItemPosition, + replaced_event_id: OwnedEventId, + replacement: PendingEdit, + ) { + match position { + TimelineItemPosition::Start { .. } | TimelineItemPosition::Update(_) => { + // Only insert the edit if there wasn't any other edit + // before. + // + // For a start position, this is the right thing to do, because if there was a + // stashed edit, it relates to a more recent one (either appended for a live + // sync, or inserted earlier via back-pagination). + // + // For an update position, if there was a stashed edit, we can't really know + // which version is the more recent, without an ordering of the + // edit events themselves, so we discard it in that case. + if let Entry::Vacant(entry) = self.meta.pending_edits.entry(replaced_event_id) { + entry.insert(replacement); + debug!("Timeline item not found, stashing edit"); + } else { + debug!("Timeline item not found, but there was a previous edit for the event: discarding"); + } + } + + TimelineItemPosition::End { .. } => { + // This is a more recent edit, coming either live from sync or from a + // forward-pagination: it's fine to overwrite the previous one, if + // available. + self.meta.pending_edits.insert(replaced_event_id, replacement); + debug!("Timeline item not found, stashing edit"); + } + } + } + /// If there's a pending edit for an item, applies it immediately, returning /// an updated [`EventTimelineItem`]. Otherwise, return the original event /// item. - fn maybe_apply_pending_edit(&mut self, item: EventTimelineItem) -> EventTimelineItem { - let Flow::Remote { event_id, .. } = &self.ctx.flow else { return item }; - let Some(edit) = self.meta.pending_edits.remove(event_id) else { return item }; - self.apply_msg_edit(&item, edit).unwrap_or(item) + fn maybe_apply_pending_edit(&mut self, item: &EventTimelineItem) -> Option { + let Flow::Remote { event_id, .. } = &self.ctx.flow else { return None }; + + if matches!(item.content(), TimelineItemContent::Message(..)) { + let pending = self.meta.pending_edits.remove(event_id)?; + let edit = as_variant!(pending, PendingEdit::RoomMessage)?; + return self.apply_msg_edit(item, edit); + } + + if matches!(item.content(), TimelineItemContent::Poll(..)) { + let pending = self.meta.pending_edits.remove(event_id)?; + let edit = as_variant!(pending, PendingEdit::Poll)?; + return self.apply_poll_edit(item, edit); + } + + None } /// Applies an edit to an existing [`EventTimelineItem`]. @@ -644,28 +670,48 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: Replacement, ) { let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) else { - debug!("Timeline item not found, discarding poll edit"); + if let Flow::Remote { position, .. } = &self.ctx.flow { + let replaced_event_id = replacement.event_id.clone(); + let replacement = PendingEdit::Poll(replacement); + self.stash_pending_edit(*position, replaced_event_id, replacement); + } else { + debug!("Local poll edit for a timeline item not found, discarding"); + } + return; + }; + + let Some(new_item) = self.apply_poll_edit(item.inner, replacement) else { return; }; + trace!("Applying poll start edit."); + self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); + self.result.items_updated += 1; + } + + fn apply_poll_edit( + &self, + item: &EventTimelineItem, + replacement: Replacement, + ) -> Option { if self.ctx.sender != item.sender() { info!( original_sender = ?item.sender(), edit_sender = ?self.ctx.sender, "Edit event applies to another user's timeline item, discarding" ); - return; + return None; } let TimelineItemContent::Poll(poll_state) = &item.content() else { info!("Edit of poll event applies to {}, discarding", item.content().debug_string(),); - return; + return None; }; let new_content = match poll_state.edit(&replacement.new_content) { Some(edited_poll_state) => TimelineItemContent::Poll(edited_poll_state), None => { info!("Not applying edit to a poll that's already ended"); - return; + return None; } }; @@ -674,19 +720,13 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Remote { raw_event, .. } => Some(raw_event.clone()), }; - trace!("Applying poll start edit."); - self.items.set( - item_pos, - TimelineItem::new( - item.with_content(new_content, edit_json), - item.internal_id.to_owned(), - ), - ); - self.result.items_updated += 1; + Some(item.with_content(new_content, edit_json)) } + /// Adds a new poll to the timeline. fn handle_poll_start(&mut self, c: NewUnstablePollStartEventContent, should_add: bool) { let mut poll_state = PollState::new(c); + if let Flow::Remote { event_id, .. } = &self.ctx.flow { // Applying the cache to remote events only because local echoes // don't have an event ID that could be referenced by responses yet. @@ -898,7 +938,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let is_room_encrypted = self.meta.is_room_encrypted; - let mut event_item = EventTimelineItem::new( + let mut item = EventTimelineItem::new( sender, sender_profile, timestamp, @@ -908,11 +948,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { is_room_encrypted, ); + if let Some(edited_item) = self.maybe_apply_pending_edit(&item) { + item = edited_item; + } + match &self.ctx.flow { Flow::Local { .. } => { trace!("Adding new local timeline item"); - let item = self.meta.new_timeline_item(event_item); + let item = self.meta.new_timeline_item(item); self.items.push_back(item); } @@ -929,8 +973,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("Adding new remote timeline item at the start"); - let event_item = self.maybe_apply_pending_edit(event_item); - let item = self.meta.new_timeline_item(event_item); + let item = self.meta.new_timeline_item(item); self.items.push_front(item); } @@ -953,19 +996,19 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // normally, but with the sliding- sync proxy, it is actually very // common. // NOTE: SS proxy workaround. - trace!(?event_item, old_item = ?*old_item, "Received duplicate event"); + trace!(?item, old_item = ?*old_item, "Received duplicate event"); - if old_item.content.is_redacted() && !event_item.content.is_redacted() { + if old_item.content.is_redacted() && !item.content.is_redacted() { warn!("Got original form of an event that was previously redacted"); - event_item.content = event_item.content.redact(&self.meta.room_version); - event_item.reactions.clear(); + item.content = item.content.redact(&self.meta.room_version); + item.reactions.clear(); } } // TODO: Check whether anything is different about the // old and new item? - transfer_details(&mut event_item, &old_item); + transfer_details(&mut item, &old_item); let old_item_id = old_item.internal_id; @@ -973,9 +1016,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // If the old item is the last one and no day divider // changes need to happen, replace and return early. trace!(idx, "Replacing existing event"); - let old_item_id = old_item_id.to_owned(); - let event_item = self.maybe_apply_pending_edit(event_item); - self.items.set(idx, TimelineItem::new(event_item, old_item_id)); + self.items.set(idx, TimelineItem::new(item, old_item_id.to_owned())); return; } @@ -1001,13 +1042,12 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); trace!("Adding new remote timeline item after all non-pending events"); - let event_item = self.maybe_apply_pending_edit(event_item); let new_item = match removed_event_item_id { // If a previous version of the same item (usually a local // echo) was removed and we now need to add it again, reuse // the previous item's ID. - Some(id) => TimelineItem::new(event_item, id), - None => self.meta.new_timeline_item(event_item), + Some(id) => TimelineItem::new(item, id), + None => self.meta.new_timeline_item(item), }; // Keep push semantics, if we're inserting at the front or the back. @@ -1022,10 +1062,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Remote { position: TimelineItemPosition::Update(idx), .. } => { trace!("Updating timeline item at position {idx}"); - let idx = *idx; - let internal_id = self.items[idx].internal_id.clone(); - let event_item = self.maybe_apply_pending_edit(event_item); - self.items.set(idx, TimelineItem::new(event_item, internal_id)); + let internal_id = self.items[*idx].internal_id.clone(); + self.items.set(*idx, TimelineItem::new(item, internal_id)); } } diff --git a/crates/matrix-sdk-ui/src/timeline/polls.rs b/crates/matrix-sdk-ui/src/timeline/polls.rs index b878c8652ca..19d8ec45e03 100644 --- a/crates/matrix-sdk-ui/src/timeline/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/polls.rs @@ -125,6 +125,11 @@ impl PollState { has_been_edited: self.has_been_edited, } } + + /// Returns true whether this poll has been edited. + pub fn is_edit(&self) -> bool { + self.has_been_edited + } } impl From for NewUnstablePollStartEventContent { diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index d4ec4249349..ff91f320afb 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -14,6 +14,7 @@ use std::time::Duration; +use as_variant::as_variant; use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; @@ -30,8 +31,8 @@ use ruma::{ event_id, events::{ poll::unstable_start::{ - NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers, - UnstablePollStartContentBlock, + NewUnstablePollStartEventContent, ReplacementUnstablePollStartEventContent, + UnstablePollAnswer, UnstablePollAnswers, UnstablePollStartContentBlock, }, room::message::{ MessageType, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, @@ -669,3 +670,97 @@ async fn test_pending_edit() { // And nothing else. assert!(timeline_stream.next().now_or_never().is_none()); } + +#[async_test] +async fn test_pending_poll_edit() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let f = EventFactory::new(); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + // When I receive an edit event for an event I don't know about… + let original_event_id = event_id!("$edited"); + let edit_event_id = event_id!("$original"); + + let new_start = UnstablePollStartContentBlock::new( + "Edited poll", + UnstablePollAnswers::try_from(vec![ + UnstablePollAnswer::new("0", "Yes"), + UnstablePollAnswer::new("1", "No"), + ]) + .unwrap(), + ); + + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.event(ReplacementUnstablePollStartEventContent::new( + new_start, + original_event_id.to_owned(), + )) + .sender(&ALICE) + .event_id(edit_event_id), + ), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // Nothing happens. + assert!(timeline_stream.next().now_or_never().is_none()); + + // But when I receive the original event after a bit… + let event_content = NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new( + "Original poll", + UnstablePollAnswers::try_from(vec![ + UnstablePollAnswer::new("0", "f yeah"), + UnstablePollAnswer::new("1", "noooope"), + ]) + .unwrap(), + )); + + sync_builder + .add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + f.event(event_content).sender(&ALICE).event_id(&original_event_id), + )); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // Then I get the edited content immediately. + assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + let item = value.as_event().unwrap(); + assert!(item.event_id().is_some()); + assert!(!item.is_own()); + + let poll = as_variant!(item.content(), TimelineItemContent::Poll).unwrap(); + assert!(poll.is_edit()); + + let results = poll.results(); + assert_eq!(results.question, "Edited poll"); + assert_eq!(results.answers[0].text, "Yes"); + assert_eq!(results.answers[1].text, "No"); + + // The day divider. + assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { + assert!(value.is_day_divider()); + }); + + // And nothing else. + assert!(timeline_stream.next().now_or_never().is_none()); +} From 40c1e8a2da578e4d888bb243f3d75fd4e9b98681 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 5 Sep 2024 16:15:47 +0200 Subject: [PATCH 075/979] timeline: add more tests for pending edits --- .../tests/integration/timeline/edit.rs | 350 ++++++++++++++---- 1 file changed, 269 insertions(+), 81 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index ff91f320afb..dc62b3e5823 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -22,11 +22,15 @@ use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ config::SyncSettings, test_utils::{events::EventFactory, logged_in_client_with_server}, + Client, }; use matrix_sdk_test::{ async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, }; -use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineDetails, TimelineItemContent}; +use matrix_sdk_ui::{ + timeline::{EventSendState, RoomExt, TimelineDetails, TimelineItemContent}, + Timeline, +}; use ruma::{ event_id, events::{ @@ -38,15 +42,18 @@ use ruma::{ MessageType, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, TextMessageEventContent, }, + AnyTimelineEvent, }, room_id, + serde::Raw, + OwnedRoomId, }; use serde_json::json; use stream_assert::assert_next_matches; use tokio::{task::yield_now, time::sleep}; use wiremock::{ matchers::{header, method, path_regex}, - Mock, ResponseTemplate, + Mock, MockServer, ResponseTemplate, }; use crate::mock_sync; @@ -356,14 +363,12 @@ async fn test_send_reply_edit() { timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; let f = EventFactory::new(); - let fst_event_id = event_id!("$original_event"); + let event_id = event_id!("$original_event"); sync_builder.add_joined_room( JoinedRoomBuilder::new(room_id) - .add_timeline_event(f.text_msg("Hello, World!").sender(&ALICE).event_id(fst_event_id)) + .add_timeline_event(f.text_msg("Hello, World!").sender(&ALICE).event_id(event_id)) .add_timeline_event( - f.text_msg("Hello, Alice!") - .reply_to(fst_event_id) - .sender(client.user_id().unwrap()), + f.text_msg("Hello, Alice!").reply_to(event_id).sender(client.user_id().unwrap()), ), ); @@ -380,7 +385,7 @@ async fn test_send_reply_edit() { assert!(!reply_message.is_edited()); assert!(reply_item.is_editable()); let in_reply_to = reply_message.in_reply_to().unwrap(); - assert_eq!(in_reply_to.event_id, fst_event_id); + assert_eq!(in_reply_to.event_id, event_id); assert_matches!(in_reply_to.event, TimelineDetails::Ready(_)); mock_encryption_state(&server, false).await; @@ -411,7 +416,7 @@ async fn test_send_reply_edit() { assert_eq!(edit_message.body(), "Hello, Room!"); assert!(edit_message.is_edited()); let in_reply_to = reply_message.in_reply_to().unwrap(); - assert_eq!(in_reply_to.event_id, fst_event_id); + assert_eq!(in_reply_to.event_id, event_id); assert_matches!(in_reply_to.event, TimelineDetails::Ready(_)); // The response to the mocked endpoint does not generate further timeline @@ -603,62 +608,108 @@ async fn test_send_edit_when_timeline_is_clear() { server.verify().await; } +struct PendingEditHelper { + client: Client, + server: MockServer, + timeline: Timeline, + sync_builder: SyncResponseBuilder, + sync_settings: SyncSettings, + room_id: OwnedRoomId, +} + +impl PendingEditHelper { + async fn new() -> Self { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + { + // Fill the initial prev-batch token to avoid waiting for it later. + let ec = client.event_cache(); + ec.subscribe().unwrap(); + ec.add_initial_events(room_id, vec![], Some("prev-batch-token".to_owned())) + .await + .unwrap(); + } + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + + Self { client, server, timeline, sync_builder, sync_settings, room_id: room_id.to_owned() } + } + + async fn handle_sync(&mut self, joined_room_builder: JoinedRoomBuilder) { + self.sync_builder.add_joined_room(joined_room_builder); + + mock_sync(&self.server, self.sync_builder.build_json_sync_response(), None).await; + let _response = self.client.sync_once(self.sync_settings.clone()).await.unwrap(); + + self.server.reset().await; + } + + async fn handle_backpagination(&mut self, events: Vec>, batch_size: u16) { + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "start": "123", + "end": "yolo", + "chunk": events, + "state": [] + }))) + .expect(1) + .mount(&self.server) + .await; + + self.timeline.live_paginate_backwards(batch_size).await.unwrap(); + + self.server.reset().await; + } +} + #[async_test] async fn test_pending_edit() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - + let mut h = PendingEditHelper::new().await; let f = EventFactory::new(); - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - let (_, mut timeline_stream) = timeline.subscribe().await; + let (_, mut timeline_stream) = h.timeline.subscribe().await; // When I receive an edit event for an event I don't know about… - let original_event_id = event_id!("$edited"); - let edit_event_id = event_id!("$original"); - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event( + let original_event_id = event_id!("$original"); + let edit_event_id = event_id!("$edit"); + + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id).add_timeline_event( f.text_msg("* hello") .sender(&ALICE) .event_id(edit_event_id) .edit(original_event_id, RoomMessageEventContent::text_plain("[edit]").into()), ), - ); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + ) + .await; // Nothing happens. assert!(timeline_stream.next().now_or_never().is_none()); // But when I receive the original event after a bit… - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id) - .add_timeline_event(f.text_msg("hi").sender(&ALICE).event_id(&original_event_id)), - ); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id) + .add_timeline_event(f.text_msg("hi").sender(&ALICE).event_id(original_event_id)), + ) + .await; // Then I get the edited content immediately. assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); - let item = value.as_event().unwrap(); - assert!(item.event_id().is_some()); - assert!(!item.is_own()); - let msg = item.content().as_message().unwrap(); + let msg = value.as_event().unwrap().content().as_message().unwrap(); assert!(msg.is_edited()); assert_eq!(msg.body(), "[edit]"); @@ -672,29 +723,176 @@ async fn test_pending_edit() { } #[async_test] -async fn test_pending_poll_edit() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); +async fn test_pending_edit_overrides() { + let mut h = PendingEditHelper::new().await; + let f = EventFactory::new(); + + let (_, mut timeline_stream) = h.timeline.subscribe().await; + + // When I receive multiple edit events for an event I don't know about… + let original_event_id = event_id!("$original"); + let edit_event_id = event_id!("$edit"); + let edit_event_id2 = event_id!("$edit2"); + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id) + .add_timeline_event( + f.text_msg("* hello") + .sender(&ALICE) + .event_id(edit_event_id) + .edit(original_event_id, RoomMessageEventContent::text_plain("hello").into()), + ) + .add_timeline_event( + f.text_msg("* bonjour") + .sender(&ALICE) + .event_id(edit_event_id2) + .edit(original_event_id, RoomMessageEventContent::text_plain("bonjour").into()), + ), + ) + .await; + + // Nothing happens. + assert!(timeline_stream.next().now_or_never().is_none()); + + // And then I receive the original event after a bit… + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id) + .add_timeline_event(f.text_msg("hi").sender(&ALICE).event_id(original_event_id)), + ) + .await; + + // Then I get the latest edited content immediately. + assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + let msg = value.as_event().unwrap().content().as_message().unwrap(); + assert!(msg.is_edited()); + assert_eq!(msg.body(), "bonjour"); + // The day divider. + assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { + assert!(value.is_day_divider()); + }); + + // And nothing else. + assert!(timeline_stream.next().now_or_never().is_none()); +} + +#[async_test] +async fn test_pending_edit_from_backpagination() { + let mut h = PendingEditHelper::new().await; let f = EventFactory::new(); - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + let (_, mut timeline_stream) = h.timeline.subscribe().await; - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + // When I receive an edit from a back-pagination for an event I don't know + // about… + let original_event_id = event_id!("$original"); + let edit_event_id = event_id!("$edit"); + h.handle_backpagination( + vec![f + .text_msg("* hello") + .sender(&ALICE) + .event_id(edit_event_id) + .room(&h.room_id) + .edit(original_event_id, RoomMessageEventContent::text_plain("hello").into()) + .into()], + 10, + ) + .await; - mock_encryption_state(&server, false).await; + // Nothing happens. + assert!(timeline_stream.next().now_or_never().is_none()); - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - let (_, mut timeline_stream) = timeline.subscribe().await; + // And then I receive the original event after a bit… + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id) + .add_timeline_event(f.text_msg("hi").sender(&ALICE).event_id(original_event_id)), + ) + .await; + + // Then I get the latest edited content immediately. + assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + let msg = value.as_event().unwrap().content().as_message().unwrap(); + assert!(msg.is_edited()); + assert_eq!(msg.body(), "hello"); + + // The day divider. + assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { + assert!(value.is_day_divider()); + }); + + // And nothing else. + assert!(timeline_stream.next().now_or_never().is_none()); +} + +#[async_test] +async fn test_pending_edit_from_backpagination_doesnt_override_pending_edit_from_sync() { + let mut h = PendingEditHelper::new().await; + let f = EventFactory::new(); + + let (_, mut timeline_stream) = h.timeline.subscribe().await; + + // When I receive an edit live from a sync for an event I don't know about… + let original_event_id = event_id!("$original"); + let edit_event_id = event_id!("$edit"); + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id).add_timeline_event( + f.text_msg("* hello") + .sender(&ALICE) + .event_id(edit_event_id) + .edit(original_event_id, RoomMessageEventContent::text_plain("[edit]").into()), + ), + ) + .await; + + // And then I receive an edit from a back-pagination for the same event… + let edit_event_id2 = event_id!("$edit2"); + h.handle_backpagination( + vec![f + .text_msg("* aloha") + .sender(&ALICE) + .event_id(edit_event_id2) + .room(&h.room_id) + .edit(original_event_id, RoomMessageEventContent::text_plain("aloha").into()) + .into()], + 10, + ) + .await; + + // Nothing happens. + assert!(timeline_stream.next().now_or_never().is_none()); + + // And then I receive the original event after a bit… + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id) + .add_timeline_event(f.text_msg("hi").sender(&ALICE).event_id(original_event_id)), + ) + .await; + + // Then I get the edit from the sync, even if the back-pagination happened + // after. + assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + let msg = value.as_event().unwrap().content().as_message().unwrap(); + assert!(msg.is_edited()); + assert_eq!(msg.body(), "[edit]"); + + // The day divider. + assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { + assert!(value.is_day_divider()); + }); + + // And nothing else. + assert!(timeline_stream.next().now_or_never().is_none()); +} + +#[async_test] +async fn test_pending_poll_edit() { + let mut h = PendingEditHelper::new().await; + let f = EventFactory::new(); + + let (_, mut timeline_stream) = h.timeline.subscribe().await; // When I receive an edit event for an event I don't know about… - let original_event_id = event_id!("$edited"); - let edit_event_id = event_id!("$original"); + let original_event_id = event_id!("$original"); + let edit_event_id = event_id!("$edit"); let new_start = UnstablePollStartContentBlock::new( "Edited poll", @@ -705,8 +903,8 @@ async fn test_pending_poll_edit() { .unwrap(), ); - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event( + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id).add_timeline_event( f.event(ReplacementUnstablePollStartEventContent::new( new_start, original_event_id.to_owned(), @@ -714,11 +912,8 @@ async fn test_pending_poll_edit() { .sender(&ALICE) .event_id(edit_event_id), ), - ); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + ) + .await; // Nothing happens. assert!(timeline_stream.next().now_or_never().is_none()); @@ -733,22 +928,15 @@ async fn test_pending_poll_edit() { .unwrap(), )); - sync_builder - .add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( - f.event(event_content).sender(&ALICE).event_id(&original_event_id), - )); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + h.handle_sync( + JoinedRoomBuilder::new(&h.room_id) + .add_timeline_event(f.event(event_content).sender(&ALICE).event_id(original_event_id)), + ) + .await; // Then I get the edited content immediately. assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); - let item = value.as_event().unwrap(); - assert!(item.event_id().is_some()); - assert!(!item.is_own()); - - let poll = as_variant!(item.content(), TimelineItemContent::Poll).unwrap(); + let poll = as_variant!(value.as_event().unwrap().content(), TimelineItemContent::Poll).unwrap(); assert!(poll.is_edit()); let results = poll.results(); From c66ea8162c90f8d22e5c39f19527ca4831547f82 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Sep 2024 14:55:07 +0200 Subject: [PATCH 076/979] timeline: use fewer early returns in code around pending edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 😥 --- .../src/timeline/event_handler.rs | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 225d8aac16f..123aea71ce9 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -454,25 +454,19 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &mut self, replacement: Replacement, ) { - let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) else { - if let Flow::Remote { position, .. } = &self.ctx.flow { - let replaced_event_id = replacement.event_id.clone(); - let replacement = PendingEdit::RoomMessage(replacement); - self.stash_pending_edit(*position, replaced_event_id, replacement); - } else { - debug!("Local message edit for a timeline item not found, discarding"); + if let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) { + if let Some(new_item) = self.apply_msg_edit(&item, replacement) { + trace!("Applied edit"); + self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); + self.result.items_updated += 1; } - - return; - }; - - let Some(new_item) = self.apply_msg_edit(&item, replacement) else { - return; - }; - - trace!("Applying edit"); - self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); - self.result.items_updated += 1; + } else if let Flow::Remote { position, .. } = &self.ctx.flow { + let replaced_event_id = replacement.event_id.clone(); + let replacement = PendingEdit::RoomMessage(replacement); + self.stash_pending_edit(*position, replaced_event_id, replacement); + } else { + debug!("Local message edit for a timeline item not found, discarding"); + } } /// Try to stash a pending edit, if it makes sense to do so. @@ -512,28 +506,35 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } - /// If there's a pending edit for an item, applies it immediately, returning - /// an updated [`EventTimelineItem`]. Otherwise, return the original event - /// item. - fn maybe_apply_pending_edit(&mut self, item: &EventTimelineItem) -> Option { - let Flow::Remote { event_id, .. } = &self.ctx.flow else { return None }; - - if matches!(item.content(), TimelineItemContent::Message(..)) { - let pending = self.meta.pending_edits.remove(event_id)?; - let edit = as_variant!(pending, PendingEdit::RoomMessage)?; - return self.apply_msg_edit(item, edit); - } - - if matches!(item.content(), TimelineItemContent::Poll(..)) { - let pending = self.meta.pending_edits.remove(event_id)?; - let edit = as_variant!(pending, PendingEdit::Poll)?; - return self.apply_poll_edit(item, edit); + /// If there's a pending edit for an item, apply it immediately, returning + /// an updated [`EventTimelineItem`]. Otherwise, return `None`. + fn maybe_unstash_pending_edit( + &mut self, + item: &EventTimelineItem, + ) -> Option { + if let Flow::Remote { event_id, .. } = &self.ctx.flow { + match item.content() { + TimelineItemContent::Message(..) => { + let pending = self.meta.pending_edits.remove(event_id)?; + let edit = as_variant!(pending, PendingEdit::RoomMessage)?; + self.apply_msg_edit(item, edit) + } + TimelineItemContent::Poll(..) => { + let pending = self.meta.pending_edits.remove(event_id)?; + let edit = as_variant!(pending, PendingEdit::Poll)?; + self.apply_poll_edit(item, edit) + } + _ => None, + } + } else { + None } - - None } - /// Applies an edit to an existing [`EventTimelineItem`]. + /// Try applying an edit to an existing [`EventTimelineItem`]. + /// + /// Return a new item if applying the edit succeeded, or `None` if there was + /// an error while applying it. fn apply_msg_edit( &self, item: &EventTimelineItem, @@ -556,6 +557,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { }; let mut msgtype = replacement.new_content.msgtype; + // Edit's content is never supposed to contain the reply fallback. msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); @@ -948,7 +950,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { is_room_encrypted, ); - if let Some(edited_item) = self.maybe_apply_pending_edit(&item) { + if let Some(edited_item) = self.maybe_unstash_pending_edit(&item) { item = edited_item; } From d005311235c5fc94ce6bea285ba9228559282c0f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Sep 2024 15:07:56 +0200 Subject: [PATCH 077/979] timeline: add comments for each item in `TimelineMetadata` --- .../src/timeline/controller/state.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 17bffec826b..2d30f9d624c 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -733,6 +733,11 @@ pub(in crate::timeline) struct TimelineMetadata { /// This value is constant over the lifetime of the metadata. pub(crate) unable_to_decrypt_hook: Option>, + /// A boolean indicating whether the room the timeline is attached to is + /// actually encrypted or not. + /// TODO: this is misplaced, it should be part of the room provider as this + /// value can change over time when a room switches from non-encrypted + /// to encrypted, see also #3850. pub(crate) is_room_encrypted: bool, /// Matrix room version of the timeline's room, or a sensible default. @@ -752,11 +757,18 @@ pub(in crate::timeline) struct TimelineMetadata { /// are discarded in the timeline items. pub all_events: VecDeque, + /// State helping matching reactions to their associated events, and + /// stashing pending reactions. pub reactions: Reactions, + /// Associated poll events received before their original poll start event. pub pending_poll_events: PendingPollEvents, + + /// Edit events received before the related event they're editing. pub pending_edits: HashMap, + /// Identifier of the fully-read event, helping knowing where to introduce + /// the read marker. pub fully_read_event: Option, /// Whether we have a fully read-marker item in the timeline, that's up to @@ -767,6 +779,9 @@ pub(in crate::timeline) struct TimelineMetadata { /// - The fully-read marker item would be the last item in the timeline. pub has_up_to_date_read_marker_item: bool, + /// Read receipts related state. + /// + /// TODO: move this over to the event cache (see also #3058). pub read_receipts: ReadReceipts, } From ef6237045e999f15a6cf652296e69c0ac6bc33d3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Sep 2024 16:00:06 +0200 Subject: [PATCH 078/979] timeline: use a RingBuffer instead of a hashmap to put an upper bound on the number of pending edits --- .../src/timeline/controller/state.rs | 18 +++++++----- .../src/timeline/event_handler.rs | 29 +++++++++++++++---- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 2d30f9d624c..fb81dd4dcea 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -12,15 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::{HashMap, VecDeque}, - future::Future, - sync::Arc, -}; +use std::{collections::VecDeque, future::Future, num::NonZeroUsize, sync::Arc}; use eyeball_im::{ObservableVector, ObservableVectorTransaction, ObservableVectorTransactionEntry}; use itertools::Itertools as _; -use matrix_sdk::{deserialized_responses::SyncTimelineEvent, send_queue::SendHandle}; +use matrix_sdk::{ + deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer, send_queue::SendHandle, +}; use matrix_sdk_base::deserialized_responses::TimelineEvent; #[cfg(test)] use ruma::events::receipt::ReceiptEventContent; @@ -765,7 +763,7 @@ pub(in crate::timeline) struct TimelineMetadata { pub pending_poll_events: PendingPollEvents, /// Edit events received before the related event they're editing. - pub pending_edits: HashMap, + pub pending_edits: RingBuffer<(OwnedEventId, PendingEdit)>, /// Identifier of the fully-read event, helping knowing where to introduce /// the read marker. @@ -785,6 +783,10 @@ pub(in crate::timeline) struct TimelineMetadata { pub read_receipts: ReadReceipts, } +/// Maximum number of stash pending edits. +/// SAFETY: 32 is not 0. +const MAX_NUM_STASHED_PENDING_EDITS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(32) }; + impl TimelineMetadata { pub(crate) fn new( own_user_id: OwnedUserId, @@ -799,7 +801,7 @@ impl TimelineMetadata { next_internal_id: Default::default(), reactions: Default::default(), pending_poll_events: Default::default(), - pending_edits: Default::default(), + pending_edits: RingBuffer::new(MAX_NUM_STASHED_PENDING_EDITS), fully_read_event: Default::default(), // It doesn't make sense to set this to false until we fill the `fully_read_event` // field, otherwise we'll keep on exiting early in `Self::update_read_marker`. diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 123aea71ce9..40e35e8b5da 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::hash_map::Entry, sync::Arc}; +use std::sync::Arc; use as_variant::as_variant; use eyeball_im::{ObservableVectorTransaction, ObservableVectorTransactionEntry}; @@ -488,8 +488,13 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // For an update position, if there was a stashed edit, we can't really know // which version is the more recent, without an ordering of the // edit events themselves, so we discard it in that case. - if let Entry::Vacant(entry) = self.meta.pending_edits.entry(replaced_event_id) { - entry.insert(replacement); + if !self + .meta + .pending_edits + .iter() + .any(|(event_id, _)| *event_id == replaced_event_id) + { + self.meta.pending_edits.push((replaced_event_id, replacement)); debug!("Timeline item not found, stashing edit"); } else { debug!("Timeline item not found, but there was a previous edit for the event: discarding"); @@ -500,7 +505,13 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // This is a more recent edit, coming either live from sync or from a // forward-pagination: it's fine to overwrite the previous one, if // available. - self.meta.pending_edits.insert(replaced_event_id, replacement); + let edits = &mut self.meta.pending_edits; + if let Some(pos) = + edits.iter().position(|(event_id, _)| *event_id == replaced_event_id) + { + edits.remove(pos); + } + edits.push((replaced_event_id, replacement)); debug!("Timeline item not found, stashing edit"); } } @@ -512,15 +523,21 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &mut self, item: &EventTimelineItem, ) -> Option { + let mut find_and_remove_pending = |event_id| { + let edits = &mut self.meta.pending_edits; + let pos = edits.iter().position(|(prev_event_id, _)| prev_event_id == event_id)?; + Some(edits.remove(pos).unwrap().1) + }; + if let Flow::Remote { event_id, .. } = &self.ctx.flow { match item.content() { TimelineItemContent::Message(..) => { - let pending = self.meta.pending_edits.remove(event_id)?; + let pending = find_and_remove_pending(event_id)?; let edit = as_variant!(pending, PendingEdit::RoomMessage)?; self.apply_msg_edit(item, edit) } TimelineItemContent::Poll(..) => { - let pending = self.meta.pending_edits.remove(event_id)?; + let pending = find_and_remove_pending(event_id)?; let edit = as_variant!(pending, PendingEdit::Poll)?; self.apply_poll_edit(item, edit) } From 83cc0acf7b1ab5057ab463f945be2bd393e5e86c Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 9 Sep 2024 15:55:31 +0100 Subject: [PATCH 079/979] ffi: Expose RoomSendQueue::unwedge to allow resending. --- bindings/matrix-sdk-ffi/src/room.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 7c6b41833de..c736007e381 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -24,7 +24,8 @@ use ruma::{ }, TimelineEventType, }, - EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, UserId, + EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, TransactionId, + UserId, }; use tokio::sync::RwLock; use tracing::error; @@ -810,6 +811,24 @@ impl Room { Ok(()) } + + /// Attempt to manually resend messages that failed to send due to issues + /// that should now have been fixed. + /// + /// This is useful for example, when there's a + /// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error; + /// the user may have re-verified on a different device and would now + /// like to send the failed message that's waiting on this device. + /// + /// # Arguments + /// + /// * `transaction_id` - The send queue transaction identifier of the local + /// echo that should be unwedged. + pub async fn try_resend(&self, transaction_id: String) -> Result<(), ClientError> { + let transaction_id: &TransactionId = transaction_id.as_str().into(); + self.inner.send_queue().unwedge(transaction_id).await?; + Ok(()) + } } /// Generates a `matrix.to` permalink to the given room alias. From 729ba3e22b1b27000b09ba630628c0b86bd2b8db Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 10 Sep 2024 11:05:55 +0200 Subject: [PATCH 080/979] ffi: rename RustShieldState to SdkShieldState This is all Rust code, after all :) --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index eed2fdba4b4..340a549353a 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -26,7 +26,7 @@ use matrix_sdk::{ AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, Thumbnail, }, - deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode}, + deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode}, Error, }; use matrix_sdk_ui::timeline::{ @@ -1021,16 +1021,16 @@ pub enum ShieldState { None, } -impl From for ShieldState { - fn from(value: RustShieldState) -> Self { +impl From for ShieldState { + fn from(value: SdkShieldState) -> Self { match value { - RustShieldState::Red { code, message } => { + SdkShieldState::Red { code, message } => { Self::Red { code, message: message.to_owned() } } - RustShieldState::Grey { code, message } => { + SdkShieldState::Grey { code, message } => { Self::Grey { code, message: message.to_owned() } } - RustShieldState::None => Self::None, + SdkShieldState::None => Self::None, } } } From a6f84d8513cb5008c07225280f93f5f6919a188a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 10 Sep 2024 10:06:34 +0200 Subject: [PATCH 081/979] feat(sdk): Use the user ID to discover the sliding sync proxy. This patch improves `Client::available_sliding_sync_versions` when trying to detect the sliding sync proxy. Previously, we were relying on the `Client::server` to send the `discover_homeserver::Request`. Sadly, this value is an `Option<_>`, meaning it's not always defined (it depends how the `Client` has been built with `HomeserverConfig`: sometimes the homeserver URL is passed directly, so the server cannot be known). This patch tries to find to discover the homeserver by using `Client::server` if it exists, like before, but it also tries by using `Client::user_id`. Another problem arises then: the user ID indeed contains a server name, but we don't know whether it's behind HTTPS or HTTP. Thus, this patch tries both: it starts by testing with `https://` and then fallbacks to `http://`. A test has been added accordingly. --- crates/matrix-sdk/src/sliding_sync/client.rs | 142 +++++++++++++++---- 1 file changed, 118 insertions(+), 24 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index d038e75d3f5..5310ccc0277 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -16,7 +16,7 @@ use tracing::error; use url::Url; use super::{SlidingSync, SlidingSyncBuilder}; -use crate::{config::RequestConfig, Client, Result, SlidingSyncRoom}; +use crate::{config::RequestConfig, http_client::HttpClient, Client, Result, SlidingSyncRoom}; /// A sliding sync version. #[derive(Clone, Debug)] @@ -156,22 +156,55 @@ impl Client { /// If `.well-known` or `/versions` is unreachable, it will simply move /// potential sliding sync versions aside. No error will be reported. pub async fn available_sliding_sync_versions(&self) -> Vec { - let well_known = match self.server().map(ToString::to_string) { - None => None, - Some(server) => self - .inner - .http_client - .send( - discover_homeserver::Request::new(), - Some(RequestConfig::short_retry()), - server, - None, - &[MatrixVersion::V1_0], - Default::default(), - ) - .await - .ok(), + async fn discover_homeserver( + http_client: &HttpClient, + server: Option, + ) -> Option { + if let Some(server) = server { + http_client + .send( + discover_homeserver::Request::new(), + Some(RequestConfig::short_retry()), + server, + None, + &[MatrixVersion::V1_0], + Default::default(), + ) + .await + .ok() + } else { + None + } + } + + let http_client = &self.inner.http_client; + + // Discover the homeserver by using: + // + // * the server if any, + // * by using the user ID's server name (if any) with `https://`, + // * by using the user ID's server name (if any) with `http://`. + // + // Otherwise, `well_known` is `None`. + let well_known = if let Some(well_known) = + discover_homeserver(http_client, self.server().map(ToString::to_string)).await + { + Some(well_known) + } else if let Some(well_known) = discover_homeserver( + http_client, + self.user_id().map(|user_id| format!("https://{}", user_id.server_name())), + ) + .await + { + Some(well_known) + } else { + discover_homeserver( + http_client, + self.user_id().map(|user_id| format!("http://{}", user_id.server_name())), + ) + .await }; + let supported_versions = self.unstable_features().await.ok().map(|unstable_features| { let mut response = get_supported_versions::Response::new(vec![]); response.unstable_features = unstable_features; @@ -325,9 +358,11 @@ mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; - use matrix_sdk_base::notification_settings::RoomNotificationMode; + use matrix_sdk_base::{notification_settings::RoomNotificationMode, SessionMeta}; use matrix_sdk_test::async_test; - use ruma::{api::MatrixVersion, assign, room_id, serde::Raw, ServerName}; + use ruma::{ + api::MatrixVersion, assign, owned_device_id, room_id, serde::Raw, OwnedUserId, ServerName, + }; use serde_json::json; use url::Url; use wiremock::{ @@ -338,6 +373,7 @@ mod tests { use super::{discover_homeserver, get_supported_versions, Version, VersionBuilder}; use crate::{ error::Result, + matrix_auth::{MatrixSession, MatrixSessionTokens}, sliding_sync::{http, VersionBuilderError}, test_utils::logged_in_client_with_server, Client, SlidingSyncList, SlidingSyncMode, @@ -393,7 +429,7 @@ mod tests { #[test] fn test_version_builder_discover_proxy_no_sliding_sync_proxy_in_well_known() { let mut response = discover_homeserver::Response::new( - discover_homeserver::HomeserverInfo::new("matrix.org".to_owned()), + discover_homeserver::HomeserverInfo::new("matrix-client.matrix.org".to_owned()), ); response.sliding_sync_proxy = None; // already `None` but the test is clearer now. @@ -406,7 +442,7 @@ mod tests { #[test] fn test_version_builder_discover_proxy_invalid_sliding_sync_proxy_in_well_known() { let mut response = discover_homeserver::Response::new( - discover_homeserver::HomeserverInfo::new("matrix.org".to_owned()), + discover_homeserver::HomeserverInfo::new("matrix-client.matrix.org".to_owned()), ); response.sliding_sync_proxy = Some(discover_homeserver::SlidingSyncProxyInfo::new("💥".to_owned())); @@ -460,22 +496,25 @@ mod tests { } #[async_test] - async fn test_available_sliding_sync_versions_proxy() { + async fn test_available_sliding_sync_versions_proxy_with_server() { let server = MockServer::start().await; + let homeserver = format!("https://{}/homeserver", server.address()); + let proxy = format!("https://{}/sliding-sync-proxy", server.address()); Mock::given(method("GET")) .and(path("/.well-known/matrix/client")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "m.homeserver": { - "base_url": "https://matrix.org", + "base_url": homeserver, }, "org.matrix.msc3575.proxy": { - "url": "https://proxy.matrix.org", + "url": proxy, }, }))) .mount(&server) .await; + // The server knows the server. let client = Client::builder() .insecure_server_name_no_tls( <&ServerName>::try_from(server.address().to_string().as_str()).unwrap(), @@ -492,7 +531,62 @@ mod tests { assert_matches!( &available_versions[0], Version::Proxy { url } => { - assert_eq!(url, &Url::parse("https://proxy.matrix.org").unwrap()); + assert_eq!(url, &Url::parse(&proxy).unwrap()); + } + ); + } + + #[async_test] + async fn test_available_sliding_sync_versions_proxy_with_user_id() { + let server = MockServer::start().await; + let homeserver = format!("https://{}/homeserver", server.address()); + let proxy = format!("https://{}/sliding-sync-proxy", server.address()); + + Mock::given(method("GET")) + .and(path("/.well-known/matrix/client")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "m.homeserver": { + "base_url": homeserver, + }, + "org.matrix.msc3575.proxy": { + "url": proxy, + }, + }))) + .mount(&server) + .await; + + // The client doesn't know the server. + let client = Client::builder() + .homeserver_url(homeserver) + .server_versions([MatrixVersion::V1_0]) + .build() + .await + .unwrap(); + + // The client knows a user. + client + .matrix_auth() + .restore_session(MatrixSession { + meta: SessionMeta { + user_id: OwnedUserId::try_from(format!("@alice:{}", server.address())).unwrap(), + device_id: owned_device_id!("DEVICEID"), + }, + tokens: MatrixSessionTokens { + access_token: "1234".to_owned(), + refresh_token: None, + }, + }) + .await + .unwrap(); + + let available_versions = client.available_sliding_sync_versions().await; + + // `.well-known` is available. + assert_eq!(available_versions.len(), 1); + assert_matches!( + &available_versions[0], + Version::Proxy { url } => { + assert_eq!(url, &Url::parse(&proxy).unwrap()); } ); } From 08df153ed9753321ff25fb081d40b25859cf8457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 10 Sep 2024 08:56:03 +0200 Subject: [PATCH 082/979] sdk-ui: create `TimelineState::replace_all` which combines `clear` and `add_remote_events_at` in the same transaction --- .../src/timeline/controller/mod.rs | 11 ++++--- .../src/timeline/controller/state.rs | 25 +++++++++++++-- .../matrix-sdk-ui/src/timeline/tests/basic.rs | 32 +++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 2ce7bebecdd..13088594940 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -637,8 +637,6 @@ impl TimelineController

{ ) { let mut state = self.state.write().await; - state.clear(); - let track_read_markers = self.settings.track_read_receipts; if track_read_markers { state.populate_initial_user_receipt(&self.room_data_provider, ReceiptType::Read).await; @@ -647,9 +645,14 @@ impl TimelineController

{ .await; } - if !events.is_empty() { + // Replace the events if either the current event list or the new one aren't + // empty. + // Previously we just had to check the new one wasn't empty because + // we did a clear operation before so the current one would always be empty, but + // now we may want to replace a populated timeline with an empty one. + if !state.items.is_empty() || !events.is_empty() { state - .add_remote_events_at( + .replace_with_remove_events( events, TimelineEnd::Back, origin, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index fb81dd4dcea..7aebfb4f4f3 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -63,7 +63,7 @@ use crate::{ pub(crate) enum TimelineEnd { /// Event should be prepended to the front of the timeline. Front, - /// Event should appended to the back of the timeline. + /// Event should be appended to the back of the timeline. Back, } @@ -101,7 +101,7 @@ impl TimelineState { } } - /// Add the given remove events at the given end of the timeline. + /// Add the given remote events at the given end of the timeline. /// /// Note: when the `position` is [`TimelineEnd::Front`], prepended events /// should be ordered in *reverse* topological order, that is, `events[0]` @@ -274,6 +274,27 @@ impl TimelineState { txn.commit(); } + /// Replaces the existing events in the timeline with the given remote ones. + /// + /// Note: when the `position` is [`TimelineEnd::Front`], prepended events + /// should be ordered in *reverse* topological order, that is, `events[0]` + /// is the most recent. + pub(super) async fn replace_with_remove_events( + &mut self, + events: Vec, + position: TimelineEnd, + origin: RemoteEventOrigin, + room_data_provider: &P, + settings: &TimelineSettings, + ) -> HandleManyEventsResult { + let mut txn = self.transaction(); + txn.clear(); + let result = + txn.add_remote_events_at(events, position, origin, room_data_provider, settings).await; + txn.commit(); + result + } + pub(super) fn transaction(&mut self) -> TimelineStateTransaction<'_> { let items = self.items.transaction(); let meta = self.meta.clone(); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index aa212be40ce..d0149b7b3a3 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -15,6 +15,7 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; +use futures_util::StreamExt; use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB, CAROL}; use ruma::{ events::{ @@ -505,3 +506,34 @@ async fn test_thread() { assert_let!(TimelineDetails::Ready(replied_to_event) = &in_reply_to.event); assert_eq!(replied_to_event.sender(), *ALICE); } + +#[async_test] +async fn test_replace_with_initial_events_when_batched() { + let timeline = TestTimeline::with_room_data_provider(TestRoomDataProvider::default()) + .with_settings(TimelineSettings::default()); + + let f = &timeline.factory; + let ev = f.text_msg("hey").sender(*ALICE).into_sync(); + + timeline.controller.add_events_at(vec![ev], TimelineEnd::Back, RemoteEventOrigin::Sync).await; + + let (items, mut stream) = timeline.controller.subscribe_batched().await; + assert_eq!(items.len(), 2); + assert!(items[0].is_day_divider()); + assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "hey"); + + let ev = f.text_msg("yo").sender(*BOB).into_sync(); + timeline.controller.replace_with_initial_remote_events(vec![ev], RemoteEventOrigin::Sync).await; + + // Assert there are more than a single Clear diff in the next batch: + // Clear + PushBack (event) + PushFront (day divider) + let batched_diffs = stream.next().await.unwrap(); + assert_eq!(batched_diffs.len(), 3); + assert_matches!(batched_diffs[0], VectorDiff::Clear); + assert_matches!(&batched_diffs[1], VectorDiff::PushBack { value } => { + assert!(value.as_event().is_some()); + }); + assert_matches!(&batched_diffs[2], VectorDiff::PushFront { value } => { + assert_matches!(value.as_virtual(), Some(VirtualTimelineItem::DayDivider(_))); + }); +} From cb825864b923d513534f8f80f9757298cba4c682 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 10 Sep 2024 20:02:11 +0200 Subject: [PATCH 083/979] event cache: reset paginator state when receiving a limited timeline --- crates/matrix-sdk/src/event_cache/mod.rs | 10 +- .../matrix-sdk/src/event_cache/pagination.rs | 3 +- .../matrix-sdk/src/event_cache/paginator.rs | 18 +++- .../tests/integration/event_cache.rs | 99 ++++++++++++++++++- 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index b7e122c6388..2c12911712b 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -53,6 +53,7 @@ use matrix_sdk_base::{ sync::{JoinedRoomUpdate, LeftRoomUpdate, RoomUpdates, Timeline}, }; use matrix_sdk_common::executor::{spawn, JoinHandle}; +use paginator::PaginatorState; use ruma::{ events::{ room::{message::Relation, redaction::SyncRoomRedactionEvent}, @@ -766,11 +767,16 @@ impl RoomEventCacheInner { self.append_events_locked_impl( room_events, sync_timeline_events, - prev_batch, + prev_batch.clone(), ephemeral_events, ambiguity_changes, ) - .await + .await?; + + // Reset the paginator status to initial. + self.pagination.paginator.set_idle_state(PaginatorState::Initial, prev_batch, None)?; + + Ok(()) } /// Append a set of events to the room cache and storage, notifying diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index f334f470794..f29f84ceb7b 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -133,12 +133,11 @@ impl RoomPagination { } async fn run_backwards_impl(&self, batch_size: u16) -> Result> { - // Make sure there's at most one back-pagination request. let prev_token = self.get_or_wait_for_token().await; let paginator = &self.inner.pagination.paginator; - paginator.set_idle_state(prev_token.clone(), None)?; + paginator.set_idle_state(PaginatorState::Idle, prev_token.clone(), None)?; // Run the actual pagination. let PaginationResult { events, hit_end_of_timeline: reached_start } = diff --git a/crates/matrix-sdk/src/event_cache/paginator.rs b/crates/matrix-sdk/src/event_cache/paginator.rs index 010bc327383..df240325a5d 100644 --- a/crates/matrix-sdk/src/event_cache/paginator.rs +++ b/crates/matrix-sdk/src/event_cache/paginator.rs @@ -219,11 +219,19 @@ impl Paginator { /// (running /context or /messages). pub(super) fn set_idle_state( &self, + next_state: PaginatorState, prev_batch_token: Option, next_batch_token: Option, ) -> Result<(), PaginatorError> { let prev_state = self.state.get(); + match next_state { + PaginatorState::Initial | PaginatorState::Idle => {} + PaginatorState::FetchingTargetEvent | PaginatorState::Paginating => { + panic!("internal error: set_idle_state only accept Initial|Idle next states"); + } + } + match prev_state { PaginatorState::Initial | PaginatorState::Idle => {} PaginatorState::FetchingTargetEvent | PaginatorState::Paginating => { @@ -236,7 +244,7 @@ impl Paginator { } } - self.state.set_if_not_eq(PaginatorState::Idle); + self.state.set_if_not_eq(next_state); *self.prev_batch_token.lock().unwrap() = prev_batch_token.into(); *self.next_batch_token.lock().unwrap() = next_batch_token.into(); @@ -1132,7 +1140,13 @@ mod tests { // Assuming a paginator ready to back- or forward- paginate, let paginator = Paginator::new(room.clone()); - paginator.set_idle_state(Some("prev".to_owned()), Some("next".to_owned())).unwrap(); + paginator + .set_idle_state( + PaginatorState::Idle, + Some("prev".to_owned()), + Some("next".to_owned()), + ) + .unwrap(); let paginator = Arc::new(paginator); diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index d6c279d8c7d..e28185a8d26 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -3,7 +3,7 @@ use std::{future::ready, ops::ControlFlow, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ event_cache::{ - BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, + paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, test_utils::{assert_event_matches_msg, events::EventFactory, logged_in_client_with_server}, @@ -789,3 +789,100 @@ async fn test_backpaginating_without_token() { assert!(room_stream.is_empty()); } + +#[async_test] +async fn test_limited_timeline_resets_pagination() { + let (client, server) = logged_in_client_with_server().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + + // If I sync and get informed I've joined The Room, without a previous batch + // token, + let room_id = room_id!("!omelette:fromage.fr"); + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + let mut sync_builder = SyncResponseBuilder::new(); + + { + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + let response_body = sync_builder.build_json_sync_response(); + + mock_sync(&server, response_body, None).await; + client.sync_once(Default::default()).await.unwrap(); + server.reset().await; + } + + let (room_event_cache, _drop_handles) = + client.get_room(room_id).unwrap().event_cache().await.unwrap(); + + let (events, mut room_stream) = room_event_cache.subscribe().await.unwrap(); + + assert!(events.is_empty()); + assert!(room_stream.is_empty()); + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "chunk": vec![ + f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline() + ], + "start": "t392-516_47314_0_7_1_1_1_11444_1", + }))) + .expect(1) + .mount(&server) + .await; + + // At the beginning, the paginator is in the initial state. + let pagination = room_event_cache.pagination(); + let mut pagination_status = pagination.status(); + assert_eq!(pagination_status.get(), PaginatorState::Initial); + + // If we try to back-paginate with a token, it will hit the end of the timeline + // and give us the resulting event. + let BackPaginationOutcome { events, reached_start } = + pagination.run_backwards(20, once).await.unwrap(); + + assert_eq!(events.len(), 1); + assert!(reached_start); + + // And the paginator state delives this as an update, and is internally + // consistent with it: + assert_eq!( + timeout(Duration::from_secs(1), pagination_status.next()).await, + Ok(Some(PaginatorState::Idle)) + ); + assert!(pagination.hit_timeline_start()); + + // When a limited sync comes back from the server, + { + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).set_timeline_limited()); + let response_body = sync_builder.build_json_sync_response(); + + mock_sync(&server, response_body, None).await; + client.sync_once(Default::default()).await.unwrap(); + server.reset().await; + } + + // We receive an update about the limited timeline. + assert_matches!( + timeout(Duration::from_secs(1), room_stream.recv()).await, + Ok(Ok(RoomEventCacheUpdate::Clear)) + ); + + // The paginator state is reset: status set to Initial, hasn't hit the timeline + // start. + assert!(!pagination.hit_timeline_start()); + assert_eq!(pagination_status.get(), PaginatorState::Initial); + + // We receive an update about the paginator status. + let next_state = timeout(Duration::from_secs(1), pagination_status.next()) + .await + .expect("timeout") + .expect("no update"); + assert_eq!(next_state, PaginatorState::Initial); + + assert!(room_stream.is_empty()); +} From b7bde3cabed55239d589c1b460a95627930407f3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 10 Sep 2024 15:16:12 +0200 Subject: [PATCH 084/979] feat(crypto): Implement OldMachine::mark_all_tracked_users_as_dirty. This patch adds the `OldMachine::mark_all_tracked_users_as_dirty`. This patch rewrites a bit `OlmMachine::new_helper` by extracting some piece of it inside `OlmMachine::new_helper_prelude`. With that, we can rewrite `OlmMachine::migration_post_verified_latch_support` to use `IdentityManager::mark_all_tracked_users_as_dirty`. This latter is the shared implementation with `OlmMachine::mark_all_tracked_users_as_dirty`. This patch adds a test for `OlmMachine:mark_all_tracked_users_as_dirty`. --- .../src/identities/manager.rs | 24 +++++- crates/matrix-sdk-crypto/src/machine/mod.rs | 84 ++++++++++++++----- .../src/machine/tests/mod.rs | 49 +++++++++-- crates/matrix-sdk-crypto/src/store/mod.rs | 4 + 4 files changed, 134 insertions(+), 27 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index 28ab62eba87..0eb5829064a 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -36,7 +36,7 @@ use crate::{ requests::KeysQueryRequest, store::{ caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager, - Result as StoreResult, Store, StoreCache, UserKeyQueryResult, + Result as StoreResult, Store, StoreCache, StoreCacheGuard, UserKeyQueryResult, }, types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey}, CryptoStoreError, LocalTrust, OwnUserIdentity, SignatureError, UserIdentities, @@ -1147,6 +1147,28 @@ impl IdentityManager { Ok(()) } + + /// Mark all tracked users as dirty. + /// + /// See `SyncedKeyQueryManager::mark_tracked_users_as_changed()` to learn + /// more. + pub(crate) async fn mark_all_tracked_users_as_dirty( + &self, + store_cache: StoreCacheGuard, + ) -> StoreResult<()> { + let store_wrapper = store_cache.store_wrapper(); + let tracked_users = store_wrapper.load_tracked_users().await?; + + self.key_query_manager + .synced(&store_cache) + .await? + .mark_tracked_users_as_changed( + tracked_users.iter().map(|tracked_user| tracked_user.user_id.as_ref()), + ) + .await?; + + Ok(()) + } } /// Log information about what changed after processing a /keys/query response. diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 5a2214566f5..34bba27cd0e 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -185,30 +185,43 @@ impl OlmMachine { }) .await?; + let (verification_machine, store, identity_manager) = + Self::new_helper_prelude(store, static_account, self.store().private_identity()); + Ok(Self::new_helper( device_id, store, - static_account, + verification_machine, + identity_manager, self.store().private_identity(), None, )) } + fn new_helper_prelude( + store_wrapper: Arc, + account: StaticAccountData, + user_identity: Arc>, + ) -> (VerificationMachine, Store, IdentityManager) { + let verification_machine = + VerificationMachine::new(account.clone(), user_identity.clone(), store_wrapper.clone()); + let store = Store::new(account, user_identity, store_wrapper, verification_machine.clone()); + + let identity_manager = IdentityManager::new(store.clone()); + + (verification_machine, store, identity_manager) + } + fn new_helper( device_id: &DeviceId, - store: Arc, - account: StaticAccountData, + store: Store, + verification_machine: VerificationMachine, + identity_manager: IdentityManager, user_identity: Arc>, maybe_backup_key: Option, ) -> Self { - let verification_machine = - VerificationMachine::new(account.clone(), user_identity.clone(), store.clone()); - let store = Store::new(account, user_identity.clone(), store, verification_machine.clone()); - let group_session_manager = GroupSessionManager::new(store.clone()); - let identity_manager = IdentityManager::new(store.clone()); - let users_for_key_claim = Arc::new(StdRwLock::new(BTreeMap::new())); let key_request_machine = GossipMachine::new( store.clone(), @@ -360,11 +373,21 @@ impl OlmMachine { let identity = Arc::new(Mutex::new(identity)); let store = Arc::new(CryptoStoreWrapper::new(user_id, device_id, store)); + let (verification_machine, store, identity_manager) = + Self::new_helper_prelude(store, static_account, identity.clone()); + // FIXME: We might want in the future a more generic high-level data migration // mechanism (at the store wrapper layer). - Self::migration_post_verified_latch_support(&store).await?; + Self::migration_post_verified_latch_support(&store, &identity_manager).await?; - Ok(OlmMachine::new_helper(device_id, store, static_account, identity, maybe_backup_key)) + Ok(Self::new_helper( + device_id, + store, + verification_machine, + identity_manager, + identity, + maybe_backup_key, + )) } // The sdk now support verified identity change detection. @@ -375,19 +398,15 @@ impl OlmMachine { // // pub(crate) visibility for testing. pub(crate) async fn migration_post_verified_latch_support( - store: &CryptoStoreWrapper, + store: &Store, + identity_manager: &IdentityManager, ) -> Result<(), CryptoStoreError> { let maybe_migrate_for_identity_verified_latch = store.get_custom_value(Self::HAS_MIGRATED_VERIFICATION_LATCH).await?.is_none(); + if maybe_migrate_for_identity_verified_latch { - // We want to mark all tracked users as dirty to ensure the verified latch is - // set up correctly. - let tracked_user = store.load_tracked_users().await?; - let mut store_updates = Vec::with_capacity(tracked_user.len()); - tracked_user.iter().for_each(|tu| { - store_updates.push((tu.user_id.as_ref(), true)); - }); - store.save_tracked_users(&store_updates).await?; + identity_manager.mark_all_tracked_users_as_dirty(store.cache().await?).await?; + store.set_custom_value(Self::HAS_MIGRATED_VERIFICATION_LATCH, vec![0]).await? } Ok(()) @@ -1992,6 +2011,17 @@ impl OlmMachine { self.inner.identity_manager.update_tracked_users(users).await } + /// Mark all tracked users as dirty. + /// + /// See `IdentityManager::mark_all_tracked_users_as_dirty()` to learn + /// more. + pub async fn mark_all_tracked_users_as_dirty(&self) -> StoreResult<()> { + self.inner + .identity_manager + .mark_all_tracked_users_as_dirty(self.inner.store.cache().await?) + .await + } + async fn wait_if_user_pending( &self, user_id: &UserId, @@ -2404,10 +2434,10 @@ impl OlmMachine { Ok(()) } - #[cfg(any(feature = "testing", test))] /// Returns whether this `OlmMachine` is the same another one. /// /// Useful for testing purposes only. + #[cfg(any(feature = "testing", test))] pub fn same_as(&self, other: &OlmMachine) -> bool { Arc::ptr_eq(&self.inner, &other.inner) } @@ -2419,6 +2449,18 @@ impl OlmMachine { let account = cache.account().await?; Ok(account.uploaded_key_count()) } + + /// Returns the identity manager. + #[cfg(test)] + pub(crate) fn identity_manager(&self) -> &IdentityManager { + &self.inner.identity_manager + } + + /// Returns a store key, only useful for testing purposes. + #[cfg(test)] + pub(crate) fn key_for_has_migrated_verification_latch() -> &'static str { + Self::HAS_MIGRATED_VERIFICATION_LATCH + } } fn sender_data_to_verification_state( diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 3d355fd603a..8d7bea24dad 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, iter, sync::Arc, time::Duration}; +use std::{collections::BTreeMap, iter, ops::Not, sync::Arc, time::Duration}; use assert_matches2::assert_matches; use futures_util::{pin_mut, FutureExt, StreamExt}; @@ -1528,6 +1528,43 @@ async fn test_unsigned_decryption() { assert_matches!(thread_encryption_result, UnsignedDecryptionResult::Decrypted(_)); } +#[async_test] +async fn test_mark_all_tracked_users_as_dirty() { + let store = MemoryStore::new(); + let account = vodozemac::olm::Account::new(); + + // Put some tracked users + let damir = user_id!("@damir:localhost"); + let ben = user_id!("@ben:localhost"); + let ivan = user_id!("@ivan:localhost"); + + // Mark them as not dirty. + store.save_tracked_users(&[(damir, false), (ben, false), (ivan, false)]).await.unwrap(); + + // Let's imagine the migration has been done: this is useful so that tracked + // users are not marked as dirty when creating the `OlmMachine`. + store + .set_custom_value(OlmMachine::key_for_has_migrated_verification_latch(), vec![0]) + .await + .unwrap(); + + let alice = + OlmMachine::with_store(user_id(), alice_device_id(), store, Some(account)).await.unwrap(); + + // All users are marked as not dirty. + alice.store().load_tracked_users().await.unwrap().iter().for_each(|tracked_user| { + assert!(tracked_user.dirty.not()); + }); + + // Now, mark all tracked users as dirty. + alice.mark_all_tracked_users_as_dirty().await.unwrap(); + + // All users are now marked as dirty. + alice.store().load_tracked_users().await.unwrap().iter().for_each(|tracked_user| { + assert!(tracked_user.dirty); + }); +} + #[async_test] async fn test_verified_latch_migration() { let store = MemoryStore::new(); @@ -1544,20 +1581,22 @@ async fn test_verified_latch_migration() { let alice = OlmMachine::with_store(user_id(), alice_device_id(), store, Some(account)).await.unwrap(); + let alice_store = alice.store(); + // A migration should have occurred and all users should be marked as dirty - alice.store().load_tracked_users().await.unwrap().iter().for_each(|tu| { + alice_store.load_tracked_users().await.unwrap().iter().for_each(|tu| { assert!(tu.dirty); }); // Ensure it does so only once - alice.store().save_tracked_users(&to_track_not_dirty).await.unwrap(); + alice_store.save_tracked_users(&to_track_not_dirty).await.unwrap(); - OlmMachine::migration_post_verified_latch_support(alice.store().crypto_store().as_ref()) + OlmMachine::migration_post_verified_latch_support(alice_store, alice.identity_manager()) .await .unwrap(); // Migration already done, so user should not be marked as dirty - alice.store().load_tracked_users().await.unwrap().iter().for_each(|tu| { + alice_store.load_tracked_users().await.unwrap().iter().for_each(|tu| { assert!(!tu.dirty); }); } diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index f294803b82a..c06de547612 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -351,6 +351,10 @@ pub(crate) struct StoreCache { } impl StoreCache { + pub(crate) fn store_wrapper(&self) -> &CryptoStoreWrapper { + self.store.as_ref() + } + /// Returns a reference to the `Account`. /// /// Either load the account from the cache, or the store if missing from From 6e361114628c093d0622468eaf54d831dccab301 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 10 Sep 2024 15:17:46 +0200 Subject: [PATCH 085/979] fix(sdk): Mark tracked users as dirty when the SS connection is reset. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is a non-negligible difference MSC3575 and MSC4186 in how the `e2ee` extension works. When the client sends a request with no `pos`: * MSC3575 returns all device lists updates since the last request from the device that asked for device lists (this works similarly to to-device message handling), * MSC4186 returns no device lists updates, as it only returns changes since the provided `pos` (which is `null` in this case); this is in line with sync v2. Therefore, with MSC4186, the device list cache must be marked as to be re-downloaded if the `since` token is `None`, otherwise it's easy to miss device lists updates that happened between the previous request and the new “initial” request. --- crates/matrix-sdk/src/sliding_sync/error.rs | 10 ++ crates/matrix-sdk/src/sliding_sync/mod.rs | 179 +++++++++++++++++++- 2 files changed, 181 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/error.rs b/crates/matrix-sdk/src/sliding_sync/error.rs index 10706d860fd..c72d5333e03 100644 --- a/crates/matrix-sdk/src/sliding_sync/error.rs +++ b/crates/matrix-sdk/src/sliding_sync/error.rs @@ -53,4 +53,14 @@ pub enum Error { /// The original `JoinError`. error: JoinError, }, + + /// No Olm machine. + #[cfg(feature = "e2e-encryption")] + #[error("The Olm machine is missing")] + NoOlmMachine, + + /// An error occurred during a E2EE operation. + #[cfg(feature = "e2e-encryption")] + #[error(transparent)] + CryptoStoreError(#[from] matrix_sdk_base::crypto::CryptoStoreError), } diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index de002be7388..ab570b18687 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -491,6 +491,28 @@ impl SlidingSync { Span::current().record("pos", &pos); + // There is a non-negligible difference MSC3575 and MSC4186 in how + // the `e2ee` extension works. When the client sends a request with + // no `pos`: + // + // * MSC3575 returns all device lists updates since the last request from the + // device that asked for device lists (this works similarly to to-device + // message handling), + // * MSC4186 returns no device lists updates, as it only returns changes since + // the provided `pos` (which is `null` in this case); this is in line with + // sync v2. + // + // Therefore, with MSC4186, the device list cache must be marked as to be + // re-downloaded if the `since` token is `None`, otherwise it's easy to miss + // device lists updates that happened between the previous request and the new + // “initial” request. + #[cfg(feature = "e2e-encryption")] + if pos.is_none() && self.inner.version.is_native() && self.is_e2ee_enabled() { + let olm_machine = self.inner.client.olm_machine().await; + let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?; + olm_machine.mark_all_tracked_users_as_dirty().await?; + } + // Configure the timeout. // // The `timeout` query is necessary when all lists require it. Please see @@ -841,15 +863,9 @@ enum SlidingSyncInternalMessage { #[cfg(any(test, feature = "testing"))] impl SlidingSync { - /// Get a copy of the `pos` value. - pub fn pos(&self) -> Option { - let position_lock = self.inner.position.blocking_lock(); - position_lock.pos.clone() - } - /// Set a new value for `pos`. - pub fn set_pos(&self, new_pos: String) { - let mut position_lock = self.inner.position.blocking_lock(); + pub async fn set_pos(&self, new_pos: String) { + let mut position_lock = self.inner.position.lock().await; position_lock.pos = Some(new_pos); } @@ -1660,6 +1676,153 @@ mod tests { Ok(()) } + // With MSC4186, with the `e2ee` extension enabled, if a request has no `pos`, + // all the tracked users by the `OlmMachine` must be marked as dirty, i.e. + // `/key/query` requests must be sent. See the code to see the details. + // + // This test is asserting that. + #[async_test] + #[cfg(feature = "e2e-encryption")] + async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> { + use matrix_sdk_base::crypto::{IncomingResponse, OutgoingRequests}; + use matrix_sdk_test::ruma_response_from_json; + use ruma::user_id; + + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + let alice = user_id!("@alice:localhost"); + let bob = user_id!("@bob:localhost"); + let me = user_id!("@example:localhost"); + + // Track and mark users are not dirty, so that we can check they are “dirty” + // after that. Dirty here means that a `/key/query` must be sent. + { + let olm_machine = client.olm_machine().await; + let olm_machine = olm_machine.as_ref().unwrap(); + + olm_machine.update_tracked_users([alice, bob]).await?; + + // Assert requests. + let outgoing_requests = olm_machine.outgoing_requests().await?; + + assert_eq!(outgoing_requests.len(), 2); + assert_matches!(outgoing_requests[0].request(), OutgoingRequests::KeysUpload(_)); + assert_matches!(outgoing_requests[1].request(), OutgoingRequests::KeysQuery(_)); + + // Fake responses. + olm_machine + .mark_request_as_sent( + outgoing_requests[0].request_id(), + IncomingResponse::KeysUpload(&ruma_response_from_json(&json!({ + "one_time_key_counts": {} + }))), + ) + .await?; + + olm_machine + .mark_request_as_sent( + outgoing_requests[1].request_id(), + IncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ + "device_keys": { + alice: {}, + bob: {}, + } + }))), + ) + .await?; + + // Once more. + let outgoing_requests = olm_machine.outgoing_requests().await?; + + assert_eq!(outgoing_requests.len(), 1); + assert_matches!(outgoing_requests[0].request(), OutgoingRequests::KeysQuery(_)); + + olm_machine + .mark_request_as_sent( + outgoing_requests[0].request_id(), + IncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ + "device_keys": { + me: {}, + } + }))), + ) + .await?; + + // No more. + let outgoing_requests = olm_machine.outgoing_requests().await?; + + assert!(outgoing_requests.is_empty()); + } + + let sync = client + .sliding_sync("test-slidingsync")? + .add_list(SlidingSyncList::builder("new_list")) + .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)})) + .build() + .await?; + + // First request: no `pos`. + let txn_id = TransactionId::new(); + let (_request, _, _) = sync + .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) + .await?; + + // Now, tracked users must be dirty. + { + let olm_machine = client.olm_machine().await; + let olm_machine = olm_machine.as_ref().unwrap(); + + // Assert requests. + let outgoing_requests = olm_machine.outgoing_requests().await?; + + assert_eq!(outgoing_requests.len(), 1); + assert_matches!( + outgoing_requests[0].request(), + OutgoingRequests::KeysQuery(request) => { + assert!(request.device_keys.contains_key(alice)); + assert!(request.device_keys.contains_key(bob)); + assert!(request.device_keys.contains_key(me)); + } + ); + + // Fake responses. + olm_machine + .mark_request_as_sent( + outgoing_requests[0].request_id(), + IncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ + "device_keys": { + alice: {}, + bob: {}, + me: {}, + } + }))), + ) + .await?; + } + + // Second request: with a `pos` this time. + sync.set_pos("chocolat".to_owned()).await; + + let txn_id = TransactionId::new(); + let (_request, _, _) = sync + .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) + .await?; + + // Tracked users are not marked as dirty. + { + let olm_machine = client.olm_machine().await; + let olm_machine = olm_machine.as_ref().unwrap(); + + // Assert requests. + let outgoing_requests = olm_machine.outgoing_requests().await?; + + assert!(outgoing_requests.is_empty()); + } + + Ok(()) + } + #[async_test] async fn test_unknown_pos_resets_pos_and_sticky_parameters() -> Result<()> { let server = MockServer::start().await; From a3ae9dca753d5ffb6bc3297deae664cf04092af8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 11 Sep 2024 09:44:37 +0200 Subject: [PATCH 086/979] doc(crypto): Improve documentation. --- crates/matrix-sdk-crypto/src/identities/manager.rs | 4 ++-- crates/matrix-sdk-crypto/src/machine/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index 0eb5829064a..7819d7acfac 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -1150,8 +1150,8 @@ impl IdentityManager { /// Mark all tracked users as dirty. /// - /// See `SyncedKeyQueryManager::mark_tracked_users_as_changed()` to learn - /// more. + /// All users *whose device lists we are tracking* are flagged as needing a + /// key query. Users whose devices we are not tracking are ignored. pub(crate) async fn mark_all_tracked_users_as_dirty( &self, store_cache: StoreCacheGuard, diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 34bba27cd0e..d0e985d0566 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -2013,8 +2013,8 @@ impl OlmMachine { /// Mark all tracked users as dirty. /// - /// See `IdentityManager::mark_all_tracked_users_as_dirty()` to learn - /// more. + /// All users *whose device lists we are tracking* are flagged as needing a + /// key query. Users whose devices we are not tracking are ignored. pub async fn mark_all_tracked_users_as_dirty(&self) -> StoreResult<()> { self.inner .identity_manager From 2576042194fd67edf02f07a7efb1067f1ee70cb3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 11 Sep 2024 09:45:18 +0200 Subject: [PATCH 087/979] chore(sdk): Add an `info` log in sliding sync. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index ab570b18687..c767709582d 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -508,6 +508,8 @@ impl SlidingSync { // “initial” request. #[cfg(feature = "e2e-encryption")] if pos.is_none() && self.inner.version.is_native() && self.is_e2ee_enabled() { + info!("Marking all tracked users as dirty"); + let olm_machine = self.inner.client.olm_machine().await; let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?; olm_machine.mark_all_tracked_users_as_dirty().await?; From 31e6df7234a0eb322b9104d020247a93bce8384d Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 11 Sep 2024 10:50:51 +0200 Subject: [PATCH 088/979] timeline: unify `edit` and `edit_poll` functions (#3951) ## Changes Takes care of [this TODO](https://github.com/matrix-org/matrix-rust-sdk/blob/9df1c480795c42afcee39e4c7e553d5a927a2680/crates/matrix-sdk-ui/src/timeline/mod.rs#L520). - sdk & sdk-ui: unify `Timeline::edit` and `Timeline::edit_polls`, the new fn takes an `EditedContent` parameter now, which includes a `PollStart` case too. - ffi: also unify the FFI fns there, using `PollStart` and a new `EditContent` enum that must be passed from the clients like: ```kotlin val messageContent = MessageEventContent.from(...) timeline.edit(event, EditedContent.RoomMessage(messageContent)) ``` Since the is mainly about changing the fns signatures I've reused the existing tests, including one that used `edit_poll` that now uses the new fn. --------- Co-authored-by: Benjamin Bouvier --- bindings/matrix-sdk-ffi/src/ruma.rs | 2 +- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 49 +++--- crates/matrix-sdk-ui/src/timeline/mod.rs | 69 +++----- .../tests/integration/timeline/edit.rs | 152 ++++++++++++++++- crates/matrix-sdk/src/room/edit.rs | 39 ++++- crates/matrix-sdk/src/send_queue.rs | 44 ++--- crates/matrix-sdk/src/test_utils/events.rs | 22 +-- .../tests/integration/send_queue.rs | 160 +++++++++++++++++- .../src/tests/timeline.rs | 6 +- 9 files changed, 432 insertions(+), 111 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 624e282d018..89b9c23f741 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -834,7 +834,7 @@ impl From<&RumaFileInfo> for FileInfo { } } -#[derive(uniffi::Enum)] +#[derive(Clone, uniffi::Enum)] pub enum PollKind { Disclosed, Undisclosed, diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 340a549353a..f28b0e9a039 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -27,6 +27,7 @@ use matrix_sdk::{ BaseThumbnailInfo, BaseVideoInfo, Thumbnail, }, deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode}, + room::edit::EditedContent as SdkEditedContent, Error, }; use matrix_sdk_ui::timeline::{ @@ -488,25 +489,10 @@ impl Timeline { pub async fn edit( &self, item: Arc, - new_content: Arc, + new_content: EditedContent, ) -> Result { - self.inner.edit(&item.0, (*new_content).clone()).await.map_err(ClientError::from) - } - - pub async fn edit_poll( - &self, - question: String, - answers: Vec, - max_selections: u8, - poll_kind: PollKind, - edit_item: Arc, - ) -> Result<(), ClientError> { - let poll_data = PollData { question, answers, max_selections, poll_kind }; - self.inner - .edit_poll(poll_data.fallback_text(), poll_data.try_into()?, &edit_item.0) - .await - .map_err(|err| anyhow::anyhow!(err))?; - Ok(()) + let new_content: SdkEditedContent = new_content.try_into()?; + self.inner.edit(&item.0, new_content).await.map_err(ClientError::from) } pub async fn send_location( @@ -1169,7 +1155,8 @@ impl From<&TimelineDetails> for ProfileDetails { } } -struct PollData { +#[derive(Clone, uniffi::Record)] +pub struct PollData { question: String, answers: Vec, max_selections: u8, @@ -1264,3 +1251,27 @@ impl From for ruma::api::client::receipt::create_receipt::v3::Recei } } } + +#[derive(Clone, uniffi::Enum)] +pub enum EditedContent { + RoomMessage { content: Arc }, + PollStart { poll_data: PollData }, +} + +impl TryFrom for SdkEditedContent { + type Error = ClientError; + fn try_from(value: EditedContent) -> Result { + match value { + EditedContent::RoomMessage { content } => { + Ok(SdkEditedContent::RoomMessage((*content).clone())) + } + EditedContent::PollStart { poll_data } => { + let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?; + Ok(SdkEditedContent::PollStart { + fallback_text: poll_data.fallback_text(), + new_content: block, + }) + } + } + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 2e346bebb27..58e52de8845 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -36,14 +36,11 @@ use pin_project_lite::pin_project; use ruma::{ api::client::receipt::create_receipt::v3::ReceiptType, events::{ - poll::unstable_start::{ - ReplacementUnstablePollStartEventContent, UnstablePollStartContentBlock, - UnstablePollStartEventContent, - }, + poll::unstable_start::{NewUnstablePollStartEventContent, UnstablePollStartEventContent}, receipt::{Receipt, ReceiptThread}, room::{ message::{ - AddMentions, ForwardThread, OriginalRoomMessageEvent, RoomMessageEventContent, + AddMentions, ForwardThread, OriginalRoomMessageEvent, RoomMessageEventContentWithoutRelation, }, pinned_events::RoomPinnedEventsEventContent, @@ -474,7 +471,7 @@ impl Timeline { pub async fn edit( &self, item: &EventTimelineItem, - new_content: RoomMessageEventContentWithoutRelation, + new_content: EditedContent, ) -> Result { let event_id = match item.identifier() { TimelineEventItemId::TransactionId(txn_id) => { @@ -484,9 +481,30 @@ impl Timeline { TimelineItemHandle::Remote(event_id) => event_id.to_owned(), TimelineItemHandle::Local(handle) => { // Relations are filled by the editing code itself. - let new_content: RoomMessageEventContent = new_content.clone().into(); + let new_content: AnyMessageLikeEventContent = match new_content { + EditedContent::RoomMessage(message) => { + if matches!(item.content, TimelineItemContent::Message(_)) { + AnyMessageLikeEventContent::RoomMessage(message.into()) + } else { + warn!("New content (m.room.message) doesn't match previous event content."); + return Ok(false); + } + } + EditedContent::PollStart { new_content, .. } => { + if matches!(item.content, TimelineItemContent::Poll(_)) { + AnyMessageLikeEventContent::UnstablePollStart( + UnstablePollStartEventContent::New( + NewUnstablePollStartEventContent::new(new_content), + ), + ) + } else { + warn!("New content (poll start) doesn't match previous event content."); + return Ok(false); + } + } + }; return Ok(handle - .edit(new_content.into()) + .edit(new_content) .await .map_err(RoomSendQueueError::StorageError)?); } @@ -500,46 +518,13 @@ impl Timeline { TimelineEventItemId::EventId(event_id) => event_id, }; - let content = - self.room().make_edit_event(&event_id, EditedContent::RoomMessage(new_content)).await?; + let content = self.room().make_edit_event(&event_id, new_content).await?; self.send(content).await?; Ok(true) } - pub async fn edit_poll( - &self, - fallback_text: impl Into, - poll: UnstablePollStartContentBlock, - edit_item: &EventTimelineItem, - ) -> Result<(), SendEventError> { - // TODO: refactor this function into [`Self::edit`], there's no good reason to - // keep a separate function for this. - - // Early returns here must be in sync with `EventTimelineItem::is_editable`. - if !edit_item.is_own() { - return Err(UnsupportedEditItem::NotOwnEvent.into()); - } - let Some(event_id) = edit_item.event_id() else { - return Err(UnsupportedEditItem::MissingEvent.into()); - }; - - let TimelineItemContent::Poll(_) = edit_item.content() else { - return Err(UnsupportedEditItem::NotPollEvent.into()); - }; - - let content = ReplacementUnstablePollStartEventContent::plain_text( - fallback_text, - poll, - event_id.into(), - ); - - self.send(UnstablePollStartEventContent::from(content).into()).await?; - - Ok(()) - } - /// Toggle a reaction on an event. /// /// The `unique_id` parameter is a string returned by diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index dc62b3e5823..8391f7c2de8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -21,6 +21,7 @@ use eyeball_im::VectorDiff; use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ config::SyncSettings, + room::edit::EditedContent, test_utils::{events::EventFactory, logged_in_client_with_server}, Client, }; @@ -37,12 +38,13 @@ use ruma::{ poll::unstable_start::{ NewUnstablePollStartEventContent, ReplacementUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers, UnstablePollStartContentBlock, + UnstablePollStartEventContent, }, room::message::{ MessageType, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, TextMessageEventContent, }, - AnyTimelineEvent, + AnyMessageLikeEventContent, AnyTimelineEvent, }, room_id, serde::Raw, @@ -233,7 +235,10 @@ async fn test_edit_local_echo() { // Let's edit the local echo. let did_edit = timeline - .edit(item, RoomMessageEventContent::text_plain("hello, world").into()) + .edit( + item, + EditedContent::RoomMessage(RoomMessageEventContent::text_plain("hello, world").into()), + ) .await .unwrap(); @@ -317,7 +322,12 @@ async fn test_send_edit() { .await; timeline - .edit(&hello_world_item, RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!")) + .edit( + &hello_world_item, + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "Hello, Room!", + )), + ) .await .unwrap(); @@ -399,7 +409,12 @@ async fn test_send_reply_edit() { .await; timeline - .edit(&reply_item, RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!")) + .edit( + &reply_item, + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "Hello, Room!", + )), + ) .await .unwrap(); @@ -494,7 +509,16 @@ async fn test_send_edit_poll() { .unwrap(); let edited_poll = UnstablePollStartContentBlock::new("Edited Test".to_owned(), edited_poll_answers); - timeline.edit_poll("poll_fallback_text", edited_poll, &poll_event).await.unwrap(); + timeline + .edit( + &poll_event, + EditedContent::PollStart { + fallback_text: "poll_fallback_text".to_owned(), + new_content: edited_poll, + }, + ) + .await + .unwrap(); // Let the send queue handle the event. yield_now().await; @@ -591,7 +615,12 @@ async fn test_send_edit_when_timeline_is_clear() { .await; timeline - .edit(&hello_world_item, RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!")) + .edit( + &hello_world_item, + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "Hello, Room!", + )), + ) .await .unwrap(); @@ -608,6 +637,117 @@ async fn test_send_edit_when_timeline_is_clear() { server.verify().await; } +#[async_test] +async fn test_edit_local_echo_with_unsupported_content() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + let mounted_send = Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(413).set_body_json(json!({ + "errcode": "M_TOO_LARGE", + }))) + .expect(1) + .mount_as_scoped(&server) + .await; + + timeline.send(RoomMessageEventContent::text_plain("hello, just you").into()).await.unwrap(); + + assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await); + + let item = item.as_event().unwrap(); + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + + assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); + assert!(day_divider.is_day_divider()); + + // We haven't set a route for sending events, so this will fail. + + assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + + let item = item.as_event().unwrap(); + assert!(item.is_local_echo()); + assert!(item.is_editable()); + + assert_matches!( + item.send_state(), + Some(EventSendState::SendingFailed { is_recoverable: false, .. }) + ); + + assert!(timeline_stream.next().now_or_never().is_none()); + + // Set up the success response before editing, since edit causes an immediate + // retry (the room's send queue is not blocked, since the one event it couldn't + // send failed in an unrecoverable way). + drop(mounted_send); + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$1" }))) + .mount(&server) + .await; + + let answers = vec![UnstablePollAnswer::new("A", "Answer A")].try_into().unwrap(); + let poll_content_block = UnstablePollStartContentBlock::new("question", answers); + let poll_start_content = EditedContent::PollStart { + fallback_text: "edited".to_owned(), + new_content: poll_content_block.clone(), + }; + + // Let's edit the local echo (message) with an unsupported type (poll start). + let did_edit = timeline.edit(item, poll_start_content).await.unwrap(); + + // We couldn't edit the local echo, since their content types didn't match + assert!(!did_edit); + + timeline + .send(AnyMessageLikeEventContent::UnstablePollStart(UnstablePollStartEventContent::New( + NewUnstablePollStartEventContent::new(poll_content_block), + ))) + .await + .unwrap(); + + assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await); + + let item = item.as_event().unwrap(); + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + + // Let's edit the local echo (poll start) with an unsupported type (message). + let did_edit = timeline + .edit( + item, + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "edited", + )), + ) + .await + .unwrap(); + + // We couldn't edit the local echo, since their content types didn't match + assert!(!did_edit); +} + struct PendingEditHelper { client: Client, server: MockServer, diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index 45e50f31d97..04b22b1c473 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -19,6 +19,10 @@ use std::future::Future; use matrix_sdk_base::{deserialized_responses::SyncTimelineEvent, SendOutsideWasm}; use ruma::{ events::{ + poll::unstable_start::{ + ReplacementUnstablePollStartEventContent, UnstablePollStartContentBlock, + UnstablePollStartEventContent, + }, room::message::{Relation, ReplacementMetadata, RoomMessageEventContentWithoutRelation}, AnyMessageLikeEvent, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEvent, SyncMessageLikeEvent, @@ -34,6 +38,14 @@ use crate::Room; pub enum EditedContent { /// The content is a `m.room.message`. RoomMessage(RoomMessageEventContentWithoutRelation), + + /// The content is a new poll start. + PollStart { + /// New fallback text for the poll. + fallback_text: String, + /// New start block for the poll. + new_content: UnstablePollStartContentBlock, + }, } #[cfg(not(tarpaulin_include))] @@ -41,6 +53,7 @@ impl std::fmt::Debug for EditedContent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::RoomMessage(_) => f.debug_tuple("RoomMessage").finish(), + Self::PollStart { .. } => f.debug_tuple("PollStart").finish(), } } } @@ -189,12 +202,34 @@ async fn make_edit_event( } }); - return Ok(new_content + Ok(new_content .make_replacement( ReplacementMetadata::new(event_id.to_owned(), mentions), replied_to_original_room_msg.as_ref(), ) - .into()); + .into()) + } + + EditedContent::PollStart { fallback_text, new_content } => { + if !matches!( + message_like_event, + AnySyncMessageLikeEvent::UnstablePollStart(SyncMessageLikeEvent::Original(_)) + ) { + return Err(EditError::IncompatibleEditType { + target: message_like_event.event_type().to_string(), + new_content: "poll start", + }); + } + + let replacement = UnstablePollStartEventContent::Replacement( + ReplacementUnstablePollStartEventContent::plain_text( + fallback_text, + new_content, + event_id.to_owned(), + ), + ); + + Ok(replacement.into()) } } } diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 6b0cc3704c3..82753295ae5 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -61,8 +61,7 @@ use matrix_sdk_base::{ use matrix_sdk_common::executor::{spawn, JoinHandle}; use ruma::{ events::{ - reaction::ReactionEventContent, relation::Annotation, - room::message::RoomMessageEventContentWithoutRelation, AnyMessageLikeEventContent, + reaction::ReactionEventContent, relation::Annotation, AnyMessageLikeEventContent, EventContent as _, }, serde::Raw, @@ -908,30 +907,33 @@ impl QueueStorage { // Check the event is one we know how to edit with an edit event. - // 1. It must be deserializable… - let content = match new_content.deserialize() { - Ok(c) => c, - Err(err) => { - warn!("unable to deserialize: {err}"); + // It must be deserializable… + let edited_content = match new_content.deserialize() { + Ok(AnyMessageLikeEventContent::RoomMessage(c)) => { + // Assume no relationships. + EditedContent::RoomMessage(c.into()) + } + + Ok(AnyMessageLikeEventContent::UnstablePollStart(c)) => { + let poll_start = c.poll_start().clone(); + EditedContent::PollStart { + fallback_text: poll_start.question.text.clone(), + new_content: poll_start, + } + } + + Ok(c) => { + warn!("Unsupported edit content type: {:?}", c.event_type()); return Ok(true); } - }; - // 2. …and a room message, at this point. - let AnyMessageLikeEventContent::RoomMessage(room_message_content) = content - else { - warn!("trying to send an edit event for a non-room message: aborting"); - return Ok(true); + Err(err) => { + warn!("Unable to deserialize: {err}"); + return Ok(true); + } }; - // Assume no relation. - let new_content: RoomMessageEventContentWithoutRelation = - room_message_content.into(); - - let edit_event = match room - .make_edit_event(&event_id, EditedContent::RoomMessage(new_content)) - .await - { + let edit_event = match room.make_edit_event(&event_id, edited_content).await { Ok(e) => e, Err(err) => { warn!("couldn't create edited event: {err}"); diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index 8b32d718e26..36728e29727 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -23,7 +23,10 @@ use ruma::{ poll::{ end::PollEndEventContent, response::{PollResponseEventContent, SelectionsContentBlock}, - start::{PollAnswer, PollContentBlock, PollStartEventContent}, + unstable_start::{ + NewUnstablePollStartEventContent, UnstablePollAnswer, + UnstablePollStartContentBlock, UnstablePollStartEventContent, + }, }, reaction::ReactionEventContent, relation::{Annotation, InReplyTo, Replacement, Thread}, @@ -279,20 +282,19 @@ impl EventFactory { content: impl Into, poll_question: impl Into, answers: Vec>, - ) -> EventBuilder { + ) -> EventBuilder { // PollAnswers 'constructor' is not public, so we need to deserialize them - let answers: Vec = answers + let answers: Vec = answers .into_iter() .enumerate() - .map(|(idx, answer)| { - PollAnswer::new(idx.to_string(), TextContentBlock::plain(answer.into())) - }) + .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer)) .collect(); let poll_answers = answers.try_into().unwrap(); - let poll_start_content = PollStartEventContent::new( - TextContentBlock::plain(content.into()), - PollContentBlock::new(TextContentBlock::plain(poll_question.into()), poll_answers), - ); + let poll_start_content = + UnstablePollStartEventContent::New(NewUnstablePollStartEventContent::plain_text( + content, + UnstablePollStartContentBlock::new(poll_question, poll_answers), + )); self.event(poll_start_content) } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 0837a36fdf6..8e6c6d1c29b 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -25,7 +25,12 @@ use ruma::{ api::MatrixVersion, event_id, events::{ - room::message::RoomMessageEventContent, AnyMessageLikeEventContent, EventContent as _, + poll::unstable_start::{ + NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers, + UnstablePollStartContentBlock, UnstablePollStartEventContent, + }, + room::message::RoomMessageEventContent, + AnyMessageLikeEventContent, EventContent as _, }, room_id, serde::Raw, @@ -841,10 +846,6 @@ async fn test_edit() { let (client, server) = logged_in_client_with_server().await; - // TODO: (#3722) if the event cache isn't available, then making the edit event - // will fail. - client.event_cache().subscribe().unwrap(); - // Mark the room as joined. let room_id = room_id!("!a:b.c"); @@ -962,12 +963,153 @@ async fn test_edit() { } #[async_test] -async fn test_edit_while_being_sent_and_fails() { +async fn test_edit_with_poll_start() { let (client, server) = logged_in_client_with_server().await; - // TODO: (#3722) if the event cache isn't available, then making the edit event - // will fail. - client.event_cache().subscribe().unwrap(); + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + + let room = mock_sync_with_new_room( + |builder| { + builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + }, + &client, + &server, + room_id, + ) + .await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + + assert!(local_echoes.is_empty()); + assert!(watch.is_empty()); + + let lock = Arc::new(Mutex::new(())); + let lock_guard = lock.lock().await; + + let mock_lock = lock.clone(); + + mock_encryption_state(&server, false).await; + + let num_request = std::sync::Mutex::new(1); + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(move |_req: &Request| { + // Wait for the signal from the main thread that we can process this query. + let mock_lock = mock_lock.clone(); + std::thread::spawn(move || { + tokio::runtime::Runtime::new().unwrap().block_on(async { + drop(mock_lock.lock().await); + }); + }) + .join() + .unwrap(); + + let mut num_request = num_request.lock().unwrap(); + + let event_id = format!("${}", *num_request); + *num_request += 1; + + ResponseTemplate::new(200).set_body_json(json!({ + "event_id": event_id, + })) + }) + .named("send_event") + .expect(2) + .mount(&server) + .await; + + // The /event endpoint is used to retrieve the original event, during creation + // of the edit event. + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200).set_body_json( + EventFactory::new() + .poll_start("poll_start", "question", vec!["Answer A"]) + .sender(client.user_id().unwrap()) + .room(room_id) + .into_raw_timeline() + .json(), + ), + ) + .expect(1) + .named("get_event") + .mount(&server) + .await; + + let poll_answers: UnstablePollAnswers = + vec![UnstablePollAnswer::new("A", "Answer A")].try_into().unwrap(); + let poll_start_block = UnstablePollStartContentBlock::new("question", poll_answers); + let poll_start_content = UnstablePollStartEventContent::New( + NewUnstablePollStartEventContent::plain_text("poll_start", poll_start_block), + ); + let handle = q.send(poll_start_content.into()).await.unwrap(); + + // Receiving updates for local echoes. + assert_let!( + Ok(Ok(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + content: LocalEchoContent::Event { + serialized_event, + // New local echoes should always start as not wedged. + is_wedged: false, + .. + }, + transaction_id: txn1, + }))) = timeout(Duration::from_secs(1), watch.recv()).await + ); + + let content = serialized_event.deserialize().unwrap(); + assert_let!(AnyMessageLikeEventContent::UnstablePollStart(_) = content); + assert!(watch.is_empty()); + + // Let the background task start now. + tokio::task::yield_now().await; + + // Edit the poll start event + let poll_answers: UnstablePollAnswers = + vec![UnstablePollAnswer::new("B", "Answer B")].try_into().unwrap(); + let poll_start_block = UnstablePollStartContentBlock::new("edited question", poll_answers); + let poll_start_content = UnstablePollStartEventContent::New( + NewUnstablePollStartEventContent::plain_text("poll_start (edited)", poll_start_block), + ); + assert!(handle.edit(poll_start_content.into()).await.unwrap()); + + assert_let!( + Ok(Ok(RoomSendQueueUpdate::ReplacedLocalEvent { + transaction_id: new_txn1, + new_content: serialized_event, + })) = timeout(Duration::from_secs(1), watch.recv()).await + ); + let content = serialized_event.deserialize().unwrap(); + assert_let!( + AnyMessageLikeEventContent::UnstablePollStart(UnstablePollStartEventContent::New( + poll_start + )) = content + ); + assert_eq!(poll_start.text.unwrap(), "poll_start (edited)"); + assert_eq!(txn1, new_txn1); + assert!(watch.is_empty()); + + // Let the server process the responses. + drop(lock_guard); + + // Now the server will process the events in order. + assert_update!(watch => sent { txn = txn1, }); + + // Let a bit of time to process the edit event sent to the server for txn1. + assert_update!(watch => sent {}); + + assert!(watch.is_empty()); +} + +#[async_test] +async fn test_edit_while_being_sent_and_fails() { + let (client, server) = logged_in_client_with_server().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 38b96ca3d52..19feb552055 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -23,6 +23,7 @@ use futures::pin_mut; use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ encryption::{backups::BackupState, EncryptionSettings}, + room::edit::EditedContent, ruma::{ api::client::room::create_room::v3::Request as CreateRoomRequest, events::{ @@ -289,7 +290,10 @@ async fn test_stale_local_echo_time_abort_edit() { // Now do a crime: try to edit the local echo. let did_edit = timeline - .edit(&local_echo, RoomMessageEventContent::text_plain("bonjour").into()) + .edit( + &local_echo, + EditedContent::RoomMessage(RoomMessageEventContent::text_plain("bonjour").into()), + ) .await .unwrap(); From cc0dfd62e7a08c53913b3d8989c8b18277f52d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 11 Sep 2024 10:40:31 +0200 Subject: [PATCH 089/979] sdk: notify when a sync response is received by SlidingSync Previously this was only done in `Client::sync_once`, which made `ClientInner::sync_beat` not that useful. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 57 +++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index c767709582d..5e0e7e43cb1 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -710,7 +710,7 @@ impl SlidingSync { // The code manipulates `Request` and `Response` from MSC4186 because it's // the future standard (at the time of writing: 2024-09-09). Let's check if // the generated request must be transformed into an MSC3575 `Request`. - if !self.inner.version.is_native() { + let result = if !self.inner.version.is_native() { self.send_sync_request( Into::::into(request), request_config, @@ -719,7 +719,12 @@ impl SlidingSync { .await } else { self.send_sync_request(request, request_config, position_guard).await - } + }; + + // Notify a new sync was received + self.inner.client.inner.sync_beat.notify(usize::MAX); + + result } /// Create a _new_ Sliding Sync sync loop. @@ -1095,7 +1100,7 @@ mod tests { use assert_matches::assert_matches; use futures_util::{future::join_all, pin_mut, StreamExt}; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timeout::timeout}; use matrix_sdk_test::async_test; use ruma::{ api::client::error::ErrorKind, assign, owned_room_id, room_id, serde::Raw, uint, @@ -3073,4 +3078,50 @@ mod tests { Ok(()) } + + #[async_test] + async fn test_sync_beat_is_notified_on_sync_response() -> Result<()> { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + let pos = Arc::new(Mutex::new(0)); + let _mock_guard = Mock::given(SlidingSyncMatcher) + .respond_with(move |_: &Request| { + let mut pos = pos.lock().unwrap(); + *pos += 1; + ResponseTemplate::new(200).set_body_json(json!({ + "pos": pos.to_string(), + "lists": {}, + "rooms": {} + })) + }) + .mount_as_scoped(&server) + .await; + + let sliding_sync = client + .sliding_sync("test")? + .with_to_device_extension( + assign!(http::request::ToDevice::default(), { enabled: Some(true)}), + ) + .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)})) + .build() + .await?; + + let sliding_sync = Arc::new(sliding_sync); + + assert!(!client.inner.sync_beat.is_notified()); + + // Create the listener and perform a sync request + let sync_beat_listener = client.inner.sync_beat.listen(); + sliding_sync.sync_once().await?; + + // The sync beat listener should be notified shortly after + timeout(sync_beat_listener, Duration::from_millis(20)) + .await + .expect("Sync beat wasn't received in time"); + + assert!(client.inner.sync_beat.is_notified()); + + Ok(()) + } } From 4b970e879f3f9cdb0ded2b01b4b00a1692c66f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 11 Sep 2024 11:43:16 +0200 Subject: [PATCH 090/979] sdk: ensure `sync_beat` is only notified with a successful sync response --- crates/matrix-sdk/src/sliding_sync/mod.rs | 63 +++++++++++++++-------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 5e0e7e43cb1..ba271019f95 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -710,21 +710,21 @@ impl SlidingSync { // The code manipulates `Request` and `Response` from MSC4186 because it's // the future standard (at the time of writing: 2024-09-09). Let's check if // the generated request must be transformed into an MSC3575 `Request`. - let result = if !self.inner.version.is_native() { + let summaries = if !self.inner.version.is_native() { self.send_sync_request( Into::::into(request), request_config, position_guard, ) - .await + .await? } else { - self.send_sync_request(request, request_config, position_guard).await + self.send_sync_request(request, request_config, position_guard).await? }; // Notify a new sync was received self.inner.client.inner.sync_beat.notify(usize::MAX); - result + Ok(summaries) } /// Create a _new_ Sliding Sync sync loop. @@ -1099,8 +1099,9 @@ mod tests { }; use assert_matches::assert_matches; + use event_listener::Listener; use futures_util::{future::join_all, pin_mut, StreamExt}; - use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timeout::timeout}; + use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::async_test; use ruma::{ api::client::error::ErrorKind, assign, owned_room_id, room_id, serde::Raw, uint, @@ -3084,17 +3085,12 @@ mod tests { let server = MockServer::start().await; let client = logged_in_client(Some(server.uri())).await; - let pos = Arc::new(Mutex::new(0)); let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(move |_: &Request| { - let mut pos = pos.lock().unwrap(); - *pos += 1; - ResponseTemplate::new(200).set_body_json(json!({ - "pos": pos.to_string(), - "lists": {}, - "rooms": {} - })) - }) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "pos": "0", + "lists": {}, + "rooms": {} + }))) .mount_as_scoped(&server) .await; @@ -3109,18 +3105,43 @@ mod tests { let sliding_sync = Arc::new(sliding_sync); - assert!(!client.inner.sync_beat.is_notified()); - // Create the listener and perform a sync request let sync_beat_listener = client.inner.sync_beat.listen(); sliding_sync.sync_once().await?; // The sync beat listener should be notified shortly after - timeout(sync_beat_listener, Duration::from_millis(20)) - .await - .expect("Sync beat wasn't received in time"); + assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_some()); + Ok(()) + } + + #[async_test] + async fn test_sync_beat_is_not_notified_on_sync_failure() -> Result<()> { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + let _mock_guard = Mock::given(SlidingSyncMatcher) + .respond_with(ResponseTemplate::new(404)) + .mount_as_scoped(&server) + .await; + + let sliding_sync = client + .sliding_sync("test")? + .with_to_device_extension( + assign!(http::request::ToDevice::default(), { enabled: Some(true)}), + ) + .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)})) + .build() + .await?; + + let sliding_sync = Arc::new(sliding_sync); + + // Create the listener and perform a sync request + let sync_beat_listener = client.inner.sync_beat.listen(); + let sync_result = sliding_sync.sync_once().await; + assert!(sync_result.is_err()); - assert!(client.inner.sync_beat.is_notified()); + // The sync beat listener won't be notified in this case + assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_none()); Ok(()) } From 47c7d0549909f3bd437fb40a51b183bc5c839932 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 11 Sep 2024 11:38:59 +0200 Subject: [PATCH 091/979] feat(base): Add `Room::creator()`. This patch adds `Room::creator()` to expose the value from `RoomInfo::creator()`. --- crates/matrix-sdk-base/src/rooms/normal.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 0d08370cd8c..2ca0fffd39e 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -120,8 +120,12 @@ impl Default for RoomInfoNotableUpdateReasons { /// invited rooms. #[derive(Debug, Clone)] pub struct Room { + /// The room ID. room_id: OwnedRoomId, + + /// Our own user ID. own_user_id: OwnedUserId, + inner: SharedObservable, room_info_notable_update_sender: broadcast::Sender, store: Arc, @@ -253,6 +257,11 @@ impl Room { &self.room_id } + /// Get a copy of the room creator. + pub fn creator(&self) -> Option { + self.inner.read().creator().map(ToOwned::to_owned) + } + /// Get our own user id. pub fn own_user_id(&self) -> &UserId { &self.own_user_id From 075f3fa9d20e0ce50fc4f2421fffae3732661257 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 11 Sep 2024 11:39:54 +0200 Subject: [PATCH 092/979] feat(ffi): Add `RoomInfo::creator`. This patch adds the `matrix_sdk_ffi::RoomInfo::creator` field that simply copies the value from `matrix_sdk_base::Room::creator()`. --- bindings/matrix-sdk-ffi/src/room_info.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/room_info.rs b/bindings/matrix-sdk-ffi/src/room_info.rs index ab216ab11bf..bbc84cfe97e 100644 --- a/bindings/matrix-sdk-ffi/src/room_info.rs +++ b/bindings/matrix-sdk-ffi/src/room_info.rs @@ -11,6 +11,7 @@ use crate::{ #[derive(uniffi::Record)] pub struct RoomInfo { id: String, + creator: Option, /// The room's name from the room state event if received from sync, or one /// that's been computed otherwise. display_name: Option, @@ -70,6 +71,7 @@ impl RoomInfo { Ok(Self { id: room.room_id().to_string(), + creator: room.creator().as_ref().map(ToString::to_string), display_name: room.cached_display_name().map(|name| name.to_string()), raw_name: room.name(), topic: room.topic(), From 2b3ad86869525971513faf1f349764100bfcdb4d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 11 Sep 2024 15:50:12 +0200 Subject: [PATCH 093/979] feat(ui): `all_rooms` in `RoomListService` requires `m.room.canonical_name`. This patch adds `m.room.canonical_name` in the `required_state` of the `all_rooms` list defined by `RoomListService`. This is useful to better compute the room name in a more robust way. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 1 + crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 24680a12402..1b5a57a3a96 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -150,6 +150,7 @@ impl RoomListService { (StateEventType::RoomMember, "$LAZY".to_owned()), (StateEventType::RoomMember, "$ME".to_owned()), (StateEventType::RoomName, "".to_owned()), + (StateEventType::RoomCanonicalAlias, "".to_owned()), (StateEventType::RoomPowerLevels, "".to_owned()), ]) .include_heroes(Some(true)) 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 ce543093c20..c4711d35221 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -327,6 +327,7 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.member", "$LAZY"], ["m.room.member", "$ME"], ["m.room.name", ""], + ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], ], "include_heroes": true, From 3555474cad1db56de3a70095878776eec994fd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 10 Sep 2024 13:37:19 +0200 Subject: [PATCH 094/979] crypto: Bump the vodozemac version and remove the PkEncryption compat module The PkEncryption support now lives inside of vodozemac so no need to keep our own copy around. --- Cargo.lock | 45 +-- Cargo.toml | 2 +- .../src/backups/keys/backup.rs | 4 +- .../src/backups/keys/compat.rs | 289 ------------------ .../src/backups/keys/decryption.rs | 31 +- .../matrix-sdk-crypto/src/backups/keys/mod.rs | 4 +- crates/matrix-sdk-crypto/src/olm/utility.rs | 2 +- 7 files changed, 30 insertions(+), 347 deletions(-) delete mode 100644 crates/matrix-sdk-crypto/src/backups/keys/compat.rs diff --git a/Cargo.lock b/Cargo.lock index 40bad7c3807..b993264efa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,23 +1378,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", - "der_derive", - "flagset", "pem-rfc7468", "zeroize", ] -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "deranged" version = "0.3.11" @@ -1887,12 +1874,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "flagset" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" - [[package]] name = "flate2" version = "1.0.30" @@ -4282,17 +4263,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkcs7" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d79178be066405e0602bf3035946edef6b11b3f9dde46dfe5f8bfd7dea4b77e7" -dependencies = [ - "der", - "spki", - "x509-cert", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -6669,8 +6639,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vodozemac" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051d4af70b53b42adf2aac459a305851b8d754f210aaf11ab509e1065beff422" +source = "git+https://github.com/matrix-org/vodozemac?rev=57cbf7e939d7b54d20207e8361b7135bd65c9cc2#57cbf7e939d7b54d20207e8361b7135bd65c9cc2" dependencies = [ "aes", "arrayvec", @@ -6684,7 +6653,6 @@ dependencies = [ "hkdf", "hmac", "matrix-pickle", - "pkcs7", "prost", "rand", "serde", @@ -7128,17 +7096,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x509-cert" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" -dependencies = [ - "const-oid", - "der", - "spki", -] - [[package]] name = "xshell" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index 4fb7a170fbb..cf07a01a28a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ tracing-subscriber = "0.3.18" uniffi = { version = "0.28.0" } uniffi_bindgen = { version = "0.28.0" } url = "2.5.0" -vodozemac = { version = "0.7.0" } +vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "57cbf7e939d7b54d20207e8361b7135bd65c9cc2", features = ["insecure-pk-encryption"] } wiremock = "0.6.0" zeroize = "1.6.0" diff --git a/crates/matrix-sdk-crypto/src/backups/keys/backup.rs b/crates/matrix-sdk-crypto/src/backups/keys/backup.rs index 7f19135bc4f..83409d950b6 100644 --- a/crates/matrix-sdk-crypto/src/backups/keys/backup.rs +++ b/crates/matrix-sdk-crypto/src/backups/keys/backup.rs @@ -18,10 +18,10 @@ use ruma::{ api::client::backup::{EncryptedSessionDataInit, KeyBackupData, KeyBackupDataInit}, serde::Base64, }; -use vodozemac::Curve25519PublicKey; +use vodozemac::{pk_encryption::PkEncryption, Curve25519PublicKey}; use zeroize::Zeroizing; -use super::{compat::PkEncryption, decryption::DecodeError}; +use super::decryption::DecodeError; use crate::{olm::InboundGroupSession, types::Signatures}; #[derive(Debug)] diff --git a/crates/matrix-sdk-crypto/src/backups/keys/compat.rs b/crates/matrix-sdk-crypto/src/backups/keys/compat.rs deleted file mode 100644 index 59c32e6ad41..00000000000 --- a/crates/matrix-sdk-crypto/src/backups/keys/compat.rs +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! ☣️ Compat support for Olm's PkEncryption and PkDecryption -use aes::{ - cipher::{ - block_padding::{Pkcs7, UnpadError}, - generic_array::GenericArray, - BlockDecryptMut, BlockEncryptMut, IvSizeUser, KeyIvInit, KeySizeUser, - }, - Aes256, -}; -use hkdf::Hkdf; -use hmac::{digest::MacError, Hmac, Mac as MacT}; -use sha2::Sha256; -use thiserror::Error; -use vodozemac::{base64_decode, Curve25519PublicKey, Curve25519SecretKey, KeyError, SharedSecret}; - -type Aes256CbcEnc = cbc::Encryptor; -type Aes256CbcDec = cbc::Decryptor; -type HmacSha256 = Hmac; - -type Aes256Key = GenericArray::KeySize>; -type Aes256Iv = GenericArray::IvSize>; -type HmacSha256Key<'a> = &'a [u8; 32]; - -const MAC_LENGTH: usize = 8; - -pub struct PkDecryption { - key: Curve25519SecretKey, - public_key: Curve25519PublicKey, -} - -struct Keys { - aes_key: Box<[u8; 32]>, - mac_key: Box<[u8; 32]>, - iv: Box<[u8; 16]>, -} - -impl Keys { - fn new(shared_secret: SharedSecret) -> Self { - let mut expanded_keys = Box::new([0u8; 80]); - - let salt = [0u8; 32]; - let hkdf: Hkdf = Hkdf::new(Some(&salt), shared_secret.as_bytes()); - - hkdf.expand(b"", &mut *expanded_keys) - .expect("We should be able to expand the shared secret into 80 bytes"); - - let mut aes_key = Box::new([0u8; 32]); - let mut mac_key = Box::new([0u8; 32]); - let mut iv = Box::new([0u8; 16]); - - aes_key.copy_from_slice(&expanded_keys[0..32]); - mac_key.copy_from_slice(&expanded_keys[32..64]); - iv.copy_from_slice(&expanded_keys[64..80]); - - Self { aes_key, mac_key, iv } - } - - fn aes_key(&self) -> &Aes256Key { - Aes256Key::from_slice(self.aes_key.as_slice()) - } - - fn iv(&self) -> &Aes256Iv { - Aes256Iv::from_slice(self.iv.as_slice()) - } - - fn mac_key(&self) -> HmacSha256Key<'_> { - &self.mac_key - } - - fn hmac(&self) -> HmacSha256 { - let hmac = HmacSha256::new_from_slice(self.mac_key()) - .expect("We should be able to create a Hmac object from a 32 byte key"); - - hmac - } -} - -impl PkDecryption { - pub fn new() -> Self { - let key = Curve25519SecretKey::new(); - let public_key = Curve25519PublicKey::from(&key); - - Self { key, public_key } - } - - pub fn public_key(&self) -> Curve25519PublicKey { - self.public_key - } - - pub fn decrypt(&self, message: &Message) -> Result, Error> { - let shared_secret = self.key.diffie_hellman(&message.ephemeral_key); - - let keys = Keys::new(shared_secret); - - let hmac = keys.hmac(); - hmac.verify_truncated_left(&message.mac)?; - - let cipher = Aes256CbcDec::new(keys.aes_key(), keys.iv()); - let decrypted = cipher.decrypt_padded_vec_mut::(&message.ciphertext)?; - - Ok(decrypted) - } - - pub fn from_bytes(bytes: &[u8; 32]) -> Self { - let key = Curve25519SecretKey::from_slice(bytes); - let public_key = Curve25519PublicKey::from(&key); - - Self { key, public_key } - } -} - -impl Default for PkDecryption { - fn default() -> Self { - Self::new() - } -} - -pub struct PkEncryption { - public_key: Curve25519PublicKey, -} - -impl From<&PkDecryption> for PkEncryption { - fn from(value: &PkDecryption) -> Self { - Self::from(value.public_key()) - } -} - -impl From for PkEncryption { - fn from(public_key: Curve25519PublicKey) -> Self { - Self { public_key } - } -} - -impl PkEncryption { - pub fn from_key(public_key: Curve25519PublicKey) -> Self { - Self { public_key } - } - - pub fn encrypt(&self, message: &[u8]) -> Message { - let ephemeral_key = Curve25519SecretKey::new(); - let shared_secret = ephemeral_key.diffie_hellman(&self.public_key); - let keys = Keys::new(shared_secret); - - let cipher = Aes256CbcEnc::new(keys.aes_key(), keys.iv()); - let ciphertext = cipher.encrypt_padded_vec_mut::(message); - - let hmac = keys.hmac(); - let mut mac = hmac.finalize().into_bytes().to_vec(); - mac.truncate(MAC_LENGTH); - - Message { ciphertext, mac, ephemeral_key: Curve25519PublicKey::from(&ephemeral_key) } - } -} - -#[derive(Debug, Error)] -pub enum MessageDecodeError { - #[error(transparent)] - Base64(#[from] vodozemac::Base64DecodeError), - #[error(transparent)] - Key(#[from] KeyError), -} - -#[derive(Debug)] -pub struct Message { - pub ciphertext: Vec, - pub mac: Vec, - pub ephemeral_key: Curve25519PublicKey, -} - -impl Message { - pub fn from_base64( - ciphertext: &str, - mac: &str, - ephemeral_key: &str, - ) -> Result { - Ok(Self { - ciphertext: base64_decode(ciphertext)?, - mac: base64_decode(mac)?, - ephemeral_key: Curve25519PublicKey::from_base64(ephemeral_key)?, - }) - } -} - -/// Error type describing the failure cases the Pk decryption step can have. -#[derive(Debug, Error)] -pub enum Error { - /// The message has invalid PKCS7 padding. - #[error("Failed decrypting, invalid padding: {0}")] - InvalidPadding(#[from] UnpadError), - /// The message failed to be authenticated. - #[error("The MAC of the ciphertext didn't pass validation {0}")] - Mac(#[from] MacError), - /// The message failed to be decoded. - #[error("The message could not been decoded: {0}")] - Decoding(#[from] MessageDecodeError), - /// The message's Curve25519 key failed to be decoded. - #[error("The message's ephemeral Curve25519 key could not been decoded: {0}")] - InvalidCurveKey(#[from] KeyError), - /// The decrypted message should contain a backed up room key, but the - /// plaintext isn't valid JSON. - #[error("The decrypted message isn't valid JSON: {0}")] - Json(#[from] serde_json::error::Error), -} - -#[cfg(test)] -mod tests { - use olm_rs::pk::{OlmPkDecryption, OlmPkEncryption, PkMessage}; - use vodozemac::{base64_encode, Curve25519PublicKey}; - - use super::{Message, MessageDecodeError, PkDecryption, PkEncryption}; - - impl TryFrom for Message { - type Error = MessageDecodeError; - - fn try_from(value: PkMessage) -> Result { - Self::from_base64(&value.ciphertext, &value.mac, &value.ephemeral_key) - } - } - - impl From for PkMessage { - fn from(val: Message) -> Self { - PkMessage { - ciphertext: base64_encode(val.ciphertext), - mac: base64_encode(val.mac), - ephemeral_key: val.ephemeral_key.to_base64(), - } - } - } - - #[test] - fn decrypt() { - let decryptor = PkDecryption::new(); - let public_key = decryptor.public_key(); - let encryptor = OlmPkEncryption::new(&public_key.to_base64()); - - let message = "It's a secret to everybody"; - - let encrypted = encryptor.encrypt(message); - let encrypted = encrypted.try_into().unwrap(); - - let decrypted = decryptor.decrypt(&encrypted).unwrap(); - - assert_eq!(message.as_bytes(), decrypted); - } - - #[test] - fn encrypt() { - let decryptor = OlmPkDecryption::new(); - let public_key = Curve25519PublicKey::from_base64(decryptor.public_key()).unwrap(); - let encryptor = PkEncryption::from_key(public_key); - - let message = "It's a secret to everybody"; - - let encrypted = encryptor.encrypt(message.as_ref()); - let encrypted = encrypted.into(); - - let decrypted = decryptor.decrypt(encrypted).unwrap(); - - assert_eq!(message, decrypted); - } - - #[test] - fn encrypt_native() { - let decryptor = PkDecryption::new(); - let public_key = decryptor.public_key(); - let encryptor = PkEncryption::from_key(public_key); - - let message = "It's a secret to everybody"; - - let encrypted = encryptor.encrypt(message.as_ref()); - let decrypted = decryptor.decrypt(&encrypted).unwrap(); - - assert_eq!(message.as_ref(), decrypted); - } -} diff --git a/crates/matrix-sdk-crypto/src/backups/keys/decryption.rs b/crates/matrix-sdk-crypto/src/backups/keys/decryption.rs index 5d218afd058..28cfa6e02f4 100644 --- a/crates/matrix-sdk-crypto/src/backups/keys/decryption.rs +++ b/crates/matrix-sdk-crypto/src/backups/keys/decryption.rs @@ -19,13 +19,13 @@ use std::{ use ruma::api::client::backup::EncryptedSessionData; use thiserror::Error; -use vodozemac::Curve25519PublicKey; +use vodozemac::{ + pk_encryption::{Message, PkDecryption}, + Curve25519PublicKey, Curve25519SecretKey, +}; use zeroize::{Zeroize, Zeroizing}; -use super::{ - compat::{Error as DecryptionError, Message, PkDecryption}, - MegolmV1BackupKey, -}; +use super::MegolmV1BackupKey; use crate::{ olm::BackedUpRoomKey, store::BackupDecryptionKey, @@ -58,6 +58,21 @@ pub enum DecodeError { PublicKey(#[from] vodozemac::KeyError), } +/// Error type describing the failure cases the Pk decryption step can have. +#[derive(Debug, Error)] +pub enum DecryptionError { + /// The message failed to decrypt. + #[error("The MAC of the ciphertext didn't pass validation {0}")] + Encryption(#[from] vodozemac::pk_encryption::Error), + /// The message failed to be decoded. + #[error("The message could not been decoded: {0}")] + Decoding(#[from] vodozemac::pk_encryption::MessageDecodeError), + /// The decrypted message should contain a backed up room key, but the + /// plaintext isn't valid JSON. + #[error("The decrypted message isn't valid JSON: {0}")] + Json(#[from] serde_json::error::Error), +} + impl TryFrom for BackupDecryptionKey { type Error = DecodeError; @@ -173,7 +188,8 @@ impl BackupDecryptionKey { } fn get_pk_decryption(&self) -> PkDecryption { - PkDecryption::from_bytes(self.inner.as_ref()) + let secret_key = Curve25519SecretKey::from_slice(self.inner.as_ref()); + PkDecryption::from_key(secret_key) } /// Extract the megolm.v1 public key from this [`BackupDecryptionKey`]. @@ -223,7 +239,8 @@ impl BackupDecryptionKey { let message = Message { ciphertext: session_data.ciphertext.into_inner(), mac: session_data.mac.into_inner(), - ephemeral_key: Curve25519PublicKey::from_slice(session_data.ephemeral.as_bytes())?, + ephemeral_key: Curve25519PublicKey::from_slice(session_data.ephemeral.as_bytes()) + .map_err(vodozemac::pk_encryption::MessageDecodeError::from)?, }; let pk = self.get_pk_decryption(); diff --git a/crates/matrix-sdk-crypto/src/backups/keys/mod.rs b/crates/matrix-sdk-crypto/src/backups/keys/mod.rs index 5f80cadc181..a197f30f520 100644 --- a/crates/matrix-sdk-crypto/src/backups/keys/mod.rs +++ b/crates/matrix-sdk-crypto/src/backups/keys/mod.rs @@ -48,9 +48,7 @@ //! the `/room_keys/version` API endpoint. mod backup; -mod compat; mod decryption; pub use backup::MegolmV1BackupKey; -pub use compat::Error as DecryptionError; -pub use decryption::DecodeError; +pub use decryption::{DecodeError, DecryptionError}; diff --git a/crates/matrix-sdk-crypto/src/olm/utility.rs b/crates/matrix-sdk-crypto/src/olm/utility.rs index 8e2ef9c11ac..27590b98c94 100644 --- a/crates/matrix-sdk-crypto/src/olm/utility.rs +++ b/crates/matrix-sdk-crypto/src/olm/utility.rs @@ -39,7 +39,7 @@ impl SignJson for Account { fn sign_json(&self, value: Value) -> Result { let serialized = to_signable_json(value)?; - Ok(self.sign(serialized.as_ref())) + Ok(self.sign(serialized)) } } From a024c010ce70be114feabb6397b60987ae1d183d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 11 Sep 2024 20:34:16 +0200 Subject: [PATCH 095/979] chore: Remove the olm-rs dep now that PkEncryption stuff has moved to vodozemac --- Cargo.lock | 33 ----------------------------- crates/matrix-sdk-crypto/Cargo.toml | 5 ----- 2 files changed, 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b993264efa6..b1296d78339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -906,15 +906,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - [[package]] name = "color-eyre" version = "0.6.3" @@ -3261,7 +3252,6 @@ dependencies = [ "matrix-sdk-common", "matrix-sdk-qrcode", "matrix-sdk-test", - "olm-rs", "pbkdf2", "proptest", "rand", @@ -3871,29 +3861,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "olm-rs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6c2c7054110ce4d7b4756d7b7fe507fea9413968ad0ef8f1d043d504aec725" -dependencies = [ - "getrandom", - "olm-sys", - "serde", - "serde_json", - "zeroize", -] - -[[package]] -name = "olm-sys" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2afecf25624989021f9f0f157f7152102fd147b89445d08449739f216002d339" -dependencies = [ - "cmake", - "fs_extra", -] - [[package]] name = "once_cell" version = "1.19.0" diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index b776e7a73fe..13923b1c95f 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -81,11 +81,6 @@ futures-executor = { workspace = true } http = { workspace = true } indoc = "2.0.1" matrix-sdk-test = { workspace = true } - -# libolm is deprecated. We use it only in tests, to ensure that our -# implementation of `PkEncryption` is compatible with that in libolm. -olm-rs = { version = "2.2.0", features = ["serde"] } - proptest = { version = "1.0.0", default-features = false, features = ["std"] } similar-asserts = "1.5.0" # required for async_test macro From 9e7ab635c687a8de394423b75d90db4bee76e78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 12 Sep 2024 11:54:46 +0200 Subject: [PATCH 096/979] bindings: Expose the PkEncryption stuff in the crypto crate bindings (#3971) --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 5e297f62880..769c7876fef 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -925,6 +925,63 @@ fn vodozemac_version() -> String { vodozemac::VERSION.to_owned() } +/// The encryption component of PkEncryption support. +/// +/// This struct can be created using a [`Curve25519PublicKey`] corresponding to +/// a `PkDecryption` object, allowing messages to be encrypted for the +/// associated decryption object. +#[derive(uniffi::Object)] +pub struct PkEncryption { + inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption, +} + +#[uniffi::export] +impl PkEncryption { + /// Create a new [`PkEncryption`] object from a `Curve25519PublicKey` + /// encoded as Base64. + /// + /// The public key should come from an existing `PkDecryption` object. + /// Returns a `DecodeError` if the Curve25519 key could not be decoded + /// correctly. + #[uniffi::constructor] + pub fn from_base64(key: &str) -> Result, DecodeError> { + let key = vodozemac::Curve25519PublicKey::from_base64(key) + .map_err(matrix_sdk_crypto::backups::DecodeError::PublicKey)?; + let inner = vodozemac::pk_encryption::PkEncryption::from_key(key); + + Ok(Self { inner }.into()) + } + + /// Encrypt a message using this [`PkEncryption`] object. + pub fn encrypt(&self, plaintext: &str) -> PkMessage { + use vodozemac::base64_encode; + + let message = self.inner.encrypt(plaintext.as_ref()); + + let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message; + + PkMessage { + ciphertext: base64_encode(ciphertext), + mac: base64_encode(mac), + ephemeral_key: ephemeral_key.to_base64(), + } + } +} + +/// A message that was encrypted using a [`PkEncryption`] object. +#[derive(uniffi::Record)] +pub struct PkMessage { + /// The ciphertext of the message. + pub ciphertext: String, + /// The message authentication code of the message. + /// + /// *Warning*: This does not authenticate the ciphertext. + pub mac: String, + /// The ephemeral Curve25519 key of the message which was used to derive the + /// individual message key. + pub ephemeral_key: String, +} + uniffi::setup_scaffolding!(); #[cfg(test)] From f8961a43825a10c56750a6f13dd253b7083c72b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 11 Sep 2024 14:11:02 +0200 Subject: [PATCH 097/979] sdk-base: add `Room::is_state_partially_or_fully_synced()` This new fn is used to check when a room is at least partially synced, which seems to be the case with SSS. --- crates/matrix-sdk-base/src/rooms/normal.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 2ca0fffd39e..07de9c6cb8d 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -347,6 +347,13 @@ impl Room { self.inner.read().sync_info == SyncInfo::FullySynced } + /// Check if the room state has been at least partially synced. + /// + /// See [`Room::is_state_fully_synced`] for more info. + pub fn is_state_partially_or_fully_synced(&self) -> bool { + self.inner.read().sync_info != SyncInfo::NoState + } + /// Check if the room has its encryption event synced. /// /// The encryption event can be missing when the room hasn't appeared in From bbe16db94ccfa1a074b850cd17d2db050b75b31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 11 Sep 2024 14:12:27 +0200 Subject: [PATCH 098/979] sdk: add `Client::await_room_remote_echo(&room_id)` This fn will loop until it finds an at least partially synced room with the given id. It uses the `ClientInner::sync_beat` listener to wait until the next check is needed. --- crates/matrix-sdk/src/client/mod.rs | 154 +++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 729d5f73aed..9ce92b31fa0 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2240,6 +2240,28 @@ impl Client { // SAFETY: always initialized in the `Client` ctor. self.inner.event_cache.get().unwrap() } + + /// Waits until an at least partially synced room is received, and returns + /// it. + /// + /// **Note: this function will loop endlessly until either it finds the room + /// or an externally set timeout happens.** + pub async fn await_room_remote_echo(&self, room_id: &RoomId) -> Room { + loop { + if let Some(room) = self.get_room(room_id) { + if room.is_state_partially_or_fully_synced() { + debug!("Found just created room!"); + return room; + } else { + warn!("Room wasn't partially synced, waiting for sync beat to try again"); + } + } else { + warn!("Room wasn't found, waiting for sync beat to try again"); + } + self.inner.sync_beat.listen().await; + debug!("New sync beat found"); + } + } } /// A weak reference to the inner client, useful when trying to get a handle @@ -2300,12 +2322,15 @@ pub(crate) mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); use ruma::{ - api::MatrixVersion, events::ignored_user_list::IgnoredUserListEventContent, owned_room_id, - room_id, RoomId, ServerName, UserId, + api::{client::room::create_room::v3::Request as CreateRoomRequest, MatrixVersion}, + assign, + events::ignored_user_list::IgnoredUserListEventContent, + owned_room_id, room_id, RoomId, ServerName, UserId, }; + use serde_json::json; use url::Url; use wiremock::{ - matchers::{body_json, header, method, path}, + matchers::{body_json, header, method, path, query_param_is_missing}, Mock, MockServer, ResponseTemplate, }; @@ -2315,6 +2340,7 @@ pub(crate) mod tests { config::{RequestConfig, SyncSettings}, test_utils::{ logged_in_client, no_retry_test_client, set_client_session, test_client_builder, + test_client_builder_with_server, }, Error, }; @@ -2773,4 +2799,126 @@ pub(crate) mod tests { // network error. client.whoami().await.unwrap_err(); } + + #[async_test] + async fn test_await_room_remote_echo_returns_the_room_if_it_was_already_synced() { + let (client_builder, server) = test_client_builder_with_server().await; + let client = client_builder.request_config(RequestConfig::new()).build().await.unwrap(); + set_client_session(&client).await; + + let builder = Mock::given(method("GET")) + .and(path("/_matrix/client/r0/sync")) + .and(header("authorization", "Bearer 1234")) + .and(query_param_is_missing("since")); + + let room_id = room_id!("!room:example.org"); + let joined_room_builder = JoinedRoomBuilder::new(room_id); + let mut sync_response_builder = SyncResponseBuilder::new(); + sync_response_builder.add_joined_room(joined_room_builder); + let response_body = sync_response_builder.build_json_sync_response(); + + builder + .respond_with(ResponseTemplate::new(200).set_body_json(response_body)) + .mount(&server) + .await; + + client.sync_once(SyncSettings::default()).await.unwrap(); + + let room = + tokio::time::timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)) + .await + .unwrap(); + assert_eq!(room.room_id(), room_id); + } + + #[async_test] + async fn test_await_room_remote_echo_returns_the_room_when_it_is_ready() { + let (client_builder, server) = test_client_builder_with_server().await; + let client = client_builder.request_config(RequestConfig::new()).build().await.unwrap(); + set_client_session(&client).await; + + let builder = Mock::given(method("GET")) + .and(path("/_matrix/client/r0/sync")) + .and(header("authorization", "Bearer 1234")) + .and(query_param_is_missing("since")); + + let room_id = room_id!("!room:example.org"); + let joined_room_builder = JoinedRoomBuilder::new(room_id); + let mut sync_response_builder = SyncResponseBuilder::new(); + sync_response_builder.add_joined_room(joined_room_builder); + let response_body = sync_response_builder.build_json_sync_response(); + + builder + .respond_with(ResponseTemplate::new(200).set_body_json(response_body)) + .mount(&server) + .await; + + let client = Arc::new(client); + + // Perform the /sync request with a delay so it starts after the + // `await_room_remote_echo` call has happened + tokio::spawn({ + let client = client.clone(); + async move { + tokio::time::sleep(Duration::from_millis(100)).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + } + }); + + let room = + tokio::time::timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)) + .await + .unwrap(); + assert_eq!(room.room_id(), room_id); + } + + #[async_test] + async fn test_await_room_remote_echo_will_timeout_if_no_room_is_found() { + let (client_builder, _) = test_client_builder_with_server().await; + let client = client_builder.request_config(RequestConfig::new()).build().await.unwrap(); + set_client_session(&client).await; + + let room_id = room_id!("!room:example.org"); + // Room is not present so the client won't be able to find it. The call will + // timeout. + let err = + tokio::time::timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)) + .await + .err(); + assert!(err.is_some()); + } + + #[async_test] + async fn test_await_room_remote_echo_will_timeout_if_room_is_found_but_not_synced() { + let (client_builder, server) = test_client_builder_with_server().await; + let client = client_builder.request_config(RequestConfig::new()).build().await.unwrap(); + set_client_session(&client).await; + + Mock::given(method("POST")) + .and(path("_matrix/client/r0/createRoom")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "room_id": "!room:example.org"})), + ) + .mount(&server) + .await; + + // Create a room in the internal store + let room = client + .create_room(assign!(CreateRoomRequest::new(), { + invite: vec![], + is_direct: false, + })) + .await + .unwrap(); + + // Room is locally present, but not synced, the call will timeout + let err = tokio::time::timeout( + Duration::from_secs(1), + client.await_room_remote_echo(room.room_id()), + ) + .await + .err(); + assert!(err.is_some()); + } } From 6ae7d3c017f2dce93dd3a22aa48ab0feea45a74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 11 Sep 2024 14:12:50 +0200 Subject: [PATCH 099/979] ffi: add FFI fn for `Client::await_room_remote_echo(&room_id)` --- bindings/matrix-sdk-ffi/src/client.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index d8ed3f22cf1..a237b07a65e 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1002,6 +1002,16 @@ impl Client { Ok(RoomPreview::from_sdk(sdk_room_preview)) } + + /// Waits until an at least partially synced room is received, and returns + /// it. + /// + /// **Note: this function will loop endlessly until either it finds the room + /// or an externally set timeout happens.** + pub async fn await_room_remote_echo(&self, room_id: String) -> Result, ClientError> { + let room_id = RoomId::parse(room_id)?; + Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await))) + } } #[uniffi::export(callback_interface)] From 25111ac9eb2c5e46e8f919d3ef110518e0f171b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 11 Sep 2024 14:14:10 +0200 Subject: [PATCH 100/979] sdk-ui: make `SlidingSyncRoom` not needed in `RoomListItem::default_room_timeline_builder`. Having initial items shouldn't be mandatory to create a timeline, the timeline can also be empty. --- .../src/room_list_service/room.rs | 38 +++++++++------- .../tests/integration/room_list_service.rs | 33 +++++++++++++- crates/matrix-sdk/src/client/mod.rs | 44 +++++++------------ 3 files changed, 71 insertions(+), 44 deletions(-) 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 cad3e845e84..ee444a20a91 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room.rs @@ -20,6 +20,7 @@ use std::{ops::Deref, sync::Arc}; use async_once_cell::OnceCell as AsyncOnceCell; use matrix_sdk::SlidingSync; use ruma::RoomId; +use tracing::info; use super::Error; use crate::{ @@ -150,27 +151,32 @@ impl Room { } /// Create a new [`TimelineBuilder`] with the default configuration. + /// + /// If the room was synced before some initial events will be added to the + /// [`TimelineBuilder`]. pub async fn default_room_timeline_builder(&self) -> Result { // TODO we can remove this once the event cache handles his own cache. - let sliding_sync_room = + let sliding_sync_room = self.inner.sliding_sync.get_room(self.inner.room.room_id()).await; + + if let Some(sliding_sync_room) = sliding_sync_room { self.inner - .sliding_sync - .get_room(self.inner.room.room_id()) + .room + .client() + .event_cache() + .add_initial_events( + self.inner.room.room_id(), + sliding_sync_room.timeline_queue().iter().cloned().collect(), + sliding_sync_room.prev_batch(), + ) .await - .ok_or_else(|| Error::RoomNotFound(self.inner.room.room_id().to_owned()))?; - - self.inner - .room - .client() - .event_cache() - .add_initial_events( - self.inner.room.room_id(), - sliding_sync_room.timeline_queue().iter().cloned().collect(), - sliding_sync_room.prev_batch(), - ) - .await - .map_err(Error::EventCache)?; + .map_err(Error::EventCache)?; + } else { + info!( + "No cached sliding sync room found for `{}`, the timeline will be empty.", + self.room_id() + ); + } Ok(Timeline::builder(&self.inner.room).track_read_marker_and_receipts()) } 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 c4711d35221..b37ec389f9c 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -20,6 +20,7 @@ use matrix_sdk_ui::{ RoomListService, }; use ruma::{ + api::client::room::create_room::v3::Request as CreateRoomRequest, assign, event_id, events::{room::message::RoomMessageEventContent, StateEventType}, mxc_uri, room_id, uint, @@ -27,7 +28,10 @@ use ruma::{ use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; use tokio::{spawn, sync::mpsc::channel, task::yield_now}; -use wiremock::MockServer; +use wiremock::{ + matchers::{header, method, path}, + Mock, MockServer, ResponseTemplate, +}; use crate::timeline::sliding_sync::{assert_timeline_stream, timeline_event}; @@ -2404,6 +2408,33 @@ async fn test_room_timeline() -> Result<(), Error> { Ok(()) } +#[async_test] +async fn test_room_empty_timeline() { + let (client, server, room_list) = new_room_list_service().await.unwrap(); + mock_encryption_state(&server, false).await; + + Mock::given(method("POST")) + .and(path("_matrix/client/r0/createRoom")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "room_id": "!example:localhost"})), + ) + .mount(&server) + .await; + + let room = client.create_room(CreateRoomRequest::default()).await.unwrap(); + let room_id = room.room_id().to_owned(); + + // The room wasn't synced, but it will be available + let room = room_list.room(&room_id).unwrap(); + let timeline = room.default_room_timeline_builder().await.unwrap().build().await.unwrap(); + let (prev_items, _) = timeline.subscribe().await; + + // However, since the room wasn't synced its timeline won't have any initial + // items + assert!(prev_items.is_empty()); +} + #[async_test] async fn test_room_latest_event() -> Result<(), Error> { let (_, server, room_list) = new_room_list_service().await?; diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 9ce92b31fa0..7ea618e7cfa 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2252,14 +2252,12 @@ impl Client { if room.is_state_partially_or_fully_synced() { debug!("Found just created room!"); return room; - } else { - warn!("Room wasn't partially synced, waiting for sync beat to try again"); } + debug!("Room wasn't partially synced, waiting for sync beat to try again"); } else { - warn!("Room wasn't found, waiting for sync beat to try again"); + debug!("Room wasn't found, waiting for sync beat to try again"); } self.inner.sync_beat.listen().await; - debug!("New sync beat found"); } } } @@ -2310,6 +2308,7 @@ pub(crate) mod tests { use std::{sync::Arc, time::Duration}; use assert_matches::assert_matches; + use futures_util::FutureExt; use matrix_sdk_base::{ store::{MemoryStore, StoreConfig}, RoomState, @@ -2328,6 +2327,10 @@ pub(crate) mod tests { owned_room_id, room_id, RoomId, ServerName, UserId, }; use serde_json::json; + use tokio::{ + spawn, + time::{sleep, timeout}, + }; use url::Url; use wiremock::{ matchers::{body_json, header, method, path, query_param_is_missing}, @@ -2668,7 +2671,7 @@ pub(crate) mod tests { let client = logged_in_client(None).await; // Wait for the init tasks to die. - tokio::time::sleep(Duration::from_secs(1)).await; + sleep(Duration::from_secs(1)).await; let weak_client = WeakClient::from_client(&client); assert_eq!(weak_client.strong_count(), 1); @@ -2691,7 +2694,7 @@ pub(crate) mod tests { drop(client); // Give a bit of time for background tasks to die. - tokio::time::sleep(Duration::from_secs(1)).await; + sleep(Duration::from_secs(1)).await; // The weak client must be the last reference to the client now. assert_eq!(weak_client.strong_count(), 0); @@ -2824,10 +2827,7 @@ pub(crate) mod tests { client.sync_once(SyncSettings::default()).await.unwrap(); - let room = - tokio::time::timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)) - .await - .unwrap(); + let room = client.await_room_remote_echo(room_id).now_or_never().unwrap(); assert_eq!(room.room_id(), room_id); } @@ -2857,18 +2857,16 @@ pub(crate) mod tests { // Perform the /sync request with a delay so it starts after the // `await_room_remote_echo` call has happened - tokio::spawn({ + spawn({ let client = client.clone(); async move { - tokio::time::sleep(Duration::from_millis(100)).await; + sleep(Duration::from_millis(100)).await; client.sync_once(SyncSettings::default()).await.unwrap(); } }); let room = - tokio::time::timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)) - .await - .unwrap(); + timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)).await.unwrap(); assert_eq!(room.room_id(), room_id); } @@ -2881,11 +2879,7 @@ pub(crate) mod tests { let room_id = room_id!("!room:example.org"); // Room is not present so the client won't be able to find it. The call will // timeout. - let err = - tokio::time::timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)) - .await - .err(); - assert!(err.is_some()); + timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)).await.unwrap_err(); } #[async_test] @@ -2913,12 +2907,8 @@ pub(crate) mod tests { .unwrap(); // Room is locally present, but not synced, the call will timeout - let err = tokio::time::timeout( - Duration::from_secs(1), - client.await_room_remote_echo(room.room_id()), - ) - .await - .err(); - assert!(err.is_some()); + timeout(Duration::from_secs(1), client.await_room_remote_echo(room.room_id())) + .await + .unwrap_err(); } } From 5827bb7ab33b275835f16eacb4c0fecfa79f4a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 11 Sep 2024 14:14:42 +0200 Subject: [PATCH 101/979] sdk-ui: fix typo in `TimelineState::replace_with_remove_events` --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 2 +- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 13088594940..33e47371fb2 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -652,7 +652,7 @@ impl TimelineController

{ // now we may want to replace a populated timeline with an empty one. if !state.items.is_empty() || !events.is_empty() { state - .replace_with_remove_events( + .replace_with_remote_events( events, TimelineEnd::Back, origin, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 7aebfb4f4f3..ef71404296b 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -279,7 +279,7 @@ impl TimelineState { /// Note: when the `position` is [`TimelineEnd::Front`], prepended events /// should be ordered in *reverse* topological order, that is, `events[0]` /// is the most recent. - pub(super) async fn replace_with_remove_events( + pub(super) async fn replace_with_remote_events( &mut self, events: Vec, position: TimelineEnd, From 2408df8bf55683821005112a0a9c01154750e597 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 12 Sep 2024 12:29:49 +0200 Subject: [PATCH 102/979] multiverse: highlight which rooms are DMs in the list --- labs/multiverse/src/main.rs | 49 +++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index f7a00a5a5ba..3829920569e 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -23,8 +23,9 @@ use matrix_sdk::{ ruma::{ api::client::receipt::create_receipt::v3::ReceiptType, events::room::message::{MessageType, RoomMessageEventContent}, - MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, + uint, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, }, + sliding_sync::http::request::RoomSubscription, AuthSession, Client, ServerName, SqliteCryptoStore, SqliteStateStore, }; use matrix_sdk_ui::{ @@ -35,7 +36,7 @@ use matrix_sdk_ui::{ }; use ratatui::{prelude::*, style::palette::tailwind, widgets::*}; use tokio::{runtime::Handle, spawn, task::JoinHandle}; -use tracing::error; +use tracing::{error, warn}; use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter}; const HEADER_BG: Color = tailwind::BLUE.c950; @@ -129,6 +130,9 @@ struct ExtraRoomInfo { /// Calculated display name for the room. display_name: Option, + + /// Is the room a DM? + is_dm: Option, } struct App { @@ -223,6 +227,23 @@ impl App { let mut new_ui_rooms = HashMap::new(); let mut new_timelines = Vec::new(); + // Update all the room info for all rooms. + for room in all_rooms.iter() { + let raw_name = room.name(); + let display_name = room.cached_display_name(); + let is_dm = room + .is_direct() + .await + .map_err(|err| { + warn!("couldn't figure whether a room is a DM or not: {err}"); + }) + .ok(); + room_infos.lock().unwrap().insert( + room.room_id().to_owned(), + ExtraRoomInfo { raw_name, display_name, is_dm }, + ); + } + // Initialize all the new rooms. for ui_room in all_rooms .into_iter() @@ -266,15 +287,6 @@ impl App { new_ui_rooms.insert(ui_room.room_id().to_owned(), ui_room); } - for (room_id, room) in &new_ui_rooms { - let raw_name = room.name(); - let display_name = room.cached_display_name(); - room_infos - .lock() - .unwrap() - .insert(room_id.to_owned(), ExtraRoomInfo { raw_name, display_name }); - } - ui_rooms.lock().unwrap().extend(new_ui_rooms); timelines.lock().unwrap().extend(new_timelines); } @@ -398,7 +410,10 @@ impl App { .get_selected_room_id(Some(selected)) .and_then(|room_id| self.ui_rooms.lock().unwrap().get(&room_id).cloned()) { - self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()], None); + let mut sub = RoomSubscription::default(); + sub.timeline_limit = Some(uint!(30)); + + self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()], Some(sub)); self.current_room_subscription = Some(room); } } @@ -588,12 +603,14 @@ impl App { let room_id = room.room_id(); let room_info = room_info.remove(room_id); - let (raw, display) = if let Some(info) = room_info { - (info.raw_name, info.display_name) + let (raw, display, is_dm) = if let Some(info) = room_info { + (info.raw_name, info.display_name, info.is_dm) } else { - (None, None) + (None, None, None) }; + let dm_marker = if is_dm.unwrap_or(false) { "🤫" } else { "" }; + let room_name = if let Some(n) = display { format!("{n} ({room_id})") } else if let Some(n) = raw { @@ -602,7 +619,7 @@ impl App { room_id.to_string() }; - format!("#{i} {}", room_name) + format!("#{i}{dm_marker} {}", room_name) }; let line = Line::styled(line, TEXT_COLOR); From 72cc2bd60cb7046bb7250c5d09baf3a54f0fbfe4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 12 Sep 2024 17:01:41 +0100 Subject: [PATCH 103/979] crypto: Include megolm ratchet index in logging span fields This field is helpful as it tells us the sequence number of the message in the megolm session, which gives us a clue about how long it will have been since the session should have been shared with us. --- crates/matrix-sdk-crypto/CHANGELOG.md | 3 +++ crates/matrix-sdk-crypto/src/machine/mod.rs | 4 +++- .../matrix-sdk-crypto/src/types/events/room/encrypted.rs | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 7ec36d90b54..7db9cb8c702 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,9 @@ Changes: +- Improve logging for undecryptable Megolm events. + ([#3989](https://github.com/matrix-org/matrix-rust-sdk/pull/3989)) + - Miscellaneous improvements to logging for verification and `OwnUserIdentity` updates. ([#3949](https://github.com/matrix-org/matrix-rust-sdk/pull/3949)) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index d0e985d0566..95a435a25dd 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -1747,7 +1747,7 @@ impl OlmMachine { self.decrypt_room_event_inner(event, room_id, true, decryption_settings).await } - #[instrument(name = "decrypt_room_event", skip_all, fields(?room_id, event_id, origin_server_ts, sender, algorithm, session_id, sender_key))] + #[instrument(name = "decrypt_room_event", skip_all, fields(?room_id, event_id, origin_server_ts, sender, algorithm, session_id, message_index, sender_key))] async fn decrypt_room_event_inner( &self, event: &Raw, @@ -1781,6 +1781,8 @@ impl OlmMachine { }; Span::current().record("session_id", content.session_id()); + Span::current().record("message_index", content.message_index()); + let result = self.decrypt_megolm_events(room_id, &event, &content, decryption_settings).await; diff --git a/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs b/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs index debb082f324..21f8e5c1831 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs @@ -268,6 +268,15 @@ impl SupportedEventEncryptionSchemes<'_> { SupportedEventEncryptionSchemes::MegolmV2AesSha2(c) => &c.session_id, } } + + /// The index of the Megolm ratchet that was used to encrypt the message. + pub fn message_index(&self) -> u32 { + match self { + SupportedEventEncryptionSchemes::MegolmV1AesSha2(c) => c.ciphertext.message_index(), + #[cfg(feature = "experimental-algorithms")] + SupportedEventEncryptionSchemes::MegolmV2AesSha2(c) => c.ciphertext.message_index(), + } + } } impl<'a> From<&'a MegolmV1AesSha2Content> for SupportedEventEncryptionSchemes<'a> { From 2532c5227f3e06873af736948128594f6b6147f7 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 13 Sep 2024 16:15:28 +0100 Subject: [PATCH 104/979] ffi: Add the registration helper URL to the ElementWellKnown file. --- bindings/matrix-sdk-ffi/src/element.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/element.rs b/bindings/matrix-sdk-ffi/src/element.rs index 3d9c81b9710..6ff2dff5b53 100644 --- a/bindings/matrix-sdk-ffi/src/element.rs +++ b/bindings/matrix-sdk-ffi/src/element.rs @@ -11,7 +11,8 @@ pub struct ElementCallWellKnown { /// Element specific well-known settings #[derive(Deserialize, uniffi::Record)] pub struct ElementWellKnown { - call: ElementCallWellKnown, + call: Option, + registration_helper_url: Option, } /// Helper function to parse a string into a ElementWellKnown struct From a9ed62284e2827cd376da9f304783b75ab07dadf Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 13 Sep 2024 16:58:43 +0100 Subject: [PATCH 105/979] ffi: Expose the server URL to the app too. --- bindings/matrix-sdk-ffi/src/client.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index a237b07a65e..d5ebd8500d1 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -793,6 +793,21 @@ impl Client { self.inner.homeserver().to_string() } + /// The URL of the server. + /// + /// Not to be confused with the `Self::homeserver`. `server` is usually + /// the server part in a user ID, e.g. with `@mnt_io:matrix.org`, here + /// `matrix.org` is the server, whilst `matrix-client.matrix.org` is the + /// homeserver (at the time of writing — 2024-08-28). + /// + /// This value is optional depending on how the `Client` has been built. + /// If it's been built from a homeserver URL directly, we don't know the + /// server. However, if the `Client` has been built from a server URL or + /// name, then the homeserver has been discovered, and we know both. + pub fn server(&self) -> Option { + self.inner.server().map(ToString::to_string) + } + pub fn rooms(&self) -> Vec> { self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect() } From dd13fe6b4e42d4dcb6a3e12f2de13081ec84eaf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 13 Sep 2024 14:33:43 +0200 Subject: [PATCH 106/979] sdk-base: use updated account data for processing direct rooms --- crates/matrix-sdk-base/src/client.rs | 7 ++++++- crates/matrix-sdk-base/src/sliding_sync/mod.rs | 14 ++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index d3838d8a13f..c4ec7ba9983 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1169,7 +1169,12 @@ impl BaseClient { // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. - self.handle_account_data(&response.account_data.events, &mut changes).await; + if let Ok(Some(direct_account_data)) = + self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await + { + debug!("Found direct room data in the Store, applying it"); + self.handle_account_data(&vec![direct_account_data], &mut changes).await; + } changes.presence = response .presence diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 6f46e09a767..8ca60515e39 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -26,11 +26,14 @@ use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use ruma::events::AnyToDeviceEvent; use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom}, - events::{AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent}, + events::{ + AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent, + GlobalAccountDataEventType, + }, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, }; -use tracing::{debug, error, instrument, trace, warn}; +use tracing::{debug, error, info, instrument, trace, warn}; use super::BaseClient; #[cfg(feature = "e2e-encryption")] @@ -303,8 +306,11 @@ impl BaseClient { // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. - if !account_data.is_empty() { - self.handle_account_data(&account_data.global, &mut changes).await; + if let Ok(Some(direct_account_data)) = + self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await + { + debug!("Found direct room data in the Store, applying it"); + self.handle_account_data(&vec![direct_account_data], &mut changes).await; } // FIXME not yet supported by sliding sync. From aa92e2634238ce1ab85ca393178367c5b67c2a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 13 Sep 2024 15:22:08 +0200 Subject: [PATCH 107/979] sdk-base: fix handle_account_data behaviour Handle the account data in the response if not empty, otherwise use the cached one. --- crates/matrix-sdk-base/src/client.rs | 12 +++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 49 +++++++++++++++- crates/matrix-sdk/tests/integration/client.rs | 56 ++++++++++++++++++- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index c4ec7ba9983..211499d767e 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1169,11 +1169,19 @@ impl BaseClient { // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. - if let Ok(Some(direct_account_data)) = + let has_new_direct_room_data = response.account_data.events.iter().any(|raw_event| { + raw_event + .deserialize() + .map(|event| event.event_type() == GlobalAccountDataEventType::Direct) + .unwrap_or_default() + }); + if has_new_direct_room_data { + self.handle_account_data(&response.account_data.events, &mut changes).await; + } else if let Ok(Some(direct_account_data)) = self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await { debug!("Found direct room data in the Store, applying it"); - self.handle_account_data(&vec![direct_account_data], &mut changes).await; + self.handle_account_data(&[direct_account_data], &mut changes).await; } changes.presence = response diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 8ca60515e39..825247b579e 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -33,7 +33,7 @@ use ruma::{ serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, }; -use tracing::{debug, error, info, instrument, trace, warn}; +use tracing::{debug, error, instrument, trace, warn}; use super::BaseClient; #[cfg(feature = "e2e-encryption")] @@ -306,11 +306,19 @@ impl BaseClient { // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. - if let Ok(Some(direct_account_data)) = + let has_new_direct_room_data = account_data.global.iter().any(|raw_event| { + raw_event + .deserialize() + .map(|event| event.event_type() == GlobalAccountDataEventType::Direct) + .unwrap_or_default() + }); + if has_new_direct_room_data { + self.handle_account_data(&account_data.global, &mut changes).await; + } else if let Ok(Some(direct_account_data)) = self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await { debug!("Found direct room data in the Store, applying it"); - self.handle_account_data(&vec![direct_account_data], &mut changes).await; + self.handle_account_data(&[direct_account_data], &mut changes).await; } // FIXME not yet supported by sliding sync. @@ -2318,6 +2326,41 @@ mod tests { assert!(pinned_event_ids.is_empty()); } + #[async_test] + async fn test_dms_are_processed_in_any_sync_response() { + let current_user_id = user_id!("@current:e.uk"); + let client = logged_in_base_client(Some(current_user_id)).await; + let user_a_id = user_id!("@a:e.uk"); + let user_b_id = user_id!("@b:e.uk"); + let room_id_1 = room_id!("!r:e.uk"); + let room_id_2 = room_id!("!s:e.uk"); + + let mut room_response = http::response::Room::new(); + set_room_joined(&mut room_response, user_a_id); + let mut response = response_with_room(room_id_1, room_response); + let mut direct_content = BTreeMap::new(); + direct_content.insert(user_a_id.to_owned(), vec![room_id_1.to_owned()]); + direct_content.insert(user_b_id.to_owned(), vec![room_id_2.to_owned()]); + response + .extensions + .account_data + .global + .push(make_global_account_data_event(DirectEventContent(direct_content))); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + let room_1 = client.get_room(room_id_1).unwrap(); + assert!(room_1.is_direct().await.unwrap()); + + // Now perform a sync without new account data + let mut room_response = http::response::Room::new(); + set_room_joined(&mut room_response, user_b_id); + let response = response_with_room(room_id_2, room_response); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + let room_2 = client.get_room(room_id_2).unwrap(); + assert!(room_2.is_direct().await.unwrap()); + } + async fn choose_event_to_cache(events: &[SyncTimelineEvent]) -> Option { let room = make_room(); let mut room_info = room.clone_info(); diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index be5517bc52d..d3af616a76f 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -13,7 +13,7 @@ use matrix_sdk_test::{ self, sync::{MIXED_INVITED_ROOM_ID, MIXED_JOINED_ROOM_ID, MIXED_LEFT_ROOM_ID, MIXED_SYNC}, }, - JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, + GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, }; use ruma::{ api::client::{ @@ -1238,3 +1238,57 @@ async fn test_rooms_stream() { assert_pending!(rooms_stream); } + +#[async_test] +async fn test_dms_are_processed_in_any_sync_response() { + let (client, server) = logged_in_client_with_server().await; + let user_a_id = user_id!("@a:e.uk"); + let user_b_id = user_id!("@b:e.uk"); + let room_id_1 = room_id!("!r:e.uk"); + let room_id_2 = room_id!("!s:e.uk"); + + let joined_room_builder = JoinedRoomBuilder::new(room_id_1); + let mut sync_response_builder = SyncResponseBuilder::new(); + sync_response_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Custom( + json!({ + "content": { + user_a_id: [ + room_id_1 + ], + user_b_id: [ + room_id_2 + ] + }, + "type": "m.direct", + "event_id": "$757957878228ekrDs:localhost", + "origin_server_ts": 17195787, + "sender": "@example:localhost", + "state_key": "", + "type": "m.direct", + "unsigned": { + "age": 139298 + } + }), + )); + sync_response_builder.add_joined_room(joined_room_builder); + let json_response = sync_response_builder.build_json_sync_response(); + + mock_sync(&server, json_response, None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + let room_1 = client.get_room(room_id_1).unwrap(); + assert!(room_1.is_direct().await.unwrap()); + + // Now perform a sync without new account data + let joined_room_builder = JoinedRoomBuilder::new(room_id_2); + sync_response_builder.add_joined_room(joined_room_builder); + let json_response = sync_response_builder.build_json_sync_response(); + + mock_sync(&server, json_response, None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + let room_2 = client.get_room(room_id_2).unwrap(); + assert!(room_2.is_direct().await.unwrap()); +} From 98a3a0b3c4c6905a20cb5ddfc3a063020d530c59 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 16 Sep 2024 10:24:47 +0200 Subject: [PATCH 108/979] chore(ui,ffi): Remove the `RoomList::entries` method. This method is now private inside `matrix_sdk_ui` and removed from `matrix_sdk_ffi`. This method is returning a stream of rooms, but updates on rooms won't update the stream (only new rooms will be seen on the stream). Nobody uses it as far as I know, and `entries_with_dynamic_adapters` is the real only API we want people to use. --- bindings/matrix-sdk-ffi/src/room_list.rs | 27 ------ .../src/room_list_service/mod.rs | 6 +- .../src/room_list_service/room_list.rs | 12 +-- .../tests/integration/room_list_service.rs | 90 ------------------- 4 files changed, 10 insertions(+), 125 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 48a6c100397..1d24c547f25 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -182,33 +182,6 @@ impl RoomList { }) } - fn entries(&self, listener: Box) -> Arc { - let this = self.inner.clone(); - let utd_hook = self.room_list_service.utd_hook.clone(); - - Arc::new(TaskHandle::new(RUNTIME.spawn(async move { - let (entries, entries_stream) = this.entries(); - - pin_mut!(entries_stream); - - listener.on_update(vec![RoomListEntriesUpdate::Append { - values: entries - .into_iter() - .map(|room| Arc::new(RoomListItem::from(room, utd_hook.clone()))) - .collect(), - }]); - - while let Some(diffs) = entries_stream.next().await { - listener.on_update( - diffs - .into_iter() - .map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone())) - .collect(), - ); - } - }))) - } - fn entries_with_dynamic_adapters( self: Arc, page_size: u32, diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 1b5a57a3a96..3f10f066774 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -44,9 +44,9 @@ //! fluid user experience for a Matrix client. //! //! [`RoomListService::all_rooms`] provides a way to get a [`RoomList`] for all -//! the rooms. From that, calling [`RoomList::entries`] provides a way to get a -//! stream of room list entry. This stream can be filtered, and the filter can -//! be changed over time. +//! the rooms. From that, calling [`RoomList::entries_with_dynamic_adapters`] +//! provides a way to get a stream of rooms. This stream is sorted, can be +//! filtered, and the filter can be changed over time. //! //! [`RoomListService::state`] provides a way to get a stream of the state //! machine's state, which can be pretty helpful for the client app. diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index de7b4870270..6f9686bbf00 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -115,8 +115,8 @@ impl RoomList { self.loading_state.subscribe() } - /// Get all previous rooms, in addition to a [`Stream`] to rooms' updates. - pub fn entries(&self) -> (Vector, impl Stream>> + '_) { + /// Get a stream of rooms. + fn entries(&self) -> (Vector, impl Stream>> + '_) { let (rooms, stream) = self.client.rooms_stream(); let map_room = |room| Room::new(room, &self.sliding_sync); @@ -127,9 +127,11 @@ impl RoomList { ) } - /// Similar to [`Self::entries`] except that it's possible to provide a - /// filter that will filter out room list entries, and that it's also - /// possible to “paginate” over the entries by `page_size`. + /// Get a configurable stream of rooms. + /// + /// It's possible to provide a filter that will filter out room list + /// entries, and that it's also possible to “paginate” over the entries by + /// `page_size`. The rooms are also sorted. /// /// The returned stream will only start yielding diffs once a filter is set /// through the returned [`RoomListDynamicEntriesController`]. For every 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 b37ec389f9c..25ba17d6b66 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -1167,96 +1167,6 @@ async fn test_loading_states() -> Result<(), Error> { Ok(()) } -#[async_test] -async fn test_entries_stream() -> Result<(), Error> { - let (_, server, room_list) = new_room_list_service().await?; - - let sync = room_list.sync(); - pin_mut!(sync); - - let all_rooms = room_list.all_rooms().await?; - - let (previous_entries, entries_stream) = all_rooms.entries(); - pin_mut!(entries_stream); - - sync_then_assert_request_and_fake_response! { - [server, room_list, sync] - states = Init => SettingUp, - assert request >= { - "lists": { - ALL_ROOMS: { - "ranges": [[0, 19]], - }, - }, - }, - respond with = { - "pos": "0", - "lists": { - ALL_ROOMS: { - "count": 10, - }, - }, - "rooms": { - "!r0:bar.org": { - "initial": true, - "timeline": [], - }, - "!r1:bar.org": { - "initial": true, - "timeline": [], - }, - "!r2:bar.org": { - "initial": true, - "timeline": [], - }, - }, - }, - }; - - assert!(previous_entries.is_empty()); - assert_entries_batch! { - [entries_stream] - push back [ "!r0:bar.org" ]; - push back [ "!r1:bar.org" ]; - push back [ "!r2:bar.org" ]; - end; - }; - - sync_then_assert_request_and_fake_response! { - [server, room_list, sync] - states = SettingUp => Running, - assert request >= { - "lists": { - ALL_ROOMS: { - "ranges": [[0, 9]], - }, - }, - }, - respond with = { - "pos": "1", - "lists": { - ALL_ROOMS: { - "count": 9, - }, - }, - "rooms": { - "!r3:bar.org": { - "initial": true, - "timeline": [], - }, - }, - }, - }; - - assert_entries_batch! { - [entries_stream] - push back [ "!r3:bar.org" ]; - end; - }; - - Ok(()) -} - #[async_test] async fn test_dynamic_entries_stream() -> Result<(), Error> { let (client, server, room_list) = new_room_list_service().await?; From af390328b58ccbbf0fb7687e8f6b4aa090676800 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 16 Sep 2024 12:01:14 +0200 Subject: [PATCH 109/979] Revert "chore(ui,ffi): Remove the `RoomList::entries` method." This reverts commit 98a3a0b3c4c6905a20cb5ddfc3a063020d530c59. --- bindings/matrix-sdk-ffi/src/room_list.rs | 27 ++++++ .../src/room_list_service/mod.rs | 6 +- .../src/room_list_service/room_list.rs | 12 ++- .../tests/integration/room_list_service.rs | 90 +++++++++++++++++++ 4 files changed, 125 insertions(+), 10 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 1d24c547f25..48a6c100397 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -182,6 +182,33 @@ impl RoomList { }) } + fn entries(&self, listener: Box) -> Arc { + let this = self.inner.clone(); + let utd_hook = self.room_list_service.utd_hook.clone(); + + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { + let (entries, entries_stream) = this.entries(); + + pin_mut!(entries_stream); + + listener.on_update(vec![RoomListEntriesUpdate::Append { + values: entries + .into_iter() + .map(|room| Arc::new(RoomListItem::from(room, utd_hook.clone()))) + .collect(), + }]); + + while let Some(diffs) = entries_stream.next().await { + listener.on_update( + diffs + .into_iter() + .map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone())) + .collect(), + ); + } + }))) + } + fn entries_with_dynamic_adapters( self: Arc, page_size: u32, diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 3f10f066774..1b5a57a3a96 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -44,9 +44,9 @@ //! fluid user experience for a Matrix client. //! //! [`RoomListService::all_rooms`] provides a way to get a [`RoomList`] for all -//! the rooms. From that, calling [`RoomList::entries_with_dynamic_adapters`] -//! provides a way to get a stream of rooms. This stream is sorted, can be -//! filtered, and the filter can be changed over time. +//! the rooms. From that, calling [`RoomList::entries`] provides a way to get a +//! stream of room list entry. This stream can be filtered, and the filter can +//! be changed over time. //! //! [`RoomListService::state`] provides a way to get a stream of the state //! machine's state, which can be pretty helpful for the client app. diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index 6f9686bbf00..de7b4870270 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -115,8 +115,8 @@ impl RoomList { self.loading_state.subscribe() } - /// Get a stream of rooms. - fn entries(&self) -> (Vector, impl Stream>> + '_) { + /// Get all previous rooms, in addition to a [`Stream`] to rooms' updates. + pub fn entries(&self) -> (Vector, impl Stream>> + '_) { let (rooms, stream) = self.client.rooms_stream(); let map_room = |room| Room::new(room, &self.sliding_sync); @@ -127,11 +127,9 @@ impl RoomList { ) } - /// Get a configurable stream of rooms. - /// - /// It's possible to provide a filter that will filter out room list - /// entries, and that it's also possible to “paginate” over the entries by - /// `page_size`. The rooms are also sorted. + /// Similar to [`Self::entries`] except that it's possible to provide a + /// filter that will filter out room list entries, and that it's also + /// possible to “paginate” over the entries by `page_size`. /// /// The returned stream will only start yielding diffs once a filter is set /// through the returned [`RoomListDynamicEntriesController`]. For every 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 25ba17d6b66..b37ec389f9c 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -1167,6 +1167,96 @@ async fn test_loading_states() -> Result<(), Error> { Ok(()) } +#[async_test] +async fn test_entries_stream() -> Result<(), Error> { + let (_, server, room_list) = new_room_list_service().await?; + + let sync = room_list.sync(); + pin_mut!(sync); + + let all_rooms = room_list.all_rooms().await?; + + let (previous_entries, entries_stream) = all_rooms.entries(); + pin_mut!(entries_stream); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + states = Init => SettingUp, + assert request >= { + "lists": { + ALL_ROOMS: { + "ranges": [[0, 19]], + }, + }, + }, + respond with = { + "pos": "0", + "lists": { + ALL_ROOMS: { + "count": 10, + }, + }, + "rooms": { + "!r0:bar.org": { + "initial": true, + "timeline": [], + }, + "!r1:bar.org": { + "initial": true, + "timeline": [], + }, + "!r2:bar.org": { + "initial": true, + "timeline": [], + }, + }, + }, + }; + + assert!(previous_entries.is_empty()); + assert_entries_batch! { + [entries_stream] + push back [ "!r0:bar.org" ]; + push back [ "!r1:bar.org" ]; + push back [ "!r2:bar.org" ]; + end; + }; + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + states = SettingUp => Running, + assert request >= { + "lists": { + ALL_ROOMS: { + "ranges": [[0, 9]], + }, + }, + }, + respond with = { + "pos": "1", + "lists": { + ALL_ROOMS: { + "count": 9, + }, + }, + "rooms": { + "!r3:bar.org": { + "initial": true, + "timeline": [], + }, + }, + }, + }; + + assert_entries_batch! { + [entries_stream] + push back [ "!r3:bar.org" ]; + end; + }; + + Ok(()) +} + #[async_test] async fn test_dynamic_entries_stream() -> Result<(), Error> { let (client, server, room_list) = new_room_list_service().await?; From 4fd4410f4a29f4acf356984d8c89c7a39215d279 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 16 Sep 2024 10:16:45 +0200 Subject: [PATCH 110/979] chore: Update to `ruma/ruma`. --- Cargo.lock | 18 +++--- Cargo.toml | 4 +- crates/matrix-sdk-base/src/rooms/mod.rs | 73 +++------------------- crates/matrix-sdk-base/src/rooms/normal.rs | 9 ++- crates/matrix-sdk/src/client/mod.rs | 2 +- 5 files changed, 25 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1296d78339..6a4925d995c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4934,7 +4934,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.10.1" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "assign", "js_int", @@ -4951,7 +4951,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "as_variant", "assign", @@ -4974,7 +4974,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "as_variant", "base64 0.22.1", @@ -5006,7 +5006,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "as_variant", "indexmap 2.2.6", @@ -5031,7 +5031,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "http", "js_int", @@ -5045,7 +5045,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.2.0" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "as_variant", "html5ever", @@ -5057,7 +5057,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "js_int", "thiserror", @@ -5066,7 +5066,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "cfg-if", "once_cell", @@ -5082,7 +5082,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://github.com/matrix-org/ruma?rev=bb6d4c531aebb571fed4b1948df0118244762741#bb6d4c531aebb571fed4b1948df0118244762741" +source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index cf07a01a28a..df695c571a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ once_cell = "1.16.0" pin-project-lite = "0.2.9" rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } -ruma = { git = "https://github.com/matrix-org/ruma", rev = "bb6d4c531aebb571fed4b1948df0118244762741", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "92a35381b56ffa3c2a611287bb5011f574271478", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -61,7 +61,7 @@ ruma = { git = "https://github.com/matrix-org/ruma", rev = "bb6d4c531aebb571fed4 "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = { git = "https://github.com/matrix-org/ruma", rev = "bb6d4c531aebb571fed4b1948df0118244762741" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "92a35381b56ffa3c2a611287bb5011f574271478" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index 264e00052fb..7038a15e962 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -40,7 +40,7 @@ use ruma::{ RedactedStateEventContent, StaticStateEventContent, SyncStateEvent, }, room::RoomType, - EventId, OwnedUserId, RoomVersionId, UserId, + EventId, OwnedUserId, RoomVersionId, }; use serde::{Deserialize, Serialize}; @@ -195,12 +195,11 @@ impl BaseRoomInfo { let mut o_ev = o_ev.clone(); o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts); - let Some(owned_user_id) = get_user_id_for_state_key(m.state_key()) else { - return false; - }; - // add the new event. - self.rtc_member.insert(owned_user_id, SyncStateEvent::Original(o_ev).into()); + self.rtc_member.insert( + m.state_key().user_id().to_owned(), + SyncStateEvent::Original(o_ev).into(), + ); // Remove all events that don't contain any memberships anymore. self.rtc_member.retain(|_, ev| { @@ -322,33 +321,6 @@ impl BaseRoomInfo { } } -/// Extract a user ID from a state key that matches one of these formats: -/// - `` -/// - `_` -/// - `__` -fn get_user_id_for_state_key(state_key: &str) -> Option { - if let Ok(user_id) = UserId::parse(state_key) { - return Some(user_id); - } - - // Ignore leading underscore if present - // (used for avoiding auth rules on @-prefixed state keys) - let state_key = state_key.strip_prefix('_').unwrap_or(state_key); - if state_key.starts_with('@') { - if let Some(colon_idx) = state_key.find(':') { - let state_key_user_id = match state_key[colon_idx + 1..].find('_') { - None => state_key, - Some(suffix_idx) => &state_key[..colon_idx + 1 + suffix_idx], - }; - if let Ok(user_id) = UserId::parse(state_key_user_id) { - return Some(user_id); - } - } - } - - None -} - bitflags! { /// Notable tags, i.e. subset of tags that we are more interested by. /// @@ -566,12 +538,9 @@ impl RoomMemberships { mod tests { use std::ops::Not; - use ruma::{ - events::tag::{TagInfo, TagName, Tags}, - user_id, - }; + use ruma::events::tag::{TagInfo, TagName, Tags}; - use super::{get_user_id_for_state_key, BaseRoomInfo, RoomNotableTags}; + use super::{BaseRoomInfo, RoomNotableTags}; #[test] fn test_handle_notable_tags_favourite() { @@ -602,32 +571,4 @@ mod tests { base_room_info.handle_notable_tags(&tags); assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not()); } - - #[test] - fn test_get_user_id_for_state_key() { - assert!(get_user_id_for_state_key("").is_none()); - assert!(get_user_id_for_state_key("abc").is_none()); - assert!(get_user_id_for_state_key("@nocolon").is_none()); - assert!(get_user_id_for_state_key("@noserverpart:").is_none()); - assert!(get_user_id_for_state_key("@noserverpart:_suffix").is_none()); - - let user_id = user_id!("@username:example.org"); - - assert_eq!(get_user_id_for_state_key(user_id.as_str()).as_deref(), Some(user_id)); - assert_eq!( - get_user_id_for_state_key(format!("{user_id}_valid_suffix").as_str()).as_deref(), - Some(user_id) - ); - assert!(get_user_id_for_state_key(format!("{user_id}:invalid_suffix").as_str()).is_none()); - - assert_eq!( - get_user_id_for_state_key(format!("_{user_id}").as_str()).as_deref(), - Some(user_id) - ); - assert_eq!( - get_user_id_for_state_key(format!("_{user_id}_valid_suffix").as_str()).as_deref(), - Some(user_id) - ); - assert!(get_user_id_for_state_key(format!("_{user_id}:invalid_suffix").as_str()).is_none()); - } } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 07de9c6cb8d..022b7b0ddb7 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1703,8 +1703,8 @@ mod tests { api::client::sync::sync_events::v3::RoomSummary as RumaSummary, events::{ call::member::{ - Application, CallApplicationContent, CallMemberEventContent, Focus, - LegacyMembershipData, LegacyMembershipDataInit, LivekitFocus, + Application, CallApplicationContent, CallMemberEventContent, CallMemberStateKey, + Focus, LegacyMembershipData, LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent, }, room::{ @@ -2743,7 +2743,10 @@ mod tests { // we can simply use now here since this will be dropped when using a MinimalStateEvent // in the roomInfo origin_server_ts: timestamp(0), - state_key: user_id.to_string(), + state_key: CallMemberStateKey::from_str(user_id.as_str()) + // SAFETY: `user_id` is a valid `UserId` and cannot fail to be transformed into a + // `CallMemberStateKey`. + .expect("Failed to transform a `UserId` into a `CallMemberStateKey`"), unsigned: StateUnsigned::new(), })) } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 7ea618e7cfa..35725a827b4 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -1204,7 +1204,7 @@ impl Client { server_names: &[OwnedServerName], ) -> Result { let request = assign!(join_room_by_id_or_alias::v3::Request::new(alias.to_owned()), { - server_name: server_names.to_owned(), + via: server_names.to_owned(), }); let response = self.send(request, None).await?; let base_room = self.base_client().room_joined(&response.room_id).await?; From 119bee66ce96fa8df4d1dcfffe4eee4a54fcd9b9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 11 Sep 2024 15:11:51 +0200 Subject: [PATCH 111/979] feat(sdk,ui): `SlidingSync::subscribe_to_rooms` has a new `cancel_in_flight_request` argument. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds a new `cancel_in_flight_request` argument to `SlidingSync::subscribe_to_rooms`, which tells the method cancel the in-flight request if any. This patch also updates `RoomListService::subscribe_to_rooms` to turn this new argument to `true` if the state machine isn't in a “starting” state. The problem it's solving is the following: * some apps starts the room list service * a first request is sent with `pos = None` * the server calculates a new session (which can be expensive) * the app subscribes to a set of rooms * a second request is immediately sent with `pos = None` again * the server does possibly NOT cancel its previous calculations, but starts a new session and its calculations This is pretty expensive for the server. This patch makes so that the immediate room subscriptions will be part of the second request, with the first request not being cancelled. --- crates/matrix-sdk-ui/src/notification_client.rs | 1 + .../matrix-sdk-ui/src/room_list_service/mod.rs | 9 ++++++++- crates/matrix-sdk/src/sliding_sync/mod.rs | 17 +++++++++-------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 6269363daa2..11b881de6ea 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -380,6 +380,7 @@ impl NotificationClient { required_state, timeline_limit: Some(uint!(16)) })), + true, ); let mut remaining_attempts = 3; diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 1b5a57a3a96..f6045837f9e 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -396,7 +396,14 @@ impl RoomListService { settings.required_state.push((StateEventType::RoomCreate, "".to_owned())); } - self.sliding_sync.subscribe_to_rooms(room_ids, Some(settings)) + let cancel_in_flight_request = match self.state.get() { + State::Init | State::Recovering | State::Error { .. } | State::Terminated { .. } => { + false + } + State::SettingUp | State::Running => true, + }; + + self.sliding_sync.subscribe_to_rooms(room_ids, Some(settings), cancel_in_flight_request) } #[cfg(test)] diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index ba271019f95..fff27f60405 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -151,12 +151,13 @@ impl SlidingSync { &self, room_ids: &[&RoomId], settings: Option, + cancel_in_flight_request: bool, ) { let settings = settings.unwrap_or_default(); let mut sticky = self.inner.sticky.write().unwrap(); let room_subscriptions = &mut sticky.data_mut().room_subscriptions; - let mut skip_sync_loop = false; + let mut skip_over_current_sync_loop_iteration = false; for room_id in room_ids { // If the room subscription already exists, let's not @@ -172,11 +173,11 @@ impl SlidingSync { entry.insert((RoomSubscriptionState::default(), settings.clone())); - skip_sync_loop = true; + skip_over_current_sync_loop_iteration = true; } } - if skip_sync_loop { + if cancel_in_flight_request && skip_over_current_sync_loop_iteration { self.inner.internal_channel_send_if_possible( SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration, ); @@ -1219,7 +1220,7 @@ mod tests { // Members are now synced! We can start subscribing and see how it goes. assert!(room0.are_members_synced()); - sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None); + sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, true); // OK, we have subscribed to some rooms. Let's check on `room0` if members are // now marked as not synced. @@ -1260,7 +1261,7 @@ mod tests { // Members are synced, good, good. assert!(room0.are_members_synced()); - sliding_sync.subscribe_to_rooms(&[room_id_0], None); + sliding_sync.subscribe_to_rooms(&[room_id_0], None, false); // Members are still synced: because we have already subscribed to the // room, the members aren't marked as unsynced. @@ -1280,7 +1281,7 @@ mod tests { let room_id_2 = room_id!("!r2:bar.org"); // Subscribe to two rooms. - sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None); + sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, false); { let sticky = sliding_sync.inner.sticky.read().unwrap(); @@ -1292,7 +1293,7 @@ mod tests { } // Subscribe to one more room. - sliding_sync.subscribe_to_rooms(&[room_id_2], None); + sliding_sync.subscribe_to_rooms(&[room_id_2], None, false); { let sticky = sliding_sync.inner.sticky.read().unwrap(); @@ -1314,7 +1315,7 @@ mod tests { } // Subscribe to one room again. - sliding_sync.subscribe_to_rooms(&[room_id_2], None); + sliding_sync.subscribe_to_rooms(&[room_id_2], None, false); { let sticky = sliding_sync.inner.sticky.read().unwrap(); From 965390cbdc8ef4e90c09585a74cbc31cd922c638 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Sep 2024 11:18:06 +0200 Subject: [PATCH 112/979] notification client: use the membership state to match an invite --- .../matrix-sdk-ui/src/notification_client.rs | 90 ++++++++++++++++--- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 11b881de6ea..1de35dfab59 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -29,7 +29,10 @@ use ruma::{ assign, directory::RoomTypeFilter, events::{ - room::{member::StrippedRoomMemberEvent, message::SyncRoomMessageEvent}, + room::{ + member::{MembershipState, StrippedRoomMemberEvent}, + message::SyncRoomMessageEvent, + }, AnyFullStateEventContent, AnyStateEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, StateEventType, TimelineEventType, }, @@ -280,9 +283,21 @@ impl NotificationClient { /// Try to run a sliding sync (without encryption) to retrieve the event /// from the notification. /// - /// This works by requesting explicit state that'll be useful for building - /// the `NotificationItem`, and subscribing to the room which the - /// notification relates to. + /// The event can either be: + /// - an invite event, + /// - or a non-invite event. + /// + /// In case it's a non-invite event, it's rather easy: we'll request + /// explicit state that'll be useful for building the + /// `NotificationItem`, and subscribe to the room which the notification + /// relates to. + /// + /// In case it's an invite-event, it's trickier because the stripped event + /// may not contain the event id, so we can't just match on it. Rather, + /// we look at stripped room member events that may be fitting (i.e. + /// match the current user and are invites), and if the SDK concludes the + /// room was in the invited state, and we didn't find the event by id, + /// *then* we'll use that stripped room member event. #[instrument(skip_all)] async fn try_sliding_sync( &self, @@ -297,9 +312,9 @@ impl NotificationClient { // notification, so we can figure out the full event and associated // information. - let notification = Arc::new(Mutex::new(None)); + let raw_notification = Arc::new(Mutex::new(None)); - let cloned_notif = notification.clone(); + let handler_raw_notification = raw_notification.clone(); let target_event_id = event_id.to_owned(); let timeline_event_handler = @@ -309,7 +324,7 @@ impl NotificationClient { if event_id == target_event_id { // found it! There shouldn't be a previous event before, but if there // is, that should be ok to just replace it. - *cloned_notif.lock().unwrap() = + *handler_raw_notification.lock().unwrap() = Some(RawNotificationEvent::Timeline(raw)); } } @@ -322,25 +337,57 @@ impl NotificationClient { } }); - let cloned_notif = notification.clone(); + // We'll only use this event if the room is in the invited state. + let raw_invite = Arc::new(Mutex::new(None)); + let target_event_id = event_id.to_owned(); + let user_id = self.client.user_id().unwrap().to_owned(); + let handler_raw_invite = raw_invite.clone(); + let handler_raw_notification = raw_notification.clone(); let stripped_member_handler = self.client.add_event_handler(move |raw: Raw| async move { + let deserialized = match raw.deserialize() { + Ok(d) => d, + Err(err) => { + warn!("failed to deserialize raw stripped room member event: {err}"); + return; + } + }; + + trace!("received a stripped room member event"); + + // Try to match the event by event_id, as it's the most precise. In theory, we + // shouldn't receive it, so that's a first attempt. match raw.get_field::("event_id") { Ok(Some(event_id)) => { if event_id == target_event_id { // found it! There shouldn't be a previous event before, but if there // is, that should be ok to just replace it. - *cloned_notif.lock().unwrap() = Some(RawNotificationEvent::Invite(raw)); + *handler_raw_notification.lock().unwrap() = + Some(RawNotificationEvent::Invite(raw)); + return; } } Ok(None) => { - warn!("a room member event had no id"); + debug!("a room member event had no id"); } Err(err) => { - warn!("a room member event id couldn't be decoded: {err}"); + debug!("a room member event id couldn't be decoded: {err}"); } } + + // Try to match the event by membership and state_key for the current user. + if deserialized.content.membership == MembershipState::Invite + && deserialized.state_key == user_id + { + debug!("found an invite event for the current user"); + // This could be it! There might be several of these following each other, so + // assume it's the latest one (in sync ordering), and override a previous one if + // present. + *handler_raw_invite.lock().unwrap() = Some(RawNotificationEvent::Invite(raw)); + } else { + debug!("not an invite event, or not for the current user"); + } }); // Room power levels are necessary to build the push context. @@ -394,7 +441,7 @@ impl NotificationClient { break; } - if notification.lock().unwrap().is_some() { + if raw_notification.lock().unwrap().is_some() || raw_invite.lock().unwrap().is_some() { // We got the event. break; } @@ -409,7 +456,24 @@ impl NotificationClient { self.client.remove_event_handler(stripped_member_handler); self.client.remove_event_handler(timeline_event_handler); - let maybe_event = notification.lock().unwrap().take(); + let mut maybe_event = raw_notification.lock().unwrap().take(); + + if maybe_event.is_none() { + trace!("we didn't have a non-invite event, looking for invited room now"); + if let Some(room) = self.client.get_room(room_id) { + if room.state() == RoomState::Invited { + maybe_event = raw_invite.lock().unwrap().take(); + } else { + debug!("the room isn't in the invited state"); + } + } else { + debug!("the room isn't an invite"); + } + } + + let found = if maybe_event.is_some() { "" } else { "not " }; + trace!("the notification event has been {found}found"); + Ok(maybe_event) } From abbe2ec52316c998c3894989b9c48b3781967d26 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Sep 2024 13:55:54 +0200 Subject: [PATCH 113/979] tests: increase timeout duration for await_room_remote_echo Fixes #4003, or so I suspect. The integration tests run in code coverage can be quite slow, so we can't put timeouts this low. --- crates/matrix-sdk/src/client/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 35725a827b4..ae2d4ad608b 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2866,7 +2866,7 @@ pub(crate) mod tests { }); let room = - timeout(Duration::from_secs(1), client.await_room_remote_echo(room_id)).await.unwrap(); + timeout(Duration::from_secs(10), client.await_room_remote_echo(room_id)).await.unwrap(); assert_eq!(room.room_id(), room_id); } From 5b79a9843e83f9f525b98e2a6fc18d7e814ec414 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 16 Sep 2024 14:35:15 +0300 Subject: [PATCH 114/979] ffi: expose method to allow user account deactivate for `m.login.password` based accounts --- bindings/matrix-sdk-ffi/src/client.rs | 25 +++++++++++++++++++++++++ crates/matrix-sdk/src/account.rs | 7 ++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index d5ebd8500d1..42386ca1391 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -74,6 +74,7 @@ use crate::{ notification_settings::NotificationSettings, room_directory_search::RoomDirectorySearch, room_preview::RoomPreview, + ruma::AuthData, sync_service::{SyncService, SyncServiceBuilder}, task_handle::TaskHandle, ClientError, @@ -1027,6 +1028,30 @@ impl Client { let room_id = RoomId::parse(room_id)?; Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await))) } + + /// Deactivate this account definitively. + /// Similarly to `encryption::reset_identity` this + /// will only work with password-based authentication (`m.login.password`) + /// + /// # Arguments + /// + /// * `auth_data` - This request uses the [User-Interactive Authentication + /// API][uiaa]. The first request needs to set this to `None` and will + /// always fail and the same request needs to be made but this time with + /// some `auth_data` provided. + pub async fn deactivate_account( + &self, + auth_data: Option, + erase_data: bool, + ) -> Result<(), ClientError> { + if let Some(auth_data) = auth_data { + _ = self.inner.account().deactivate(None, Some(auth_data.into()), erase_data).await?; + } else { + _ = self.inner.account().deactivate(None, None, erase_data).await?; + } + + Ok(()) + } } #[uniffi::export(callback_interface)] diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 2fbf4c65a8a..d46ed6c8058 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -360,6 +360,9 @@ impl Account { /// information for the interactive auth and the same request needs to be /// made but this time with some `auth_data` provided. /// + /// * `erase` - Whether the user would like their content to be erased as + /// much as possible from the server. + /// /// # Examples /// /// ```no_run @@ -376,7 +379,7 @@ impl Account { /// # let homeserver = Url::parse("http://localhost:8080")?; /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); - /// let response = account.deactivate(None, None).await; + /// let response = account.deactivate(None, None, false).await; /// /// // Proceed with UIAA. /// # anyhow::Ok(()) }; @@ -388,10 +391,12 @@ impl Account { &self, id_server: Option<&str>, auth_data: Option, + erase_data: bool, ) -> Result { let request = assign!(deactivate::v3::Request::new(), { id_server: id_server.map(ToOwned::to_owned), auth: auth_data, + erase: erase_data, }); Ok(self.client.send(request, None).await?) } From ea4b9635c9886b55176c01d854026a98c8ebc427 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 16 Sep 2024 14:58:19 +0300 Subject: [PATCH 115/979] ffi: add another method that tells the client if account deactivation is supported - i.e. only works for `m.login.password` --- bindings/matrix-sdk-ffi/src/client.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 42386ca1391..2414d32c44c 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1029,6 +1029,12 @@ impl Client { Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await))) } + /// Lets the user know whether this is an `m.login.password` based + /// auth and if the account can actually be deactivated + pub fn can_deactivate_account(&self) -> bool { + matches!(self.inner.auth_api(), Some(AuthApi::Matrix(_))) + } + /// Deactivate this account definitively. /// Similarly to `encryption::reset_identity` this /// will only work with password-based authentication (`m.login.password`) From a1bb7c0acc98f35a36b3b3d2dcfbc250b0b7dd24 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 16 Sep 2024 16:33:15 +0300 Subject: [PATCH 116/979] sdk: add account deactivation tests --- .../matrix-sdk/tests/integration/account.rs | 62 +++++++++++++++++++ crates/matrix-sdk/tests/integration/main.rs | 1 + 2 files changed, 63 insertions(+) create mode 100644 crates/matrix-sdk/tests/integration/account.rs diff --git a/crates/matrix-sdk/tests/integration/account.rs b/crates/matrix-sdk/tests/integration/account.rs new file mode 100644 index 00000000000..d87074e28e8 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/account.rs @@ -0,0 +1,62 @@ +use assert_matches::assert_matches; +use matrix_sdk_test::async_test; +use serde_json::json; +use wiremock::{ + matchers::{method, path}, + Mock, Request, ResponseTemplate, +}; + +use crate::logged_in_client_with_server; + +#[async_test] +async fn test_account_deactivation() { + #[derive(serde::Deserialize)] + struct Parameters { + pub id_server: Option, + pub erase: Option, + } + + let (client, server) = logged_in_client_with_server().await; + + { + let _scope = Mock::given(method("POST")) + .and(path("/_matrix/client/r0/account/deactivate")) + .respond_with(|req: &Request| { + let params: Parameters = req.body_json().unwrap(); + assert_eq!(params.id_server, Some("FirstIdentityServer".to_owned())); + assert_eq!(params.erase, None); + + ResponseTemplate::new(200).set_body_json(json!({ + "id_server_unbind_result": "success" + })) + }) + .expect(1) + .mount_as_scoped(&server) + .await; + + assert!(client + .account() + .deactivate(Some("FirstIdentityServer"), None, false) + .await + .is_ok()); + } + + { + let _scope = Mock::given(method("POST")) + .and(path("/_matrix/client/r0/account/deactivate")) + .respond_with(|req: &Request| { + let params: Parameters = req.body_json().unwrap(); + assert_eq!(params.id_server, None); + assert_eq!(params.erase, Some(true)); + + ResponseTemplate::new(200).set_body_json(json!({ + "id_server_unbind_result": "success" + })) + }) + .expect(1) + .mount_as_scoped(&server) + .await; + + assert!(client.account().deactivate(None, None, true).await.is_ok()); + } +} diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 69b79b05f73..9192d0233f9 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -10,6 +10,7 @@ use wiremock::{ Mock, MockGuard, MockServer, ResponseTemplate, }; +mod account; mod client; #[cfg(feature = "e2e-encryption")] mod encryption; From ea9da3bdad5c6b7b5ef21a948ce2ffde4fd5b0e0 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Sep 2024 15:48:04 +0200 Subject: [PATCH 117/979] tests: increase timeout duration when awaiting a timeline update --- testing/matrix-sdk-integration-testing/src/tests/timeline.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 19feb552055..bf8c670105f 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -265,12 +265,11 @@ async fn test_stale_local_echo_time_abort_edit() { // It is then sent. The timeline stream can be racy here: // // - either the local echo is marked as sent *before*, and we receive an update - // for this before - // the remote echo. + // for this before the remote echo. // - or the remote echo comes up faster. // // Handle both orderings. - while let Ok(Some(vector_diff)) = timeout(Duration::from_secs(1), stream.next()).await { + while let Ok(Some(vector_diff)) = timeout(Duration::from_secs(3), stream.next()).await { let VectorDiff::Set { index: 0, value: echo } = vector_diff else { panic!("unexpected diff: {vector_diff:#?}"); }; From b7ce2dc7e6c624f433b5ae9813801482fa9ae146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 16 Sep 2024 17:06:07 +0200 Subject: [PATCH 118/979] chore: Fix a clippy warning --- crates/matrix-sdk/tests/integration/account.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/matrix-sdk/tests/integration/account.rs b/crates/matrix-sdk/tests/integration/account.rs index d87074e28e8..64ca06a6655 100644 --- a/crates/matrix-sdk/tests/integration/account.rs +++ b/crates/matrix-sdk/tests/integration/account.rs @@ -1,4 +1,3 @@ -use assert_matches::assert_matches; use matrix_sdk_test::async_test; use serde_json::json; use wiremock::{ From 1429c1a06a7a97b496fdec147b54966e656dfd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 16 Sep 2024 15:22:02 +0200 Subject: [PATCH 119/979] test: Confirm that the notification client doesn't create duplicate one-time keys --- crates/matrix-sdk-ui/Cargo.toml | 2 +- .../integration/encryption_sync_service.rs | 252 +++++++++++++++++- 2 files changed, 250 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index db4bac2952b..0a757b1586c 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -55,7 +55,7 @@ assert-json-diff = { workspace = true } assert_matches = { workspace = true } assert_matches2 = { workspace = true } eyeball-im-util = { workspace = true } -matrix-sdk = { workspace = true, features = ["testing"] } +matrix-sdk = { workspace = true, features = ["testing", "sqlite"] } matrix-sdk-test = { workspace = true } stream_assert = { workspace = true } tempfile = "3.3.0" diff --git a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs index cc37762c94a..e97bd0b93a1 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -1,17 +1,35 @@ -use std::sync::{Arc, Mutex}; +use std::{ + collections::{BTreeMap, HashSet}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, +}; use futures_util::{pin_mut, StreamExt as _}; -use matrix_sdk::test_utils::logged_in_client_with_server; +use matrix_sdk::{ + config::RequestConfig, + matrix_auth::{MatrixSession, MatrixSessionTokens}, + test_utils::{logged_in_client_with_server, test_client_builder_with_server}, + SessionMeta, +}; use matrix_sdk_base::crypto::store::Changes; use matrix_sdk_test::async_test; use matrix_sdk_ui::encryption_sync_service::{ EncryptionSyncPermit, EncryptionSyncService, WithLocking, }; +use ruma::{device_id, user_id}; +use serde::Deserialize; use serde_json::json; use tokio::sync::Mutex as AsyncMutex; -use wiremock::{Mock, MockGuard, MockServer, Request, ResponseTemplate}; +use tracing::{error, info, trace, warn}; +use wiremock::{ + matchers::{method, path}, + Mock, MockGuard, MockServer, Request, ResponseTemplate, +}; use crate::{ + mock_sync, sliding_sync::{check_requests, PartialSlidingSyncRequest, SlidingSyncMatcher}, sliding_sync_then_assert_request_and_fake_response, }; @@ -320,3 +338,231 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result< Ok(()) } + +#[async_test] +async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> anyhow::Result<()> { + use tempfile::tempdir; + + let dir = tempdir().unwrap(); + let user_id = user_id!("@example:morpheus.localhost"); + + let (builder, server) = test_client_builder_with_server().await; + let client = builder + .request_config(RequestConfig::new().disable_retry()) + .sqlite_store(dir.path(), None) + .build() + .await + .unwrap(); + + let session = MatrixSession { + meta: SessionMeta { user_id: user_id.into(), device_id: device_id!("DEVICEID").to_owned() }, + tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None }, + }; + + client.restore_session(session.to_owned()).await.unwrap(); + + info!("Creating the notification client"); + let notification_client = client + .notification_client() + .await + .expect("We should be able to build a notification client"); + + let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing())); + let sync_permit_guard = sync_permit.lock_owned().await; + let encryption_sync = + EncryptionSyncService::new("tests".to_owned(), client.clone(), None, WithLocking::Yes) + .await?; + + let stream = encryption_sync.sync(sync_permit_guard); + pin_mut!(stream); + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/keys/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .mount(&server) + .await; + + info!("First sync, uploading 50 one-time keys"); + + sliding_sync_then_assert_request_and_fake_response! { + [server, stream] + assert request = { + "conn_id": "encryption", + "extensions": { + "e2ee": { + "enabled": true + }, + "to_device": { + "enabled": true + } + } + }, + respond with = { + "pos": "0", + "extensions": { + "to_device": { + "next_batch": "nb0" + }, + } + }, + }; + + #[derive(Debug, Deserialize)] + struct UploadRequest { + one_time_keys: BTreeMap, + } + + let found_duplicate = Arc::new(AtomicBool::new(false)); + let uploaded_key_ids = Arc::new(Mutex::new(HashSet::new())); + + Mock::given(method("POST")) + .and(path("/_matrix/client/r0/keys/upload")) + .respond_with({ + let found_duplicate = found_duplicate.clone(); + let uploaded_key_ids = uploaded_key_ids.clone(); + + move |request: &Request| { + let request: UploadRequest = request + .body_json() + .expect("The /keys/upload request should contain one-time keys"); + + let mut uploaded_key_ids = uploaded_key_ids.lock().unwrap(); + + let new_key_ids: HashSet = request.one_time_keys.into_keys().collect(); + + warn!(?new_key_ids, "Got a new /keys/upload request"); + + let duplicates: HashSet<_> = uploaded_key_ids.intersection(&new_key_ids).collect(); + + if let Some(duplicate) = duplicates.into_iter().next() { + error!("Duplicate one-time keys were uploaded."); + + found_duplicate.store(true, Ordering::SeqCst); + + ResponseTemplate::new(400).set_body_json(json!({ + "errcode": "M_WAT", + "error:": format!("One time key {duplicate} already exists!") + })) + } else { + trace!("No duplicate one-time keys found."); + uploaded_key_ids.extend(new_key_ids); + + ResponseTemplate::new(200).set_body_json(json!({ + "one_time_key_counts": { + "signed_curve25519": 50 + } + })) + } + } + }) + .expect(4) + .mount(&server) + .await; + + info!("Main sync now gets told that a one-time key has been used up."); + + sliding_sync_then_assert_request_and_fake_response! { + [server, stream] + assert request = { + "conn_id": "encryption", + "extensions": { + "to_device": { + "since": "nb0", + }, + } + }, + respond with = { + "pos": "2", + "extensions": { + "to_device": { + "next_batch": "nb2" + }, + "e2ee": { + "device_one_time_keys_count": { + "signed_curve25519": 49 + } + } + } + }, + }; + + assert!( + !found_duplicate.load(Ordering::SeqCst), + "The main sync should not have caused a duplicate one-time key" + ); + + mock_sync( + &server, + json!({ + "next_batch": "foo", + "device_one_time_keys_count": { + "signed_curve25519": 49 + } + }), + None, + ) + .await; + + info!("The notification client now syncs and tries to upload some one-time keys"); + + notification_client + .sync_once(Default::default()) + .await + .expect("The notification client should be able to sync successfully"); + + info!("Back to the main sync"); + + sliding_sync_then_assert_request_and_fake_response! { + [server, stream] + assert request = { + "conn_id": "encryption", + "extensions": { + "to_device": { + "since": "foo", + }, + } + }, + respond with = { + "pos": "2", + "extensions": { + "to_device": { + "next_batch": "nb4" + }, + "e2ee": { + "device_one_time_keys_count": { + "signed_curve25519": 49 + } + } + } + }, + }; + + sliding_sync_then_assert_request_and_fake_response! { + [server, stream] + assert request = { + "conn_id": "encryption", + "extensions": { + "to_device": { + "since": "nb4", + }, + } + }, + respond with = { + "pos": "2", + "extensions": { + "to_device": { + "next_batch": "nb5" + }, + } + }, + }; + + assert!( + !found_duplicate.load(Ordering::SeqCst), + "Duplicate one-time keys should not have been created" + ); + + server.verify().await; + + Ok(()) +} From f576c72ef8cf4d735e3f91e9ec52aba0278c643f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 13 Sep 2024 17:32:15 +0200 Subject: [PATCH 120/979] crypto: Avoid deep copying the OlmMachine when creating a NotificationClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NotificationClient, responsible for handling, fetching, and potentially decrypting events received via push notifications, creates a copy of the main Client object. During this process, the Client object is adjusted to use an in-memory state store to prevent concurrency issues from multiple sync loops attempting to write to the same database. This copying unintentionally recreated the OlmMachine with fresh data loaded from the database. If both Client instances were used for syncing without proper cross-process locking, forks of the vodozemac Account and Olm Sessions could be created and later persisted to the database. This behavior can lead to the duplication of one-time keys, cause sessions to lose their ability to decrypt messages, and result in the generation of undecryptable messages on the recipient’s side. --- crates/matrix-sdk-base/src/client.rs | 33 +++++++++++++++++++++++----- crates/matrix-sdk/src/client/mod.rs | 19 +--------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 211499d767e..586192b397c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -162,21 +162,42 @@ impl BaseClient { /// Clones the current base client to use the same crypto store but a /// different, in-memory store config, and resets transient state. #[cfg(feature = "e2e-encryption")] - pub fn clone_with_in_memory_state_store(&self) -> Self { + pub async fn clone_with_in_memory_state_store(&self) -> Result { let config = StoreConfig::new().state_store(MemoryStore::new()); let config = config.crypto_store(self.crypto_store.clone()); - let mut result = Self::with_store_config(config); - result.room_key_recipient_strategy = self.room_key_recipient_strategy.clone(); - result + let copy = Self { + store: Store::new(config.state_store), + event_cache_store: config.event_cache_store, + // We copy the crypto store as well as the `OlmMachine` for two reasons: + // 1. The `self.crypto_store` is the same as the one used inside the `OlmMachine`. + // 2. We need to ensure that the parent and child use the same data and caches inside + // the `OlmMachine` so the various ratchets and places where new randomness gets + // introduced don't diverge, i.e. one-time keys that get generated by the Olm Account + // or Olm sessions when they encrypt or decrypt messages. + crypto_store: self.crypto_store.clone(), + olm_machine: self.olm_machine.clone(), + ignore_user_list_changes: Default::default(), + room_info_notable_update_sender: self.room_info_notable_update_sender.clone(), + room_key_recipient_strategy: self.room_key_recipient_strategy.clone(), + }; + + if let Some(session_meta) = self.session_meta().cloned() { + copy.store + .set_session_meta(session_meta, ©.room_info_notable_update_sender) + .await?; + } + + Ok(copy) } /// Clones the current base client to use the same crypto store but a /// different, in-memory store config, and resets transient state. #[cfg(not(feature = "e2e-encryption"))] - pub fn clone_with_in_memory_state_store(&self) -> Self { + #[allow(clippy::unused_async)] + pub async fn clone_with_in_memory_state_store(&self) -> Result { let config = StoreConfig::new().state_store(MemoryStore::new()); - Self::with_store_config(config) + Ok(Self::with_store_config(config)) } /// Get the session meta information. diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index ae2d4ad608b..18c190907d4 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2204,7 +2204,7 @@ impl Client { #[cfg(feature = "experimental-sliding-sync")] self.sliding_sync_version(), self.inner.http_client.clone(), - self.inner.base_client.clone_with_in_memory_state_store(), + self.inner.base_client.clone_with_in_memory_state_store().await?, self.inner.server_capabilities.read().await.clone(), self.inner.respect_login_well_known, self.inner.event_cache.clone(), @@ -2215,23 +2215,6 @@ impl Client { .await, }; - // Copy the parent's session meta into the child. This initializes the in-memory - // state store of the child client with `SessionMeta`, and regenerates - // the `OlmMachine` if needs be. - // - // Note: we don't need to do a full `restore_session`, because this would - // overwrite the session information shared with the parent too, and it - // must be initialized at most once. - if let Some(session) = self.session() { - client - .set_session_meta( - session.into_meta(), - #[cfg(feature = "e2e-encryption")] - None, - ) - .await?; - } - Ok(client) } From 7a2728f8b5f91b7bcba6bf1d155a939aa0be1111 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 17 Sep 2024 13:20:07 +0200 Subject: [PATCH 121/979] feat(ui): Add logs when `RoomList` entries receive a lag. This patch updates `merge_stream_and_receiver` to display an `error!` when the room info receiver reads an error, like `Closed` or `Lagged`. This is helpful when debugging. --- .../src/room_list_service/room_list.rs | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index de7b4870270..0cde4bbcd40 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -26,8 +26,11 @@ use matrix_sdk::{ Client, SlidingSync, SlidingSyncList, }; use matrix_sdk_base::RoomInfoNotableUpdate; -use tokio::{select, sync::broadcast}; -use tracing::trace; +use tokio::{ + select, + sync::broadcast::{self, error::RecvError}, +}; +use tracing::{error, trace}; use super::{ filters::BoxedFilterFn, @@ -215,32 +218,27 @@ fn merge_stream_and_receiver( } } - Ok(update) = room_info_notable_update_receiver.recv() => { - // We are temporarily listening to all updates. - /* - use RoomInfoNotableUpdateReasons as NotableUpdate; - - let reasons = &update.reasons; - - // We are interested by these _reasons_. - if reasons.contains(NotableUpdate::LATEST_EVENT) || - reasons.contains(NotableUpdate::RECENCY_STAMP) || - reasons.contains(NotableUpdate::READ_RECEIPT) || - reasons.contains(NotableUpdate::UNREAD_MARKER) || - reasons.contains(NotableUpdate::MEMBERSHIP) { - */ - // Emit a `VectorDiff::Set` for the specific rooms. - if let Some(index) = raw_current_values.iter().position(|room| room.room_id() == update.room_id) { - let room = &raw_current_values[index]; - let update = VectorDiff::Set { index, value: room.clone() }; - /* - trace!(room = %room.room_id(), "updated because of notable reason: {reasons:?}"); - */ - yield vec![update]; + update = room_info_notable_update_receiver.recv() => { + match update { + Ok(update) => { + // Emit a `VectorDiff::Set` for the specific rooms. + if let Some(index) = raw_current_values.iter().position(|room| room.room_id() == update.room_id) { + let room = &raw_current_values[index]; + let update = VectorDiff::Set { index, value: room.clone() }; + yield vec![update]; + } + } + + Err(RecvError::Closed) => { + error!("Cannot receive room info notable updates because the sender has been closed"); + + break; + } + + Err(RecvError::Lagged(n)) => { + error!(number_of_missed_updates = n, "Lag when receiving room info notable update"); } - /* } - */ } } } From 72febaee5735c49bb81cf6038f379e0f2362e02a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 17 Sep 2024 15:25:37 +0200 Subject: [PATCH 122/979] feat(base): Increase the `room_info_notable_update_sender` capacity. This broadcast channel can easily be overflowed if more than 100 updates arrive at the time. This patch extends the capacity to 2^16 - 1. --- crates/matrix-sdk-base/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 586192b397c..780d3300c8c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -143,7 +143,7 @@ impl BaseClient { /// previous login call. pub fn with_store_config(config: StoreConfig) -> Self { let (room_info_notable_update_sender, _room_info_notable_update_receiver) = - broadcast::channel(100); + broadcast::channel(u16::MAX as usize); BaseClient { store: Store::new(config.state_store), From decdd6f47ee2aceb7086a83e0bd35b66a9d854cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 17 Sep 2024 16:07:17 +0200 Subject: [PATCH 123/979] crypto-ffi: update the x86-64 Android workaround to match `matrix-sdk-ffi` This workaround was applied to `matrix-sdk-ffi` and it should be used here too --- bindings/matrix-sdk-crypto-ffi/build.rs | 7 +++++-- bindings/matrix-sdk-ffi/build.rs | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/build.rs b/bindings/matrix-sdk-crypto-ffi/build.rs index a3572f17f7a..e66920150e3 100644 --- a/bindings/matrix-sdk-crypto-ffi/build.rs +++ b/bindings/matrix-sdk-crypto-ffi/build.rs @@ -5,6 +5,9 @@ use vergen::EmitBuilder; /// Adds a temporary workaround for an issue with the Rust compiler and Android /// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717. /// The workaround comes from: https://github.com/mozilla/application-services/pull/5442 +/// +/// IMPORTANT: if you modify this, make sure to modify +/// [../matrix-sdk-ffi/build.rs] too! fn setup_x86_64_android_workaround() { let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set"); @@ -18,11 +21,11 @@ fn setup_x86_64_android_workaround() { "Unsupported OS. You must use either Linux, MacOS or Windows to build the crate." ), }; - const DEFAULT_CLANG_VERSION: &str = "14.0.7"; + const DEFAULT_CLANG_VERSION: &str = "18"; let clang_version = env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned()); let linux_x86_64_lib_dir = format!( - "toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/" + "toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/" ); println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}"); println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android"); diff --git a/bindings/matrix-sdk-ffi/build.rs b/bindings/matrix-sdk-ffi/build.rs index 69aecbe2348..30a6b178a9f 100644 --- a/bindings/matrix-sdk-ffi/build.rs +++ b/bindings/matrix-sdk-ffi/build.rs @@ -5,6 +5,9 @@ use vergen::EmitBuilder; /// Adds a temporary workaround for an issue with the Rust compiler and Android /// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717. /// The workaround comes from: https://github.com/mozilla/application-services/pull/5442 +/// +/// IMPORTANT: if you modify this, make sure to modify +/// [../matrix-sdk-crypto-ffi/build.rs] too! fn setup_x86_64_android_workaround() { let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set"); From 8119697ef0398c8f0e6bb209ae933a86d8d49653 Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 10 Sep 2024 17:15:50 +0200 Subject: [PATCH 124/979] MatrixRTC: Fix different devices from the same user overwriting the room info state event. --- Cargo.lock | 18 +- Cargo.toml | 4 +- crates/matrix-sdk-base/src/rooms/mod.rs | 20 +- crates/matrix-sdk-base/src/rooms/normal.rs | 181 +++++++++++++----- .../src/store/migration_helpers.rs | 2 +- 5 files changed, 159 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a4925d995c..9a23b3552c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4934,7 +4934,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "assign", "js_int", @@ -4951,7 +4951,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "as_variant", "assign", @@ -4974,7 +4974,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "as_variant", "base64 0.22.1", @@ -5006,7 +5006,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "as_variant", "indexmap 2.2.6", @@ -5031,7 +5031,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "http", "js_int", @@ -5045,7 +5045,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.2.0" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "as_variant", "html5ever", @@ -5057,7 +5057,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "js_int", "thiserror", @@ -5066,7 +5066,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "cfg-if", "once_cell", @@ -5082,7 +5082,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=92a35381b56ffa3c2a611287bb5011f574271478#92a35381b56ffa3c2a611287bb5011f574271478" +source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index df695c571a8..9337eb47977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ once_cell = "1.16.0" pin-project-lite = "0.2.9" rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } -ruma = { git = "https://github.com/ruma/ruma", rev = "92a35381b56ffa3c2a611287bb5011f574271478", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -61,7 +61,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "92a35381b56ffa3c2a611287bb "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "92a35381b56ffa3c2a611287bb5011f574271478" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index 7038a15e962..fac5b164f99 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -19,7 +19,7 @@ use ruma::{ assign, events::{ beacon_info::BeaconInfoEventContent, - call::member::CallMemberEventContent, + call::member::{CallMemberEventContent, CallMemberStateKey}, macros::EventContent, room::{ avatar::RoomAvatarEventContent, @@ -112,7 +112,8 @@ pub struct BaseRoomInfo { /// All minimal state events that containing one or more running matrixRTC /// memberships. #[serde(skip_serializing_if = "BTreeMap::is_empty", default)] - pub(crate) rtc_member: BTreeMap>, + pub(crate) rtc_member_events: + BTreeMap>, /// Whether this room has been manually marked as unread. #[serde(default)] pub(crate) is_marked_unread: bool, @@ -195,14 +196,12 @@ impl BaseRoomInfo { let mut o_ev = o_ev.clone(); o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts); - // add the new event. - self.rtc_member.insert( - m.state_key().user_id().to_owned(), - SyncStateEvent::Original(o_ev).into(), - ); + // Add the new event. + self.rtc_member_events + .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into()); // Remove all events that don't contain any memberships anymore. - self.rtc_member.retain(|_, ev| { + self.rtc_member_events.retain(|_, ev| { ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty()) }); } @@ -302,7 +301,8 @@ impl BaseRoomInfo { } else if self.topic.has_event_id(redacts) { self.topic.as_mut().unwrap().redact(&room_version); } else { - self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts)); + self.rtc_member_events + .retain(|_, member_event| member_event.event_id() != Some(redacts)); } } @@ -367,7 +367,7 @@ impl Default for BaseRoomInfo { name: None, tombstone: None, topic: None, - rtc_member: BTreeMap::new(), + rtc_member_events: BTreeMap::new(), is_marked_unread: false, notable_tags: RoomNotableTags::empty(), pinned_events: None, diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 022b7b0ddb7..2c399a97856 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -30,7 +30,7 @@ use ruma::events::AnySyncTimelineEvent; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, events::{ - call::member::MembershipData, + call::member::{CallMemberStateKey, MembershipData}, ignored_user_list::IgnoredUserListEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ @@ -1485,10 +1485,10 @@ impl RoomInfo { /// associated UserId's in this room. /// /// The vector is ordered by oldest membership to newest. - fn active_matrix_rtc_memberships(&self) -> Vec<(OwnedUserId, MembershipData<'_>)> { + fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> { let mut v = self .base_info - .rtc_member + .rtc_member_events .iter() .filter_map(|(user_id, ev)| { ev.as_original().map(|ev| { @@ -1509,7 +1509,7 @@ impl RoomInfo { /// returns Memberships with application "m.call" and scope "m.room". /// /// The vector is ordered by oldest membership user to newest. - fn active_room_call_memberships(&self) -> Vec<(OwnedUserId, MembershipData<'_>)> { + fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> { self.active_matrix_rtc_memberships() .into_iter() .filter(|(_user_id, m)| m.is_room_call()) @@ -1531,7 +1531,10 @@ impl RoomInfo { /// /// The vector is ordered by oldest membership user to newest. pub fn active_room_call_participants(&self) -> Vec { - self.active_room_call_memberships().iter().map(|(user_id, _)| user_id.clone()).collect() + self.active_room_call_memberships() + .iter() + .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned()) + .collect() } /// Returns the latest (decrypted) event recorded for this room. @@ -1701,11 +1704,12 @@ mod tests { use matrix_sdk_test::{async_test, ALICE, BOB, CAROL}; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, + device_id, event_id, events::{ call::member::{ - Application, CallApplicationContent, CallMemberEventContent, CallMemberStateKey, - Focus, LegacyMembershipData, LegacyMembershipDataInit, LivekitFocus, - OriginalSyncCallMemberEvent, + ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent, + CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData, + LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent, }, room::{ canonical_alias::RoomCanonicalAliasEventContent, @@ -1722,8 +1726,8 @@ mod tests { owned_event_id, room_alias_id, room_id, serde::Raw, time::SystemTime, - user_id, EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, - UserId, + user_id, DeviceId, EventEncryptionAlgorithm, EventId, MilliSecondsSinceUnixEpoch, + OwnedEventId, OwnedUserId, UserId, }; use serde_json::json; use stream_assert::{assert_pending, assert_ready}; @@ -2729,51 +2733,101 @@ mod tests { .expect("date out of range") } - fn call_member_state_event( + fn legacy_membership_for_my_call( + device_id: &DeviceId, + membership_id: &str, + minutes_ago: u32, + ) -> LegacyMembershipData { + let (application, foci) = foci_and_application(); + assign!( + LegacyMembershipData::from(LegacyMembershipDataInit { + application, + device_id: device_id.to_owned(), + expires: Duration::from_millis(3_600_000), + foci_active: foci, + membership_id: membership_id.to_owned(), + }), + { created_ts: Some(timestamp(minutes_ago)) } + ) + } + + fn legacy_member_state_event( memberships: Vec, - ev_id: &str, + ev_id: &EventId, user_id: &UserId, ) -> AnySyncStateEvent { let content = CallMemberEventContent::new_legacy(memberships); AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent { content, - event_id: OwnedEventId::from_str(ev_id).unwrap(), + event_id: ev_id.to_owned(), sender: user_id.to_owned(), // we can simply use now here since this will be dropped when using a MinimalStateEvent // in the roomInfo origin_server_ts: timestamp(0), - state_key: CallMemberStateKey::from_str(user_id.as_str()) - // SAFETY: `user_id` is a valid `UserId` and cannot fail to be transformed into a - // `CallMemberStateKey`. - .expect("Failed to transform a `UserId` into a `CallMemberStateKey`"), + state_key: CallMemberStateKey::new(user_id.to_owned(), None, false), unsigned: StateUnsigned::new(), })) } - fn membership_for_my_call( - device_id: &str, - membership_id: &str, + struct InitData<'a> { + device_id: &'a DeviceId, minutes_ago: u32, - ) -> LegacyMembershipData { + } + + fn session_member_state_event( + ev_id: &EventId, + user_id: &UserId, + init_data: Option>, + ) -> AnySyncStateEvent { let application = Application::Call(CallApplicationContent::new( "my_call_id_1".to_owned(), ruma::events::call::member::CallScope::Room, )); - let foci_active = vec![Focus::Livekit(LivekitFocus::new( + let foci_preferred = vec![Focus::Livekit(LivekitFocus::new( "my_call_foci_alias".to_owned(), "https://lk.org".to_owned(), ))]; + let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new()); + let (content, state_key) = match init_data { + Some(InitData { device_id, minutes_ago }) => ( + CallMemberEventContent::new( + application, + device_id.to_owned(), + focus_active, + foci_preferred, + Some(timestamp(minutes_ago)), + ), + CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false), + ), + None => ( + CallMemberEventContent::new_empty(None), + CallMemberStateKey::new(user_id.to_owned(), None, false), + ), + }; - assign!( - LegacyMembershipData::from(LegacyMembershipDataInit { - application, - device_id: device_id.to_owned(), - expires: Duration::from_millis(3_600_000), - foci_active, - membership_id: membership_id.to_owned(), - }), - { created_ts: Some(timestamp(minutes_ago)) } + AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent { + content, + event_id: ev_id.to_owned(), + sender: user_id.to_owned(), + // we can simply use now here since this will be dropped when using a MinimalStateEvent + // in the roomInfo + origin_server_ts: timestamp(0), + state_key, + unsigned: StateUnsigned::new(), + })) + } + + fn foci_and_application() -> (Application, Vec) { + ( + Application::Call(CallApplicationContent::new( + "my_call_id_1".to_owned(), + ruma::events::call::member::CallScope::Room, + )), + vec![Focus::Livekit(LivekitFocus::new( + "my_call_foci_alias".to_owned(), + "https://lk.org".to_owned(), + ))], ) } @@ -2790,20 +2844,20 @@ mod tests { /// `user_a`: empty memberships /// `user_b`: one membership /// `user_c`: two memberships (two devices) - fn create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room { + fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room { let (_, room) = make_room_test_helper(RoomState::Joined); - let a_empty = call_member_state_event(Vec::new(), "$1234", a); + let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a); // make b 10min old - let m_init_b = membership_for_my_call("0", "0", 1); - let b_one = call_member_state_event(vec![m_init_b], "$12345", b); + let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1); + let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b); // c1 1min old - let m_init_c1 = membership_for_my_call("0", "0", 10); + let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10); // c2 20min old - let m_init_c2 = membership_for_my_call("1", "0", 20); - let c_two = call_member_state_event(vec![m_init_c1, m_init_c2], "$123456", c); + let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20); + let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c); // Intentionally use a non time sorted receive order. receive_state_events(&room, vec![&c_two, &a_empty, &b_one]); @@ -2811,26 +2865,65 @@ mod tests { room } + /// `user_a`: empty memberships + /// `user_b`: one membership + /// `user_c`: two memberships (two devices) + fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room { + let (_, room) = make_room_test_helper(RoomState::Joined); + + let a_empty = session_member_state_event(event_id!("$1234"), a, None); + + // make b 10min old + let b_one = session_member_state_event( + event_id!("$12345"), + b, + Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }), + ); + + let m_c1 = session_member_state_event( + event_id!("$123456_0"), + c, + Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }), + ); + let m_c2 = session_member_state_event( + event_id!("$123456_1"), + c, + Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }), + ); + // Intentionally use a non time sorted receive order1 + receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]); + + room + } + #[test] fn test_show_correct_active_call_state() { - let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL); + let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL); // This check also tests the ordering. // We want older events to be in the front. // user_b (Bob) is 1min old, c1 (CAROL) 10min old, c2 (CAROL) 20min old assert_eq!( vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()], - room.active_room_call_participants() + room_legacy.active_room_call_participants() + ); + assert!(room_legacy.has_active_room_call()); + + let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL); + assert_eq!( + vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()], + room_session.active_room_call_participants() ); - assert!(room.has_active_room_call()); + assert!(room_session.has_active_room_call()); } #[test] fn test_active_call_is_false_when_everyone_left() { - let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL); + let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL); - let b_empty_membership = call_member_state_event(Vec::new(), "$1234_1", &BOB); - let c_empty_membership = call_member_state_event(Vec::new(), "$12345_1", &CAROL); + let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB); + let c_empty_membership = + legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL); receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]); diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index 3d69fa40ad5..40076f19458 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -212,7 +212,7 @@ impl BaseRoomInfoV1 { name, tombstone, topic, - rtc_member: BTreeMap::new(), + rtc_member_events: BTreeMap::new(), is_marked_unread: false, notable_tags: RoomNotableTags::empty(), pinned_events: None, From 7d1bbfaa32518cbba70056ad8b5984f0b1953f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 17 Sep 2024 10:28:19 +0200 Subject: [PATCH 125/979] sdk-base: split `handle_account_data` and `process_direct_rooms` This removes a couple of TODOs in the codebase. --- crates/matrix-sdk-base/src/client.rs | 135 ++++++++++-------- .../matrix-sdk-base/src/sliding_sync/mod.rs | 27 ++-- 2 files changed, 93 insertions(+), 69 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 780d3300c8c..8be5a1f1ca0 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -688,13 +688,18 @@ impl BaseClient { } } + /// Parses and stores any raw global account data events into the + /// [`StateChanges`]. + /// + /// Returns a list with the parsed account data events. #[instrument(skip_all)] pub(crate) async fn handle_account_data( &self, events: &[Raw], changes: &mut StateChanges, - ) { + ) -> Vec { let mut account_data = BTreeMap::new(); + let mut parsed_events = Vec::new(); for raw_event in events { let event = match raw_event.deserialize() { @@ -705,64 +710,78 @@ impl BaseClient { continue; } }; + account_data.insert(event.event_type(), raw_event.clone()); + parsed_events.push(event); + } - if let AnyGlobalAccountDataEvent::Direct(e) = &event { - let mut new_dms = HashMap::<&RoomId, HashSet>::new(); - for (user_id, rooms) in e.content.iter() { - for room_id in rooms { - new_dms.entry(room_id).or_default().insert(user_id.clone()); - } + changes.account_data = account_data; + parsed_events + } + + /// Processes the direct rooms in a sync response: + /// + /// Given a [`StateChanges`] instance, processes any direct room info + /// from the global account data and adds it to the room infos to + /// save. + #[instrument(skip_all)] + pub(crate) async fn process_direct_rooms( + &self, + events: &[AnyGlobalAccountDataEvent], + changes: &mut StateChanges, + ) { + for event in events { + let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue }; + let mut new_dms = HashMap::<&RoomId, HashSet>::new(); + for (user_id, rooms) in direct_event.content.iter() { + for room_id in rooms { + new_dms.entry(room_id).or_default().insert(user_id.clone()); } + } - let rooms = self.store.rooms(); - let mut old_dms = rooms - .iter() - .filter_map(|r| { - let direct_targets = r.direct_targets(); - (!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets)) - }) - .collect::>(); - - // Update the direct targets of rooms if they changed. - for (room_id, new_direct_targets) in new_dms { - if let Some(old_direct_targets) = old_dms.remove(&room_id) { - if old_direct_targets == new_direct_targets { - continue; - } + let rooms = self.store.rooms(); + let mut old_dms = rooms + .iter() + .filter_map(|r| { + let direct_targets = r.direct_targets(); + (!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets)) + }) + .collect::>(); + + // Update the direct targets of rooms if they changed. + for (room_id, new_direct_targets) in new_dms { + if let Some(old_direct_targets) = old_dms.remove(&room_id) { + if old_direct_targets == new_direct_targets { + continue; } + } - trace!( - ?room_id, targets = ?new_direct_targets, - "Marking room as direct room" - ); + trace!( + ?room_id, targets = ?new_direct_targets, + "Marking room as direct room" + ); - if let Some(info) = changes.room_infos.get_mut(room_id) { - info.base_info.dm_targets = new_direct_targets; - } else if let Some(room) = self.store.room(room_id) { - let mut info = room.clone_info(); - info.base_info.dm_targets = new_direct_targets; - changes.add_room(info); - } + if let Some(info) = changes.room_infos.get_mut(room_id) { + info.base_info.dm_targets = new_direct_targets; + } else if let Some(room) = self.store.room(room_id) { + let mut info = room.clone_info(); + info.base_info.dm_targets = new_direct_targets; + changes.add_room(info); } + } - // Remove the targets of old direct chats. - for room_id in old_dms.keys() { - trace!(?room_id, "Unmarking room as direct room"); + // Remove the targets of old direct chats. + for room_id in old_dms.keys() { + trace!(?room_id, "Unmarking room as direct room"); - if let Some(info) = changes.room_infos.get_mut(*room_id) { - info.base_info.dm_targets.clear(); - } else if let Some(room) = self.store.room(room_id) { - let mut info = room.clone_info(); - info.base_info.dm_targets.clear(); - changes.add_room(info); - } + if let Some(info) = changes.room_infos.get_mut(*room_id) { + info.base_info.dm_targets.clear(); + } else if let Some(room) = self.store.room(room_id) { + let mut info = room.clone_info(); + info.base_info.dm_targets.clear(); + changes.add_room(info); } } - - account_data.insert(event.event_type(), raw_event.clone()); } - - changes.account_data = account_data; } #[cfg(feature = "e2e-encryption")] @@ -971,7 +990,8 @@ impl BaseClient { let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); - self.handle_account_data(&response.account_data.events, &mut changes).await; + let global_account_data_events = + self.handle_account_data(&response.account_data.events, &mut changes).await; let push_rules = self.get_push_rules(&changes).await?; @@ -1186,23 +1206,24 @@ impl BaseClient { new_rooms.invite.insert(room_id, new_info); } - // TODO remove this, we're processing account data events here again + // We're processing direct state events here separately // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. - let has_new_direct_room_data = response.account_data.events.iter().any(|raw_event| { - raw_event - .deserialize() - .map(|event| event.event_type() == GlobalAccountDataEventType::Direct) - .unwrap_or_default() - }); + let has_new_direct_room_data = global_account_data_events + .iter() + .any(|event| event.event_type() == GlobalAccountDataEventType::Direct); if has_new_direct_room_data { - self.handle_account_data(&response.account_data.events, &mut changes).await; + self.process_direct_rooms(&global_account_data_events, &mut changes).await; } else if let Ok(Some(direct_account_data)) = self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await { debug!("Found direct room data in the Store, applying it"); - self.handle_account_data(&[direct_account_data], &mut changes).await; + if let Ok(direct_account_data) = direct_account_data.deserialize() { + self.process_direct_rooms(&[direct_account_data], &mut changes).await; + } else { + warn!("Failed to deserialize direct room account data"); + } } changes.presence = response diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 825247b579e..5aa0993b09d 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -164,9 +164,11 @@ impl BaseClient { let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone()); let account_data = &extensions.account_data; - if !account_data.is_empty() { - self.handle_account_data(&account_data.global, &mut changes).await; - } + let global_account_data_events = if !account_data.is_empty() { + self.handle_account_data(&account_data.global, &mut changes).await + } else { + Vec::new() + }; let mut new_rooms = RoomUpdates::default(); let mut notifications = Default::default(); @@ -302,23 +304,24 @@ impl BaseClient { } } - // TODO remove this, we're processing account data events here again + // We're processing direct state events here separately // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. - let has_new_direct_room_data = account_data.global.iter().any(|raw_event| { - raw_event - .deserialize() - .map(|event| event.event_type() == GlobalAccountDataEventType::Direct) - .unwrap_or_default() - }); + let has_new_direct_room_data = global_account_data_events + .iter() + .any(|event| event.event_type() == GlobalAccountDataEventType::Direct); if has_new_direct_room_data { - self.handle_account_data(&account_data.global, &mut changes).await; + self.process_direct_rooms(&global_account_data_events, &mut changes).await; } else if let Ok(Some(direct_account_data)) = self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await { debug!("Found direct room data in the Store, applying it"); - self.handle_account_data(&[direct_account_data], &mut changes).await; + if let Ok(direct_account_data) = direct_account_data.deserialize() { + self.process_direct_rooms(&[direct_account_data], &mut changes).await; + } else { + warn!("Failed to deserialize direct room account data"); + } } // FIXME not yet supported by sliding sync. From 1b4f665d9972bae3fd5bf3adba04b768c7aaaff3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 18 Sep 2024 16:45:33 +0200 Subject: [PATCH 126/979] integration tests: update instructions (#4017) - explain what these tests are - mention that it's sometimes needed to rebuild the synapse image --------- Signed-off-by: Benjamin Bouvier Co-authored-by: Ivan Enderlin --- .../matrix-sdk-integration-testing/README.md | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/README.md b/testing/matrix-sdk-integration-testing/README.md index 8a03072cec0..76c31754d0c 100644 --- a/testing/matrix-sdk-integration-testing/README.md +++ b/testing/matrix-sdk-integration-testing/README.md @@ -1,9 +1,13 @@ # Matrix SDK integration test +This set of tests requires a Synapse instance, and it runs the tests from this directory against +this real-world server. As a result, these tests depend on the load of the machine/server, and as +such they might be more sensitive to timing issues than other tests in the code base. + ## Requirements -This requires a synapse backend with a ci patched configuration. You can easily get -it up and running with `docker compose` via: +This requires a synapse backend with a configuration patched for CI. You can get it up and running +with `docker compose` via: ```sh docker compose -f assets/docker-compose.yml up -d @@ -13,8 +17,8 @@ docker compose -f assets/docker-compose.yml logs --tail 100 -f Note that this works also with `podman compose`. **Patches** -You can see the patches we do to configuration (namely activate registration and -resetting rate limits), check out what `assets/ci-start.sh` changes. +You can see the patches we do to configuration (namely activate registration and resetting rate +limits, enable a few experimental features), check out what `assets/ci-start.sh` changes. ## Running @@ -22,12 +26,30 @@ The integration tests can be run with `cargo test` or `cargo nextest run`. The integration tests expect the environment variables `HOMESERVER_URL` to be the HTTP URL to access the synapse server and `HOMESERVER_DOMAIN` to be set to the domain configured in -that server. If you are using the provided `docker-compose.yml`, the default will be fine. +that server. These variables are set to a default value that matches the default `docker-compose.yml` +template; if you haven't touched it, you don't need to manually set those environment variables. ## Maintenance -To drop the database of your docker compose run: +### Delete all instance data + +To stop the services and drop the database of your docker-compose cluster, run: + +```bash +docker compose -f assets/docker-compose.yml down --volumes --remove-orphan -t 0 +``` + +### Rebuild the synapse image + +If the Synapse image has been updated in version, you may need to rebuild the custom Synapse image +using the following: + +```bash +docker compose -f assets/docker-compose.yml build --pull synapse +``` + +Then restart the synapse service: ```bash -docker compose -f assets/docker-compose.yml down --volumes +docker compose -f assets/docker-compose.yml up -d synapse ``` From 746d7e13abc89951a368c658b68029f5a9a0dfcd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 18 Sep 2024 16:29:29 +0200 Subject: [PATCH 127/979] tests: change strategy for `test_ensure_max_concurrency_is_observed` --- .../tests/integration/timeline/pinned_event.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index a46c6df9177..269e975b2ce 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -635,8 +635,6 @@ async fn test_ensure_max_concurrency_is_observed() { } ))); - // Amount of time to delay the response of an /event mock request, in ms. - let request_delay = 50; let pinned_event = EventFactory::new().room(&room_id).sender(*BOB).text_msg("A message").into_raw_timeline(); Mock::given(method("GET")) @@ -644,7 +642,7 @@ async fn test_ensure_max_concurrency_is_observed() { .and(header("authorization", "Bearer 1234")) .respond_with( ResponseTemplate::new(200) - .set_delay(Duration::from_millis(request_delay)) + .set_delay(Duration::from_secs(60)) .set_body_json(pinned_event.json()), ) // Verify this endpoint is only called the max concurrent amount of times. @@ -669,13 +667,16 @@ async fn test_ensure_max_concurrency_is_observed() { } }); - // Give it time to load events. As each request takes `request_delay`, we should - // have exactly `MAX_PINNED_EVENTS_CONCURRENT_REQUESTS` if the max - // concurrency setting is honoured. - sleep(Duration::from_millis(request_delay / 2)).await; + // Give the timeline enough time to spawn the maximum number of concurrent + // requests. + sleep(Duration::from_secs(2)).await; // Abort handle to stop requests from being processed. handle.abort(); + + // The real check happens here, based on the `max_concurrent_requests` expected + // value set above for the mock endpoint. + server.verify().await; } struct TestHelper { From e16ca9d8ba2bd97ccf08bbcd729235def110dece Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Wed, 18 Sep 2024 21:20:59 +0200 Subject: [PATCH 128/979] ffi: default to reldbg when building iOS bindings Relates to: #4009 Signed-off-by: Johannes Marbach --- xtask/src/swift.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs index 5546da7ea98..80ce0d08db0 100644 --- a/xtask/src/swift.rs +++ b/xtask/src/swift.rs @@ -64,7 +64,9 @@ impl SwiftArgs { target: targets, sequentially, } => { - let profile = profile.as_deref().unwrap_or(if release { "release" } else { "dev" }); + // The dev profile seems to cause crashes on some platforms so we default to reldbg + // https://github.com/matrix-org/matrix-rust-sdk/issues/4009 + let profile = profile.as_deref().unwrap_or(if release { "release" } else { "reldbg" }); build_xcframework(profile, targets, components_path, sequentially) } } From 79d8738ff5028f832617b5e07de425bc023ee857 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Wed, 18 Sep 2024 21:25:36 +0200 Subject: [PATCH 129/979] Reformat --- xtask/src/swift.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs index 80ce0d08db0..85b34770599 100644 --- a/xtask/src/swift.rs +++ b/xtask/src/swift.rs @@ -66,7 +66,8 @@ impl SwiftArgs { } => { // The dev profile seems to cause crashes on some platforms so we default to reldbg // https://github.com/matrix-org/matrix-rust-sdk/issues/4009 - let profile = profile.as_deref().unwrap_or(if release { "release" } else { "reldbg" }); + let profile = + profile.as_deref().unwrap_or(if release { "release" } else { "reldbg" }); build_xcframework(profile, targets, components_path, sequentially) } } From 2a03de3bd5ffd57dc63328964cb430be786d426e Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Wed, 18 Sep 2024 21:29:12 +0200 Subject: [PATCH 130/979] Reformat again... --- xtask/src/swift.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs index 85b34770599..f05a09d13e6 100644 --- a/xtask/src/swift.rs +++ b/xtask/src/swift.rs @@ -64,8 +64,8 @@ impl SwiftArgs { target: targets, sequentially, } => { - // The dev profile seems to cause crashes on some platforms so we default to reldbg - // https://github.com/matrix-org/matrix-rust-sdk/issues/4009 + // The dev profile seems to cause crashes on some platforms so we default to + // reldbg (https://github.com/matrix-org/matrix-rust-sdk/issues/4009) let profile = profile.as_deref().unwrap_or(if release { "release" } else { "reldbg" }); build_xcframework(profile, targets, components_path, sequentially) From 794dbb36dc863667e29e9298d96fd010c23e212d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Sep 2024 17:14:52 +0100 Subject: [PATCH 131/979] crypto: minor fixes to documentation on UserIdentity --- .../matrix-sdk-crypto/src/identities/user.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 72b89870608..7b5af637cc9 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -364,19 +364,18 @@ impl UserIdentity { Ok(()) } - /// Did the identity change after an initial observation in a way that - /// requires approval from the user? + /// Has the identity changed in a way that requires approval from the user? /// /// A user identity needs approval if it changed after the crypto machine - /// has already observed ("pinned") a different identity for that user *and* - /// it is not an explicitly verified identity (using for example interactive - /// verification). + /// has already observed ("pinned") a different identity for that user, + /// unless it is an explicitly verified identity (using for example + /// interactive verification). /// - /// Such a change is to be considered a pinning violation which the - /// application should report to the local user, and can be resolved by: + /// This situation can be resolved by: /// - /// - Verifying the new identity with [`UserIdentity::request_verification`] - /// - Or by updating the pin to the new identity with + /// - Verifying the new identity with + /// [`UserIdentity::request_verification`], or: + /// - Updating the pin to the new identity with /// [`UserIdentity::pin_current_master_key`]. pub fn identity_needs_user_approval(&self) -> bool { // First check if the current identity is verified. @@ -741,7 +740,7 @@ impl OtherUserIdentityData { /// True if we verified this identity (with any own identity, at any /// point). /// - /// To pass this latch back to false, one must call + /// To set this latch back to false, call /// [`OtherUserIdentityData::withdraw_verification()`]. pub fn was_previously_verified(&self) -> bool { self.previously_verified.load(Ordering::SeqCst) @@ -758,12 +757,13 @@ impl OtherUserIdentityData { /// Returns true if the identity has changed since we last pinned it. /// - /// Key pinning acts as a trust on first use mechanism, the first time an + /// Key pinning acts as a trust on first use mechanism: the first time an /// identity is known for a user it will be pinned. + /// /// For future interaction with a user, the identity is expected to be the /// one that was pinned. In case of identity change the UI client should - /// receive reports of pinning violation and decide to act accordingly; - /// that is accept and pin the new identity, perform a verification or + /// receive reports of pinning violation and decide to act accordingly: + /// accept and pin the new identity, perform a verification, or /// stop communications. pub(crate) fn has_pin_violation(&self) -> bool { let pinned_master_key = self.pinned_master_key.read().unwrap(); From 5e9f629edb44b639a73154c28d17fc89df0f9bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 12 Sep 2024 10:58:31 +0200 Subject: [PATCH 132/979] sdk-ui: make room encryption optional to create a timeline Instead of forcing the room encryption to be known when the timeline is created and failing if it's not known, take the latest room encryption info as a base value and update it when processing timeline events. At the time of writing this commit, the encryption info is only used to decide whether shields should be calculated for timeline items or not. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 11 +- .../src/timeline/controller/mod.rs | 30 ++++- .../src/timeline/controller/state.rs | 46 ++++++-- .../src/timeline/day_dividers.rs | 8 +- .../src/timeline/event_handler.rs | 8 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 2 + .../matrix-sdk-ui/src/timeline/tests/mod.rs | 18 ++- crates/matrix-sdk-ui/src/timeline/traits.rs | 9 +- .../tests/integration/timeline/mod.rs | 107 +++++++++++++++++- crates/matrix-sdk/src/test_utils/events.rs | 10 ++ 10 files changed, 224 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 04bfcad83fe..3553c2ecd71 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -158,8 +158,7 @@ impl TimelineBuilder { let (_, mut event_subscriber) = room_event_cache.subscribe().await?; let is_pinned_events = matches!(focus, TimelineFocus::PinnedEvents { .. }); - let is_room_encrypted = - room.is_encrypted().await.map_err(|_| Error::UnknownEncryptionState)?; + let is_room_encrypted = room.is_encrypted().await.ok(); let controller = TimelineController::new( room, @@ -196,6 +195,13 @@ impl TimelineBuilder { None }; + let encryption_changes_handle = spawn({ + let inner = controller.clone(); + async move { + inner.handle_encryption_state_changes().await; + } + }); + let room_update_join_handle = spawn({ let room_event_cache = room_event_cache.clone(); let inner = controller.clone(); @@ -421,6 +427,7 @@ impl TimelineBuilder { room_key_backup_enabled_join_handle, local_echo_listener_handle, _event_cache_drop_handle: event_cache_drop, + encryption_changes_handle, }), }; diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 33e47371fb2..cd78c04e624 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -240,7 +240,7 @@ impl TimelineController

{ focus: TimelineFocus, internal_id_prefix: Option, unable_to_decrypt_hook: Option>, - is_room_encrypted: bool, + is_room_encrypted: Option, ) -> Self { let (focus_data, focus_kind) = match focus { TimelineFocus::Live => (TimelineFocusData::Live, TimelineFocusKind::Live), @@ -345,6 +345,34 @@ impl TimelineController

{ } } + /// Listens to encryption state changes for the room in + /// [`matrix_sdk_base::RoomInfo`] and applies the new value to the + /// existing timeline items. This will then cause a refresh of those + /// timeline items. + pub async fn handle_encryption_state_changes(&self) { + let mut room_info = self.room_data_provider.room_info(); + + while let Some(info) = room_info.next().await { + let changed = { + let state = self.state.read().await; + let mut old_is_room_encrypted = state.meta.is_room_encrypted.write().unwrap(); + let is_encrypted_now = info.is_encrypted(); + + if *old_is_room_encrypted != Some(is_encrypted_now) { + *old_is_room_encrypted = Some(is_encrypted_now); + true + } else { + false + } + }; + + if changed { + let mut state = self.state.write().await; + state.update_all_events_is_room_encrypted(); + } + } + } + pub(crate) async fn reload_pinned_events( &self, ) -> Result, PinnedEventsLoaderError> { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index ef71404296b..7acef444b2f 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::VecDeque, future::Future, num::NonZeroUsize, sync::Arc}; +use std::{ + collections::VecDeque, + future::Future, + num::NonZeroUsize, + sync::{Arc, RwLock}, +}; use eyeball_im::{ObservableVector, ObservableVectorTransaction, ObservableVectorTransactionEntry}; use itertools::Itertools as _; @@ -83,7 +88,7 @@ impl TimelineState { room_version: RoomVersionId, internal_id_prefix: Option, unable_to_decrypt_hook: Option>, - is_room_encrypted: bool, + is_room_encrypted: Option, ) -> Self { Self { // Upstream default capacity is currently 16, which is making @@ -295,6 +300,16 @@ impl TimelineState { result } + pub(super) fn update_all_events_is_room_encrypted(&mut self) { + let is_room_encrypted = *self.meta.is_room_encrypted.read().unwrap(); + + // When this transaction finishes, all items in the timeline will be emitted + // again with the updated encryption value + let mut txn = self.transaction(); + txn.update_all_events_is_room_encrypted(is_room_encrypted); + txn.commit(); + } + pub(super) fn transaction(&mut self) -> TimelineStateTransaction<'_> { let items = self.items.transaction(); let meta = self.meta.clone(); @@ -720,6 +735,24 @@ impl TimelineStateTransaction<'_> { fn adjust_day_dividers(&mut self, mut adjuster: DayDividerAdjuster) { adjuster.run(&mut self.items, &mut self.meta); } + + /// This method replaces the `is_room_encrypted` value for all timeline + /// items to its updated version and creates a `VectorDiff::Set` operation + /// for each item which will be added to this transaction. + fn update_all_events_is_room_encrypted(&mut self, is_encrypted: Option) { + for idx in 0..self.items.len() { + let item = &self.items[idx]; + + if let Some(event) = item.as_event() { + let mut cloned_event = event.clone(); + cloned_event.is_room_encrypted = is_encrypted; + + // Replace the existing item with a new version with the right encryption flag + let item = item.with_kind(cloned_event); + self.items.set(idx, item); + } + } + } } #[derive(Clone)] @@ -754,10 +787,7 @@ pub(in crate::timeline) struct TimelineMetadata { /// A boolean indicating whether the room the timeline is attached to is /// actually encrypted or not. - /// TODO: this is misplaced, it should be part of the room provider as this - /// value can change over time when a room switches from non-encrypted - /// to encrypted, see also #3850. - pub(crate) is_room_encrypted: bool, + pub(crate) is_room_encrypted: Arc>>, /// Matrix room version of the timeline's room, or a sensible default. /// @@ -814,7 +844,7 @@ impl TimelineMetadata { room_version: RoomVersionId, internal_id_prefix: Option, unable_to_decrypt_hook: Option>, - is_room_encrypted: bool, + is_room_encrypted: Option, ) -> Self { Self { own_user_id, @@ -831,7 +861,7 @@ impl TimelineMetadata { room_version, unable_to_decrypt_hook, internal_id_prefix, - is_room_encrypted, + is_room_encrypted: Arc::new(RwLock::new(is_room_encrypted)), } } diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index ae873e68862..b83d7154220 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -643,7 +643,13 @@ mod tests { } fn test_metadata() -> TimelineMetadata { - TimelineMetadata::new(owned_user_id!("@a:b.c"), ruma::RoomVersionId::V11, None, None, false) + TimelineMetadata::new( + owned_user_id!("@a:b.c"), + ruma::RoomVersionId::V11, + None, + None, + Some(false), + ) } #[test] diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 40e35e8b5da..7bd1ea253fc 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -402,6 +402,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } TimelineEventKind::OtherState { state_key, content } => { + // Update room encryption if a `m.room.encryption` event is found in the + // timeline if should_add { self.add_item(TimelineItemContent::OtherState(OtherState { state_key, @@ -955,7 +957,11 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } }; - let is_room_encrypted = self.meta.is_room_encrypted; + let is_room_encrypted = if let Ok(is_room_encrypted) = self.meta.is_room_encrypted.read() { + is_room_encrypted.unwrap_or_default() + } else { + false + }; let mut item = EventTimelineItem::new( sender, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 58e52de8845..8db2e657a83 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -839,6 +839,7 @@ struct TimelineDropHandle { room_key_backup_enabled_join_handle: JoinHandle<()>, local_echo_listener_handle: JoinHandle<()>, _event_cache_drop_handle: Arc, + encryption_changes_handle: JoinHandle<()>, } impl Drop for TimelineDropHandle { @@ -855,6 +856,7 @@ impl Drop for TimelineDropHandle { self.room_update_join_handle.abort(); self.room_key_from_backups_join_handle.abort(); self.room_key_backup_enabled_join_handle.abort(); + self.encryption_changes_handle.abort(); } } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index bbc1f0bb6d6..590bf74b669 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -20,6 +20,7 @@ use std::{ sync::Arc, }; +use eyeball::{SharedObservable, Subscriber}; use eyeball_im::VectorDiff; use futures_core::Stream; use futures_util::FutureExt as _; @@ -33,8 +34,8 @@ use matrix_sdk::{ test_utils::events::EventFactory, BoxFuture, }; -use matrix_sdk_base::latest_event::LatestEvent; -use matrix_sdk_test::{EventBuilder, ALICE, BOB}; +use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo, RoomState}; +use matrix_sdk_test::{EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID}; use ruma::{ event_id, events::{ @@ -103,7 +104,7 @@ impl TestTimeline { TimelineFocus::Live, Some(prefix), None, - false, + Some(false), ), event_builder: EventBuilder::new(), factory: EventFactory::new(), @@ -117,7 +118,7 @@ impl TestTimeline { TimelineFocus::Live, None, None, - false, + Some(false), ), event_builder: EventBuilder::new(), factory: EventFactory::new(), @@ -131,7 +132,7 @@ impl TestTimeline { TimelineFocus::Live, None, Some(hook), - true, + Some(true), ), event_builder: EventBuilder::new(), factory: EventFactory::new(), @@ -146,7 +147,7 @@ impl TestTimeline { TimelineFocus::Live, None, None, - encrypted, + Some(encrypted), ), event_builder: EventBuilder::new(), factory: EventFactory::new(), @@ -444,4 +445,9 @@ impl RoomDataProvider for TestRoomDataProvider { } .boxed() } + + fn room_info(&self) -> Subscriber { + let info = RoomInfo::new(*DEFAULT_TEST_ROOM_ID, RoomState::Joined); + SharedObservable::new(info).subscribe() + } } diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index 49fa44163af..d8c3bd61dc0 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -14,6 +14,7 @@ use std::future::Future; +use eyeball::Subscriber; use futures_util::FutureExt as _; use indexmap::IndexMap; #[cfg(test)] @@ -22,7 +23,7 @@ use matrix_sdk::{ deserialized_responses::TimelineEvent, event_cache::paginator::PaginableRoom, BoxFuture, Result, Room, }; -use matrix_sdk_base::latest_event::LatestEvent; +use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo}; use ruma::{ events::{ fully_read::FullyReadEventContent, @@ -107,6 +108,8 @@ pub(super) trait RoomDataProvider: reason: Option<&'a str>, transaction_id: Option, ) -> BoxFuture<'a, Result<(), super::Error>>; + + fn room_info(&self) -> Subscriber; } impl RoomDataProvider for Room { @@ -271,6 +274,10 @@ impl RoomDataProvider for Room { } .boxed() } + + fn room_info(&self) -> Subscriber { + self.subscribe_info() + } } // Internal helper to make most of retry_event_decryption independent of a room diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index bd65ec8654e..460badeb2ee 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -26,14 +26,22 @@ use matrix_sdk_test::{ async_test, mocks::{mock_encryption_state, mock_redaction}, sync_timeline_event, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, - SyncResponseBuilder, + SyncResponseBuilder, BOB, +}; +use matrix_sdk_ui::{ + timeline::{ + AnyOtherFullStateEventContent, EventSendState, RoomExt, TimelineItemContent, + VirtualTimelineItem, + }, + RoomListService, Timeline, }; -use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent, VirtualTimelineItem}; use ruma::{ - event_id, events::room::message::RoomMessageEventContent, room_id, user_id, - MilliSecondsSinceUnixEpoch, + event_id, + events::room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, + room_id, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::json; +use stream_assert::{assert_next_matches, assert_pending}; use wiremock::{ matchers::{header, method, path_regex}, Mock, ResponseTemplate, @@ -621,6 +629,95 @@ async fn test_unpin_event_is_returning_an_error() { setup.reset_server().await; } +#[async_test] +async fn test_timeline_without_encryption_info() { + // No encryption is mocked for this client/server pair + let (client, server) = logged_in_client_with_server().await; + let _ = RoomListService::new(client.clone()).await.unwrap(); + + let room_id = room_id!("!a98sd12bjh:example.org"); + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let f = EventFactory::new().room(room_id).sender(*BOB); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("A message").into_raw_sync()), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + // Previously this would have panicked. + let timeline = room.timeline().await.unwrap(); + + let (items, _) = timeline.subscribe().await; + assert_eq!(items.len(), 2); + assert!(items[0].as_virtual().is_some()); + // No encryption, no shields + assert!(items[1].as_event().unwrap().get_shield(false).is_none()); +} + +#[async_test] +async fn test_timeline_without_encryption_can_update() { + // No encryption is mocked for this client/server pair + let (client, server) = logged_in_client_with_server().await; + let _ = RoomListService::new(client.clone()).await.unwrap(); + + let room_id = room_id!("!jEsUZKDJdhlrceRyVU:example.org"); + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let f = EventFactory::new().room(room_id).sender(*BOB); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("A message").into_raw_sync()), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + // Previously this would have panicked. + // We're creating a timeline without read receipts tracking to check only the + // encryption changes + let timeline = Timeline::builder(&room).build().await.unwrap(); + + let (items, mut stream) = timeline.subscribe().await; + assert_eq!(items.len(), 2); + assert!(items[0].as_virtual().is_some()); + // No encryption, no shields + assert!(items[1].as_event().unwrap().get_shield(false).is_none()); + + let encryption_event_content = RoomEncryptionEventContent::with_recommended_defaults(); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.event(encryption_event_content).state_key("").into_raw_sync()) + .add_timeline_event(f.text_msg("An encrypted message").into_raw_sync()), + ); + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // Previous timeline event now has a shield + assert_next_matches!(stream, VectorDiff::Set { index, value } => { + assert_eq!(index, 1); + assert!(value.as_event().unwrap().get_shield(false).is_some()); + }); + // Room encryption event is received + assert_next_matches!(stream, VectorDiff::PushBack { value } => { + assert_let!(TimelineItemContent::OtherState(other_state) = value.as_event().unwrap().content()); + assert_let!(AnyOtherFullStateEventContent::RoomEncryption(_) = other_state.content()); + assert!(value.as_event().unwrap().get_shield(false).is_some()); + }); + // New message event is received and has a shield + assert_next_matches!(stream, VectorDiff::PushBack { value } => { + assert!(value.as_event().unwrap().get_shield(false).is_some()); + }); + assert_pending!(stream); +} + struct PinningTestSetup<'a> { event_id: &'a ruma::EventId, room_id: &'a ruma::RoomId, @@ -647,7 +744,7 @@ impl PinningTestSetup<'_> { Self { event_id, room_id, client, server, sync_settings, sync_builder } } - async fn timeline(&self) -> matrix_sdk_ui::Timeline { + async fn timeline(&self) -> Timeline { mock_encryption_state(&self.server, false).await; let room = self.client.get_room(self.room_id).unwrap(); room.timeline().await.unwrap() diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index 36728e29727..ce7b0bb5f1f 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -57,6 +57,7 @@ pub struct EventBuilder { content: E, server_ts: MilliSecondsSinceUnixEpoch, unsigned: Option, + state_key: Option, } impl EventBuilder @@ -89,6 +90,11 @@ where self } + pub fn state_key(mut self, state_key: impl Into) -> Self { + self.state_key = Some(state_key.into()); + self + } + #[inline(always)] fn construct_json(self, requires_room: bool) -> Raw { let event_id = self @@ -119,6 +125,9 @@ where if let Some(unsigned) = self.unsigned { map.insert("unsigned".to_owned(), json!(unsigned)); } + if let Some(state_key) = self.state_key { + map.insert("state_key".to_owned(), json!(state_key)); + } Raw::new(map).unwrap().cast() } @@ -237,6 +246,7 @@ impl EventFactory { redacts: None, content, unsigned: None, + state_key: None, } } From 3492bd6929724807dbe322be72bd32bc8dfa874b Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 24 Sep 2024 12:40:53 +0100 Subject: [PATCH 133/979] doc: Fix a typo in an error message --- crates/matrix-sdk/src/encryption/recovery/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/encryption/recovery/mod.rs b/crates/matrix-sdk/src/encryption/recovery/mod.rs index c5d818764c2..0d051216c63 100644 --- a/crates/matrix-sdk/src/encryption/recovery/mod.rs +++ b/crates/matrix-sdk/src/encryption/recovery/mod.rs @@ -575,7 +575,7 @@ impl Recovery { async fn update_recovery_state_no_fail(&self) { if let Err(e) = self.update_recovery_state().await { - error!("Coulnd't update the recovery state: {e:?}"); + error!("Couldn't update the recovery state: {e:?}"); } } From 40f1ce80ea09e6a3b8941936cb673e4629c5fb39 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 11 Sep 2024 16:24:29 +0200 Subject: [PATCH 134/979] test: Bye bye SS proxy, hello Synapse \o/. This patch removes the sliding sync proxy, and makes the `matrix-sdk-integration-testing` tests to run against Synapse with MSC4186 enabled. --- .github/workflows/ci.yml | 15 ++------------- .github/workflows/coverage.yml | 14 +------------- .../assets/ci-start.sh | 1 + .../assets/docker-compose.yml | 15 --------------- .../matrix-sdk-integration-testing/src/helpers.rs | 15 ++++++--------- .../src/tests/sliding_sync/mod.rs | 4 ++-- .../src/tests/sliding_sync/notification_client.rs | 4 ++++ .../src/tests/sliding_sync/room.rs | 12 ++++++++++-- 8 files changed, 26 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82666e8e113..7959a51bce0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -335,7 +335,7 @@ jobs: # run several docker containers with the same networking stack so the hostname 'postgres' # maps to the postgres container, etc. services: - # sliding sync needs a postgres container + # synapse needs a postgres container postgres: # Docker Hub image image: postgres @@ -353,21 +353,10 @@ jobs: ports: # Maps tcp port 5432 on service container to the host - 5432:5432 - # run sliding sync and point it at the postgres container and synapse container. - # the postgres container needs to be above this to make sure it has started prior to this service. - slidingsync: - image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./coverage.yml - env: - SYNCV3_SERVER: "http://synapse:8008" - SYNCV3_SECRET: "SUPER_CI_SECRET" - SYNCV3_BINDADDR: ":8118" - SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres" - ports: - - 8118:8118 # tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the # latter does not provide networking for services to communicate with it. synapse: - image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./coverage.yml + image: ghcr.io/matrix-org/synapse-service:v1.114.0 # keep in sync with ./coverage.yml env: SYNAPSE_COMPLEMENT_DATABASE: sqlite SERVER_NAME: synapse diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d0f04243867..05964459c3b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -29,7 +29,6 @@ jobs: # run several docker containers with the same networking stack so the hostname 'postgres' # maps to the postgres container, etc. services: - # sliding sync needs a postgres container postgres: # Docker Hub image image: postgres @@ -47,21 +46,10 @@ jobs: ports: # Maps tcp port 5432 on service container to the host - 5432:5432 - # run sliding sync and point it at the postgres container and synapse container. - # the postgres container needs to be above this to make sure it has started prior to this service. - slidingsync: - image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./ci.yml - env: - SYNCV3_SERVER: "http://synapse:8008" - SYNCV3_SECRET: "SUPER_CI_SECRET" - SYNCV3_BINDADDR: ":8118" - SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres" - ports: - - 8118:8118 # tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the # latter does not provide networking for services to communicate with it. synapse: - image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./ci.yml + image: ghcr.io/matrix-org/synapse-service:v1.114.0 # keep in sync with ./ci.yml env: SYNAPSE_COMPLEMENT_DATABASE: sqlite SERVER_NAME: synapse diff --git a/testing/matrix-sdk-integration-testing/assets/ci-start.sh b/testing/matrix-sdk-integration-testing/assets/ci-start.sh index 2c7e9b3efdd..fb41f9735b6 100644 --- a/testing/matrix-sdk-integration-testing/assets/ci-start.sh +++ b/testing/matrix-sdk-integration-testing/assets/ci-start.sh @@ -59,6 +59,7 @@ rc_invites: experimental_features: msc3266_enabled: true + msc4186_enabled: true """ >> /data/homeserver.yaml echo " ====== Starting server with: ====== " diff --git a/testing/matrix-sdk-integration-testing/assets/docker-compose.yml b/testing/matrix-sdk-integration-testing/assets/docker-compose.yml index 5617afa7ccb..a3072f09217 100644 --- a/testing/matrix-sdk-integration-testing/assets/docker-compose.yml +++ b/testing/matrix-sdk-integration-testing/assets/docker-compose.yml @@ -24,21 +24,6 @@ services: volumes: - db:/var/lib/postgresql/data - sliding-sync-proxy: - image: ghcr.io/matrix-org/sliding-sync:v0.99.11 - depends_on: - synapse: - condition: service_started - postgres: - condition: service_healthy - environment: - SYNCV3_SERVER: http://synapse:8008 - SYNCV3_SECRET: SUPER_SECRET - SYNCV3_BINDADDR: ":8338" - SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres" - ports: - - 8338:8338 - volumes: synapse: db: diff --git a/testing/matrix-sdk-integration-testing/src/helpers.rs b/testing/matrix-sdk-integration-testing/src/helpers.rs index d8fbf1a5ddf..9b4ee6a3020 100644 --- a/testing/matrix-sdk-integration-testing/src/helpers.rs +++ b/testing/matrix-sdk-integration-testing/src/helpers.rs @@ -20,7 +20,6 @@ use matrix_sdk::{ }; use once_cell::sync::Lazy; use rand::Rng as _; -use reqwest::Url; use tempfile::{tempdir, TempDir}; use tokio::{sync::Mutex, time::sleep}; @@ -75,21 +74,19 @@ impl TestClientBuilder { self } + pub fn http_proxy(mut self, url: String) -> Self { + self.http_proxy = Some(url); + self + } + fn common_client_builder(&self) -> ClientBuilder { let homeserver_url = option_env!("HOMESERVER_URL").unwrap_or("http://localhost:8228").to_owned(); - let sliding_sync_proxy_url = - option_env!("SLIDING_SYNC_PROXY_URL").unwrap_or("http://localhost:8338").to_owned(); let mut client_builder = Client::builder() .user_agent("matrix-sdk-integration-tests") .homeserver_url(homeserver_url) - // Disable MSC4186 for the integration tests as, at the time of writing - // (2024-07-15), we use a Synapse version that doesn't support MSC4186. - .sliding_sync_version_builder(VersionBuilder::Proxy { - url: Url::parse(&sliding_sync_proxy_url) - .expect("Sliding sync proxy URL is invalid"), - }) + .sliding_sync_version_builder(VersionBuilder::Native) .with_encryption_settings(self.encryption_settings) .request_config(RequestConfig::short_retry()); diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/mod.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/mod.rs index 58419212825..98036d034ed 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/mod.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/mod.rs @@ -1,2 +1,2 @@ -// mod notification_client; -// mod room; +mod notification_client; +mod room; diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs index 6e25093768f..e7378b56508 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs @@ -1,3 +1,6 @@ +// TODO: Remove this once all tests are re-enabled. +#![allow(unused)] + use std::sync::Arc; use anyhow::{ensure, Result}; @@ -28,6 +31,7 @@ use tracing::warn; use crate::helpers::TestClientBuilder; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore] async fn test_notification() -> Result<()> { // Create new users for each test run, to avoid conflicts with invites existing // from previous runs. diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index c42d3e7dddf..263e7f60d87 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -1,3 +1,6 @@ +// TODO: Remove this once all tests are re-enabled. +#![allow(unused)] + use std::{ sync::{Arc, Mutex as StdMutex}, time::Duration, @@ -31,6 +34,7 @@ use matrix_sdk::{ space::SpaceRoomJoinRule, RoomId, }, + sliding_sync::VersionBuilder, Client, RoomInfo, RoomMemberships, RoomState, SlidingSyncList, SlidingSyncMode, }; use matrix_sdk_base::sliding_sync::http; @@ -118,6 +122,7 @@ async fn test_left_room() -> Result<()> { } #[tokio::test] +#[ignore] async fn test_room_avatar_group_conversation() -> Result<()> { let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; @@ -268,12 +273,11 @@ async fn test_joined_user_can_create_push_context_with_room_list_service() -> Re // And a new device for Alice that uses sliding sync, let hs = alice.homeserver(); - let sliding_sync_version = alice.sliding_sync_version(); let alice_id = alice.user_id().unwrap().localpart().to_owned(); let alice = Client::builder() .homeserver_url(hs) - .sliding_sync_version(sliding_sync_version) + .sliding_sync_version_builder(VersionBuilder::Native) .build() .await .unwrap(); @@ -354,6 +358,7 @@ impl UpdateObserver { } #[tokio::test] +#[ignore] async fn test_room_notification_count() -> Result<()> { use tokio::time::timeout; @@ -715,6 +720,7 @@ impl wiremock::Respond for &CustomResponder { } #[tokio::test] +#[ignore] async fn test_delayed_decryption_latest_event() -> Result<()> { let server = MockServer::start().await; @@ -902,6 +908,7 @@ async fn test_delayed_invite_response_and_sent_message_decryption() -> Result<() } #[tokio::test] +#[ignore] async fn test_room_info_notable_update_deduplication() -> Result<()> { let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; @@ -990,6 +997,7 @@ async fn test_room_info_notable_update_deduplication() -> Result<()> { } #[tokio::test] +#[ignore] async fn test_room_preview() -> Result<()> { let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; From bda2acf5f694201368dac44a8fc869310ba3fb6c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Sep 2024 13:15:33 +0200 Subject: [PATCH 135/979] use precise Dockerfile version --- testing/matrix-sdk-integration-testing/assets/Dockerfile | 2 +- testing/matrix-sdk-integration-testing/assets/ci-start.sh | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/assets/Dockerfile b/testing/matrix-sdk-integration-testing/assets/Dockerfile index b68b2519096..cb214e69a12 100644 --- a/testing/matrix-sdk-integration-testing/assets/Dockerfile +++ b/testing/matrix-sdk-integration-testing/assets/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/matrixdotorg/synapse:latest +FROM docker.io/matrixdotorg/synapse:v1.114.0 ADD ci-start.sh /ci-start.sh RUN chmod 770 /ci-start.sh ENTRYPOINT /ci-start.sh diff --git a/testing/matrix-sdk-integration-testing/assets/ci-start.sh b/testing/matrix-sdk-integration-testing/assets/ci-start.sh index fb41f9735b6..2c7e9b3efdd 100644 --- a/testing/matrix-sdk-integration-testing/assets/ci-start.sh +++ b/testing/matrix-sdk-integration-testing/assets/ci-start.sh @@ -59,7 +59,6 @@ rc_invites: experimental_features: msc3266_enabled: true - msc4186_enabled: true """ >> /data/homeserver.yaml echo " ====== Starting server with: ====== " From 5ba90611b43915926a37be83f3de1d4d53bca6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 17 Sep 2024 15:23:31 +0200 Subject: [PATCH 136/979] ui: Allow to subscribe to read receipt changes in timeline metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- Cargo.lock | 30 +++++++++++++------ Cargo.toml | 2 +- crates/matrix-sdk-ui/Cargo.toml | 1 + .../src/timeline/controller/mod.rs | 5 ++++ crates/matrix-sdk-ui/src/timeline/mod.rs | 5 ++++ .../src/timeline/read_receipts.rs | 20 ++++++++++++- .../integration/timeline/read_receipts.rs | 11 +++++++ 7 files changed, 63 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a23b3552c3..be76deb7752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1150,7 +1150,7 @@ dependencies = [ "bitflags 2.6.0", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -3528,6 +3528,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-stream", "tracing", "unicode-normalization", "uniffi", @@ -3609,6 +3610,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "multiverse" version = "0.1.0" @@ -5547,7 +5560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -5966,26 +5979,25 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.2", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9337eb47977..531d3d59158 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ sha2 = "0.10.8" similar-asserts = "1.5.0" stream_assert = "0.1.1" thiserror = "1.0.38" -tokio = { version = "1.30.0", default-features = false, features = ["sync"] } +tokio = { version = "1.39.1", default-features = false, features = ["sync"] } tokio-stream = "0.1.14" tracing = { version = "0.1.40", default-features = false, features = ["std"] } tracing-core = "0.1.32" diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index 0a757b1586c..cd1293b6420 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -45,6 +45,7 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } +tokio-stream = { workspace = true, features = ["sync"] } tracing = { workspace = true, features = ["attributes"] } unicode-normalization = "0.1.22" uniffi = { workspace = true, optional = true } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index cd78c04e624..e543f054074 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1233,6 +1233,11 @@ impl TimelineController

{ self.state.read().await.latest_user_read_receipt_timeline_event_id(user_id) } + /// Subscribe to changes in the read receipts of our own user. + pub async fn subscribe_own_user_read_receipts_changed(&self) -> impl Stream { + self.state.read().await.meta.read_receipts.subscribe_own_user_read_receipts_changed() + } + /// Handle a room send update that's a new local echo. pub(crate) async fn handle_local_echo(&self, echo: LocalEcho) { match echo.content { diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 8db2e657a83..bd501f59562 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -681,6 +681,11 @@ impl Timeline { self.controller.latest_user_read_receipt_timeline_event_id(user_id).await } + /// Subscribe to changes in the read receipts of our own user. + pub async fn subscribe_own_user_read_receipts_changed(&self) -> impl Stream { + self.controller.subscribe_own_user_read_receipts_changed().await + } + /// Send the given receipt. /// /// This uses [`Room::send_single_receipt`] internally, but checks diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs index c309134c7ec..4287877670a 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs @@ -19,11 +19,14 @@ use std::{ }; use eyeball_im::ObservableVectorTransaction; +use futures_core::Stream; use indexmap::IndexMap; use ruma::{ events::receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType}, EventId, OwnedEventId, OwnedUserId, UserId, }; +use tokio::sync::watch; +use tokio_stream::wrappers::WatchStream; use tracing::{debug, error, warn}; use super::{ @@ -48,6 +51,9 @@ pub(super) struct ReadReceipts { /// User ID => Receipt type => Read receipt of the user of the given /// type. latest_by_user: HashMap>, + + /// A sender to notify of changes to the receipts of our own user. + own_user_read_receipts_changed_sender: watch::Sender<()>, } impl ReadReceipts { @@ -57,6 +63,12 @@ impl ReadReceipts { self.latest_by_user.clear(); } + /// Subscribe to changes in the read receipts of our own user. + pub(super) fn subscribe_own_user_read_receipts_changed(&self) -> impl Stream { + let subscriber = self.own_user_read_receipts_changed_sender.subscribe(); + WatchStream::from_changes(subscriber) + } + /// Read the latest read receipt of the given type for the given user, from /// the in-memory cache. fn get_latest( @@ -176,7 +188,13 @@ impl ReadReceipts { (new_receipt.event_id.to_owned(), new_receipt.receipt.clone()), ); - if is_own_user_id || new_item_event_id == old_item_event_id { + if is_own_user_id { + self.own_user_read_receipts_changed_sender.send_replace(()); + // This receipt cannot change items in the timeline. + return; + } + + if new_item_event_id == old_item_event_id { // The receipt did not change in the timeline. return; } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs index f16d9f299f3..cd9817f80b3 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs @@ -35,6 +35,7 @@ use ruma::{ room_id, user_id, RoomVersionId, }; use serde_json::json; +use stream_assert::{assert_pending, assert_ready}; use wiremock::{ matchers::{body_json, header, method, path_regex}, Mock, ResponseTemplate, @@ -76,8 +77,10 @@ async fn test_read_receipts_updates() { let room = client.get_room(room_id).unwrap(); let timeline = room.timeline().await.unwrap(); let (items, mut timeline_stream) = timeline.subscribe().await; + let mut own_receipts_subscriber = timeline.subscribe_own_user_read_receipts_changed().await; assert!(items.is_empty()); + assert_pending!(own_receipts_subscriber); let own_receipt = timeline.latest_user_read_receipt(own_user_id).await; assert_matches!(own_receipt, None); @@ -137,6 +140,9 @@ async fn test_read_receipts_updates() { let (own_receipt_event_id, _) = timeline.latest_user_read_receipt(own_user_id).await.unwrap(); assert_eq!(own_receipt_event_id, first_event.event_id().unwrap()); + assert_ready!(own_receipts_subscriber); + assert_pending!(own_receipts_subscriber); + // Implicit read receipt of @alice:localhost. assert_let!(Some(VectorDiff::PushBack { value: second_item }) = timeline_stream.next().await); let second_event = second_item.as_event().unwrap(); @@ -257,6 +263,8 @@ async fn test_read_receipts_updates() { let (bob_receipt_event_id, _) = timeline.latest_user_read_receipt(bob).await.unwrap(); assert_eq!(bob_receipt_event_id, third_event_id); + assert_pending!(own_receipts_subscriber); + // Private read receipt is updated. sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( EphemeralTestEvent::Custom(json!({ @@ -280,6 +288,9 @@ async fn test_read_receipts_updates() { let (own_user_receipt_event_id, _) = timeline.latest_user_read_receipt(own_user_id).await.unwrap(); assert_eq!(own_user_receipt_event_id, second_event_id); + + assert_ready!(own_receipts_subscriber); + assert_pending!(own_receipts_subscriber); } #[async_test] From 03f780600043a40e95284d4a4be987c2f24f98d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 25 Sep 2024 10:41:22 +0200 Subject: [PATCH 137/979] sdk-ui & sdk: add a relationship type filter to `load_event_with_relations` We need this for pinned events, where we want reactions, edits and redactions, but we don't want replies or threaded replies. --- .../src/timeline/pinned_events_loader.rs | 18 +- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 3 +- crates/matrix-sdk/src/event_cache/mod.rs | 171 +++++++++++++++--- 3 files changed, 161 insertions(+), 31 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs index 7debb031037..88bc56d5c53 100644 --- a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs +++ b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs @@ -20,7 +20,7 @@ use matrix_sdk::{ SendOutsideWasm, SyncOutsideWasm, }; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; -use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId}; +use ruma::{events::relation::RelationType, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId}; use thiserror::Error; use tracing::{debug, warn}; @@ -76,8 +76,13 @@ impl PinnedEventsLoader { let mut loaded_events: Vec = stream::iter(pinned_event_ids.into_iter().map(|event_id| { let provider = self.room.clone(); + let relations_filter = + Some(vec![RelationType::Annotation, RelationType::Replacement]); async move { - match provider.load_event_with_relations(&event_id, request_config).await { + match provider + .load_event_with_relations(&event_id, request_config, relations_filter) + .await + { Ok((event, related_events)) => { let mut events = vec![event]; events.extend(related_events); @@ -117,10 +122,15 @@ impl PinnedEventsLoader { pub trait PinnedEventsRoom: SendOutsideWasm + SyncOutsideWasm { /// Load a single room event using the cache or network and any events /// related to it, if they are cached. + /// + /// You can control which types of related events are retrieved using + /// `related_event_filters`. A `None` value will retrieve any type of + /// related event. fn load_event_with_relations<'a>( &'a self, event_id: &'a EventId, request_config: Option, + related_event_filters: Option>, ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>>; /// Get the pinned event ids for a room. @@ -138,10 +148,12 @@ impl PinnedEventsRoom for Room { &'a self, event_id: &'a EventId, request_config: Option, + related_event_filters: Option>, ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>> { async move { if let Ok((cache, _handles)) = self.event_cache().await { - if let Some(ret) = cache.event_with_relations(event_id).await { + if let Some(ret) = cache.event_with_relations(event_id, related_event_filters).await + { debug!("Loaded pinned event {event_id} and related events from cache"); return Ok(ret); } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 590bf74b669..5c12916221f 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -41,7 +41,7 @@ use ruma::{ events::{ reaction::ReactionEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, - relation::Annotation, + relation::{Annotation, RelationType}, AnyMessageLikeEventContent, AnyTimelineEvent, EmptyStateKey, RedactedMessageLikeEventContent, RedactedStateEventContent, StaticStateEventContent, }, @@ -345,6 +345,7 @@ impl PinnedEventsRoom for TestRoomDataProvider { &'a self, _event_id: &'a EventId, _request_config: Option, + _related_event_filters: Option>, ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>> { unimplemented!(); } diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 2c12911712b..46ba056b80d 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -42,6 +42,7 @@ #![forbid(missing_docs)] use std::{ + cmp::Ordering, collections::{BTreeMap, BTreeSet}, fmt::Debug, sync::{Arc, OnceLock}, @@ -56,6 +57,7 @@ use matrix_sdk_common::executor::{spawn, JoinHandle}; use paginator::PaginatorState; use ruma::{ events::{ + relation::RelationType, room::{message::Relation, redaction::SyncRoomRedactionEvent}, AnyMessageLikeEventContent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, @@ -320,7 +322,32 @@ impl EventCache { } type AllEventsMap = BTreeMap; -type RelationsMap = BTreeMap>; +type RelationsMap = BTreeMap>; + +/// Contains relationship information for a related event. +#[derive(Clone, Eq, PartialEq)] +struct RelatedEvent { + related_event_id: OwnedEventId, + relation_type: RelationType, +} + +impl RelatedEvent { + fn new(related_event_id: OwnedEventId, relation_type: RelationType) -> Self { + RelatedEvent { related_event_id, relation_type } + } +} + +impl PartialOrd for RelatedEvent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for RelatedEvent { + fn cmp(&self, other: &Self) -> Ordering { + self.related_event_id.cmp(&other.related_event_id) + } +} /// Cache wrapper containing both copies of received events and lists of event /// ids related to them. @@ -520,16 +547,20 @@ impl RoomEventCache { None } - /// Try to find an event by id in this room, along with all relations. + /// Try to find an event by id in this room, along with its related events. + /// + /// You can filter which types of related events to retrieve using + /// `filter`. `None` will retrieve related events of any type. pub async fn event_with_relations( &self, event_id: &EventId, + filter: Option>, ) -> Option<(SyncTimelineEvent, Vec)> { let mut relation_events = Vec::new(); let cache = self.inner.all_events_cache.read().await; if let Some((_, event)) = cache.events.get(event_id) { - Self::collect_related_events(&cache, event_id, &mut relation_events); + Self::collect_related_events(&cache, event_id, &filter, &mut relation_events); Some((event.clone(), relation_events)) } else { None @@ -542,19 +573,28 @@ impl RoomEventCache { fn collect_related_events( cache: &RwLockReadGuard<'_, AllEventsCache>, event_id: &EventId, + filter: &Option>, results: &mut Vec, ) { if let Some(related_event_ids) = cache.relations.get(event_id) { - for id in related_event_ids { + for RelatedEvent { related_event_id, relation_type } in related_event_ids { + if let Some(filter) = filter { + if !filter.contains(relation_type) { + continue; + } + } + // If the event was already added to the related ones, skip it. if results.iter().any(|e| { - e.event_id().is_some_and(|added_related_event_id| added_related_event_id == *id) + e.event_id().is_some_and(|added_related_event_id| { + added_related_event_id == *related_event_id + }) }) { continue; } - if let Some((_, ev)) = cache.events.get(id) { + if let Some((_, ev)) = cache.events.get(related_event_id) { results.push(ev.clone()); - Self::collect_related_events(cache, id, results); + Self::collect_related_events(cache, related_event_id, filter, results); } } } @@ -813,20 +853,24 @@ impl RoomEventCacheInner { { if let Some(redacted_event_id) = ev.content.redacts.as_ref().or(ev.redacts.as_ref()) { - cache - .relations - .entry(redacted_event_id.to_owned()) - .or_default() - .insert(ev.event_id.to_owned()); + cache.relations.entry(redacted_event_id.to_owned()).or_default().insert( + RelatedEvent::new(ev.event_id.to_owned(), RelationType::Replacement), + ); } } else { - let original_event_id = match ev.original_content() { + let relationship = match ev.original_content() { Some(AnyMessageLikeEventContent::RoomMessage(c)) => { if let Some(relation) = c.relates_to { match relation { - Relation::Replacement(replacement) => Some(replacement.event_id), - Relation::Reply { in_reply_to } => Some(in_reply_to.event_id), - Relation::Thread(thread) => Some(thread.event_id), + Relation::Replacement(replacement) => { + Some((replacement.event_id, RelationType::Replacement)) + } + Relation::Reply { in_reply_to } => { + Some((in_reply_to.event_id, RelationType::Reference)) + } + Relation::Thread(thread) => { + Some((thread.event_id, RelationType::Thread)) + } // Do nothing for custom _ => None, } @@ -835,21 +879,29 @@ impl RoomEventCacheInner { } } Some(AnyMessageLikeEventContent::PollResponse(c)) => { - Some(c.relates_to.event_id) + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::PollEnd(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) } - Some(AnyMessageLikeEventContent::PollEnd(c)) => Some(c.relates_to.event_id), Some(AnyMessageLikeEventContent::UnstablePollResponse(c)) => { - Some(c.relates_to.event_id) + Some((c.relates_to.event_id, RelationType::Reference)) } Some(AnyMessageLikeEventContent::UnstablePollEnd(c)) => { - Some(c.relates_to.event_id) + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::Reaction(c)) => { + Some((c.relates_to.event_id, RelationType::Annotation)) } - Some(AnyMessageLikeEventContent::Reaction(c)) => Some(c.relates_to.event_id), _ => None, }; - if let Some(event_id) = original_event_id { - cache.relations.entry(event_id).or_default().insert(ev.event_id().to_owned()); + if let Some(relationship) = relationship { + cache + .relations + .entry(relationship.0) + .or_default() + .insert(RelatedEvent::new(ev.event_id().to_owned(), relationship.1)); } } } @@ -994,8 +1046,11 @@ mod tests { use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::async_test; use ruma::{ - event_id, events::room::message::RoomMessageEventContentWithoutRelation, room_id, - serde::Raw, user_id, RoomId, + event_id, + events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation}, + room_id, + serde::Raw, + user_id, RoomId, }; use serde_json::json; @@ -1288,6 +1343,68 @@ mod tests { .await; } + #[async_test] + async fn test_event_with_filtered_relationships() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let associated_related_id = event_id!("$recursive_related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let event_factory = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + let original_event = event_factory.text_msg("Original event").event_id(original_id).into(); + let related_event = event_factory + .text_msg("* Edited event") + .edit(original_id, RoomMessageEventContentWithoutRelation::text_plain("Edited event")) + .event_id(related_id) + .into(); + let associated_related_event = + event_factory.redaction(related_id).event_id(associated_related_id).into(); + + let client = logged_in_client(None).await; + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + // Save the original event. + room_event_cache.save_event(original_event).await; + + // Save the related event. + room_event_cache.save_event(related_event).await; + + // Save the associated related event, which redacts the related event. + room_event_cache.save_event(associated_related_event).await; + + let filter = Some(vec![RelationType::Replacement]); + let (event, related_events) = + room_event_cache.event_with_relations(original_id, filter).await.unwrap(); + // Fetched event is the right one. + let cached_event_id = event.event_id().unwrap(); + assert_eq!(cached_event_id, original_id); + + // There are both the related id and the associatively related id + assert_eq!(related_events.len(), 2); + + let related_event_id = related_events[0].event_id().unwrap(); + assert_eq!(related_event_id, related_id); + let related_event_id = related_events[1].event_id().unwrap(); + assert_eq!(related_event_id, associated_related_id); + + // Now we'll filter threads instead, there should be no related events + let filter = Some(vec![RelationType::Thread]); + let (event, related_events) = + room_event_cache.event_with_relations(original_id, filter).await.unwrap(); + // Fetched event is the right one. + let cached_event_id = event.event_id().unwrap(); + assert_eq!(cached_event_id, original_id); + // No Thread related events found + assert!(related_events.is_empty()); + } + #[async_test] async fn test_event_with_recursive_relation() { let original_id = event_id!("$original"); @@ -1325,7 +1442,7 @@ mod tests { room_event_cache.save_event(associated_related_event).await; let (event, related_events) = - room_event_cache.event_with_relations(original_id).await.unwrap(); + room_event_cache.event_with_relations(original_id, None).await.unwrap(); // Fetched event is the right one. let cached_event_id = event.event_id().unwrap(); assert_eq!(cached_event_id, original_id); @@ -1370,7 +1487,7 @@ mod tests { room_event_cache.save_event(related_event).await; let (event, related_events) = - room_event_cache.event_with_relations(&original_event_id).await.unwrap(); + room_event_cache.event_with_relations(&original_event_id, None).await.unwrap(); // Fetched event is the right one. let cached_event_id = event.event_id().unwrap(); assert_eq!(cached_event_id, original_event_id); From 8469c6465edc8e5ec399b17e52c2db2bd2fa1f79 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 13:53:50 +0200 Subject: [PATCH 138/979] test(integration): Update Synapse to 1.115. --- .github/workflows/ci.yml | 2 +- .github/workflows/coverage.yml | 2 +- testing/matrix-sdk-integration-testing/assets/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7959a51bce0..4067e8e7fc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -356,7 +356,7 @@ jobs: # tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the # latter does not provide networking for services to communicate with it. synapse: - image: ghcr.io/matrix-org/synapse-service:v1.114.0 # keep in sync with ./coverage.yml + image: ghcr.io/matrix-org/synapse-service:v1.115.0 # keep in sync with ./coverage.yml env: SYNAPSE_COMPLEMENT_DATABASE: sqlite SERVER_NAME: synapse diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 05964459c3b..8a9e24da0c2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -49,7 +49,7 @@ jobs: # tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the # latter does not provide networking for services to communicate with it. synapse: - image: ghcr.io/matrix-org/synapse-service:v1.114.0 # keep in sync with ./ci.yml + image: ghcr.io/matrix-org/synapse-service:v1.115.0 # keep in sync with ./ci.yml env: SYNAPSE_COMPLEMENT_DATABASE: sqlite SERVER_NAME: synapse diff --git a/testing/matrix-sdk-integration-testing/assets/Dockerfile b/testing/matrix-sdk-integration-testing/assets/Dockerfile index cb214e69a12..e34ca68958a 100644 --- a/testing/matrix-sdk-integration-testing/assets/Dockerfile +++ b/testing/matrix-sdk-integration-testing/assets/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/matrixdotorg/synapse:v1.114.0 +FROM docker.io/matrixdotorg/synapse:v1.115.0 ADD ci-start.sh /ci-start.sh RUN chmod 770 /ci-start.sh ENTRYPOINT /ci-start.sh From c3caf6cbca8ec88a960575db0dc2263a84680f2c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 10:43:04 +0200 Subject: [PATCH 139/979] test(integration): Enable `test_notification`. This test was failing since the migration from the sliding sync proxy to Synapse. This patch fixes the test. The failing part was: ```rust assert_eq!(notification.joined_members_count, 1); ``` This patch changes the value from 1 to 0. Indeed, Synapse doesn't share this data for the sake of privacy because the room is not joined. A comment has been made on MSC4186 to precise this behaviour: https://github.com/matrix-org/matrix-spec-proposals/pull/4186#discussion_r1774775560. Moreover, this test was asserting a bug (which is alright), but now a bug report has been made. The patch contains the link to this bug report. The code has been a bit rewritten to make it simpler, and more comments have been added. --- .../tests/sliding_sync/notification_client.rs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs index e7378b56508..30bfd3bae3f 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs @@ -31,7 +31,6 @@ use tracing::warn; use crate::helpers::TestClientBuilder; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] -#[ignore] async fn test_notification() -> Result<()> { // Create new users for each test run, to avoid conflicts with invites existing // from previous runs. @@ -61,14 +60,16 @@ async fn test_notification() -> Result<()> { let room_id = alice_room.room_id().to_owned(); // Bob receives a notification about it. - let bob_invite_response = bob.sync_once(Default::default()).await?; let sync_token = bob_invite_response.next_batch; - let mut invited_rooms = bob_invite_response.rooms.invite.into_iter(); + let mut invited_rooms = bob_invite_response.rooms.invite; + + assert_eq!(invited_rooms.len(), 1, "must be only one invitation"); - let (_, invited_room) = invited_rooms.next().expect("must be invited to one room"); - assert!(invited_rooms.next().is_none(), "no more invited rooms: {invited_rooms:#?}"); + let Some(invited_room) = invited_rooms.get(&room_id) else { + panic!("bob is invited in an unexpected room"); + }; if let Some(event_id) = invited_room.invite_state.events.iter().find_map(|event| { let Ok(AnyStrippedStateEvent::RoomMember(room_member_ev)) = event.deserialize() else { @@ -90,6 +91,7 @@ async fn test_notification() -> Result<()> { // Try with sliding sync first. let notification_client = NotificationClient::new(bob.clone(), process_setup.clone()).await.unwrap(); + assert_let!( NotificationStatus::Event(notification) = notification_client.get_notification_with_sliding_sync(&room_id, &event_id).await? @@ -97,8 +99,8 @@ async fn test_notification() -> Result<()> { warn!("sliding_sync: checking invite notification"); + assert_matches!(notification.event, NotificationEvent::Invite(_)); assert_eq!(notification.event.sender(), alice.user_id().unwrap()); - assert_eq!(notification.joined_members_count, 1); assert_eq!(notification.is_room_encrypted, None); assert!(notification.is_direct_message_room); @@ -109,8 +111,10 @@ async fn test_notification() -> Result<()> { assert_eq!(notification.sender_display_name.as_deref(), Some(ALICE_NAME)); // In theory, the room name ought to be ROOM_NAME here, but the sliding sync - // proxy returns the other person's name as the room's name (as of + // server returns the other person's name as the room's name (as of // 2023-08-04). + // + // See https://github.com/element-hq/synapse/issues/17763. assert!(notification.room_computed_display_name != ROOM_NAME); assert_eq!(notification.room_computed_display_name, ALICE_NAME); @@ -125,7 +129,7 @@ async fn test_notification() -> Result<()> { warn!("Couldn't get the invite event."); } - // Bob accepts the invite, joins the room. + // Bob inspects the invite room. { let room = bob.get_room(&room_id).expect("bob doesn't know about the room"); ensure!( @@ -137,13 +141,16 @@ async fn test_notification() -> Result<()> { assert_eq!(sender.user_id(), alice.user_id().expect("alice has a user_id")); } - // Bob joins the room. - bob.get_room(alice_room.room_id()).unwrap().join().await?; + bob.get_room(alice_room.room_id()) + .unwrap() + // Bob joins the room. + .join() + .await?; // Now Alice sends a message to Bob. alice_room.send(RoomMessageEventContent::text_plain("Hello world!")).await?; - // In this sync, bob receives the message from Alice. + // In this sync, Bob receives the message from Alice. let bob_response = bob.sync_once(SyncSettings::default().token(sync_token)).await?; let mut joined_rooms = bob_response.rooms.join.into_iter(); @@ -201,6 +208,7 @@ async fn test_notification() -> Result<()> { assert_eq!(notification.room_computed_display_name, ROOM_NAME); }; + // Check with sliding sync. let notification_client = NotificationClient::new(bob.clone(), process_setup.clone()).await.unwrap(); assert_let!( @@ -209,6 +217,7 @@ async fn test_notification() -> Result<()> { ); check_notification(true, notification); + // Check with `/context`. let notification_client = NotificationClient::new(bob.clone(), process_setup).await.unwrap(); let notification = notification_client .get_notification_with_context(&room_id, &event_id) From a5dc8ff871b8a3f6b6849d1f5866b879c496a73c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 13:36:05 +0200 Subject: [PATCH 140/979] test(integration): Remove what seems to be a bug but it's not. When Bob receives the invite, the room has the correct name. Bob to sync more to receive the new name. This is not a bug. This patch updates the `CreateRoomRequest` to set the correct name immediately. --- .../tests/sliding_sync/notification_client.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs index 30bfd3bae3f..54292df470b 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs @@ -46,17 +46,15 @@ async fn test_notification() -> Result<()> { alice.account().set_display_name(Some(ALICE_NAME)).await?; // Initial setup: Alice creates a room, invites Bob. + const ROOM_NAME: &str = "Kingdom of Integration Testing"; let invite = vec![bob.user_id().expect("bob has a userid!").to_owned()]; let request = assign!(CreateRoomRequest::new(), { invite, + name: Some(ROOM_NAME.to_owned()), is_direct: true, }); let alice_room = alice.create_room(request).await?; - - const ROOM_NAME: &str = "Kingdom of Integration Testing"; - alice_room.set_name(ROOM_NAME.to_owned()).await?; - let room_id = alice_room.room_id().to_owned(); // Bob receives a notification about it. @@ -109,14 +107,7 @@ async fn test_notification() -> Result<()> { }); assert_eq!(notification.sender_display_name.as_deref(), Some(ALICE_NAME)); - - // In theory, the room name ought to be ROOM_NAME here, but the sliding sync - // server returns the other person's name as the room's name (as of - // 2023-08-04). - // - // See https://github.com/element-hq/synapse/issues/17763. - assert!(notification.room_computed_display_name != ROOM_NAME); - assert_eq!(notification.room_computed_display_name, ALICE_NAME); + assert_eq!(notification.room_computed_display_name, ROOM_NAME); // Then with /context. let notification_client = @@ -141,7 +132,7 @@ async fn test_notification() -> Result<()> { assert_eq!(sender.user_id(), alice.user_id().expect("alice has a user_id")); } - bob.get_room(alice_room.room_id()) + bob.get_room(&room_id) .unwrap() // Bob joins the room. .join() From 88ceeb3513cc81cd83bf07b7f6ab970d5def2a73 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 15:35:48 +0200 Subject: [PATCH 141/979] testing again --- .../src/tests/sliding_sync/notification_client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs index 54292df470b..6a178cd71d6 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs @@ -99,6 +99,7 @@ async fn test_notification() -> Result<()> { assert_matches!(notification.event, NotificationEvent::Invite(_)); assert_eq!(notification.event.sender(), alice.user_id().unwrap()); + assert_eq!(notification.joined_members_count, 0); assert_eq!(notification.is_room_encrypted, None); assert!(notification.is_direct_message_room); From fcb1c96869e479289fcc5a201f6e94466da1ae9a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 14:43:44 +0200 Subject: [PATCH 142/979] feat(sdk): Sliding Sync has a required `timeline_limit` now. Since MSC4186, the `timeline_limit` value is required. This patch uses 1 as the default value for `timeline_limit`, and forces the `timeline_limit` to be defined everywhere. --- crates/matrix-sdk/src/sliding_sync/list/builder.rs | 11 +++++------ crates/matrix-sdk/src/sliding_sync/list/mod.rs | 13 +++++-------- crates/matrix-sdk/src/sliding_sync/list/sticky.rs | 10 +++++----- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs index 01673744d55..fe4b2d1638a 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -37,7 +37,7 @@ pub struct SlidingSyncListBuilder { required_state: Vec<(StateEventType, String)>, include_heroes: Option, filters: Option, - timeline_limit: Option, + timeline_limit: Bound, pub(crate) name: String, /// Should this list be cached and reloaded from the cache? @@ -76,7 +76,7 @@ impl SlidingSyncListBuilder { ], include_heroes: None, filters: None, - timeline_limit: None, + timeline_limit: 1, name: name.into(), reloaded_cached_data: None, cache_policy: SlidingSyncListCachePolicy::Disabled, @@ -123,14 +123,13 @@ impl SlidingSyncListBuilder { /// Set the limit of regular events to fetch for the timeline. pub fn timeline_limit(mut self, timeline_limit: Bound) -> Self { - self.timeline_limit = Some(timeline_limit); + self.timeline_limit = timeline_limit; self } - /// Reset the limit of regular events to fetch for the timeline. It is left - /// to the server to decide how many to send back + /// Set the limit of regular events to fetch for the timeline to 0. pub fn no_timeline_limit(mut self) -> Self { - self.timeline_limit = Default::default(); + self.timeline_limit = 0; self } diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index 9e34e2ed1fc..c0e07a1d8cb 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -132,12 +132,12 @@ impl SlidingSyncList { } /// Get the timeline limit. - pub fn timeline_limit(&self) -> Option { + pub fn timeline_limit(&self) -> Bound { self.inner.sticky.read().unwrap().data().timeline_limit() } /// Set timeline limit. - pub fn set_timeline_limit(&self, timeline: Option) { + pub fn set_timeline_limit(&self, timeline: Bound) { self.inner.sticky.write().unwrap().data_mut().set_timeline_limit(timeline); } @@ -594,13 +594,10 @@ mod tests { .timeline_limit(7) .build(sender); - assert_eq!(list.inner.sticky.read().unwrap().data().timeline_limit(), Some(7)); + assert_eq!(list.inner.sticky.read().unwrap().data().timeline_limit(), 7); - list.set_timeline_limit(Some(42)); - assert_eq!(list.inner.sticky.read().unwrap().data().timeline_limit(), Some(42)); - - list.set_timeline_limit(None); - assert_eq!(list.inner.sticky.read().unwrap().data().timeline_limit(), None); + list.set_timeline_limit(42); + assert_eq!(list.inner.sticky.read().unwrap().data().timeline_limit(), 42); } macro_rules! assert_ranges { diff --git a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs index 3770cc7a0b6..ff6adb4f833 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs @@ -19,7 +19,7 @@ pub(super) struct SlidingSyncListStickyParameters { filters: Option, /// The maximum number of timeline events to query for. - timeline_limit: Option, + timeline_limit: Bound, } impl SlidingSyncListStickyParameters { @@ -27,7 +27,7 @@ impl SlidingSyncListStickyParameters { required_state: Vec<(StateEventType, String)>, include_heroes: Option, filters: Option, - timeline_limit: Option, + timeline_limit: Bound, ) -> Self { // Consider that each list will have at least one parameter set, so invalidate // it by default. @@ -36,11 +36,11 @@ impl SlidingSyncListStickyParameters { } impl SlidingSyncListStickyParameters { - pub(super) fn timeline_limit(&self) -> Option { + pub(super) fn timeline_limit(&self) -> Bound { self.timeline_limit } - pub(super) fn set_timeline_limit(&mut self, timeline: Option) { + pub(super) fn set_timeline_limit(&mut self, timeline: Bound) { self.timeline_limit = timeline; } } @@ -50,7 +50,7 @@ impl StickyData for SlidingSyncListStickyParameters { fn apply(&self, request: &mut Self::Request) { request.room_details.required_state = self.required_state.to_vec(); - request.room_details.timeline_limit = self.timeline_limit.map(Into::into); + request.room_details.timeline_limit = Some(self.timeline_limit.into()); request.include_heroes = self.include_heroes; request.filters = self.filters.clone(); } From 2562aa3feea8d9e6e8b5dd43f329129e62e341b1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 15:21:02 +0200 Subject: [PATCH 143/979] chore(test): Clean a test by rewriting the code a little bit. --- .../src/tests/sliding_sync/room.rs | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 263e7f60d87..baeb3ca8a62 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -330,6 +330,7 @@ impl UpdateObserver { async fn next(&mut self) -> Option { // Wait for the room info updates to stabilize. let mut update = None; + while let Ok(Some(up)) = timeout(Duration::from_secs(2), self.subscriber.next()).await { update = Some(up); } @@ -363,23 +364,24 @@ async fn test_room_notification_count() -> Result<()> { use tokio::time::timeout; let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; + let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; - // Spawn sync for bob. - let b = bob.clone(); - spawn(async move { - let bob = b; - loop { - if let Err(err) = bob.sync(Default::default()).await { - tracing::error!("bob sync error: {err}"); + // Spawn sync for Bob. + spawn({ + let bob = bob.clone(); + + async move { + loop { + if let Err(err) = bob.sync(Default::default()).await { + tracing::error!("bob sync error: {err}"); + } } } }); - // Set up sliding sync for alice. - let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; - + // Spawn sync for Alice (with sliding sync). spawn({ - let sync = alice + let alice_sync = alice .sliding_sync("main")? .with_receipt_extension( assign!(http::request::Receipts::default(), { enabled: Some(true) }), @@ -400,22 +402,30 @@ async fn test_room_notification_count() -> Result<()> { .await?; async move { - let stream = sync.sync(); + let stream = alice_sync.sync(); pin_mut!(stream); - while let Some(up) = stream.next().await { - warn!("alice sliding sync received an update: {up:?}"); + + while let Some(update) = stream.next().await { + warn!(?update, "Alice sliding sync received an update"); + + assert!(update.is_ok(), "Syncing Alice via sliding sync has failed"); } } }); let latest_event = Arc::new(Mutex::new(None)); - let l = latest_event.clone(); - alice.add_event_handler(|ev: AnySyncMessageLikeEvent| async move { - let mut latest_event = l.lock().await; - *latest_event = Some(ev); + + // Handle new event to update the `latest_event` for Alice. + alice.add_event_handler({ + let latest_event = latest_event.clone(); + + |ev: AnySyncMessageLikeEvent| async move { + let mut latest_event = latest_event.lock().await; + *latest_event = Some(ev); + } }); - // alice creates a room and invites bob. + // Alice creates a room and invites Bob. let room_id = alice .create_room(assign!(CreateRoomRequest::new(), { invite: vec![bob.user_id().unwrap().to_owned()], @@ -425,16 +435,7 @@ async fn test_room_notification_count() -> Result<()> { .room_id() .to_owned(); - let mut alice_room = None; - for i in 1..=4 { - sleep(Duration::from_millis(30 * i)).await; - alice_room = alice.get_room(&room_id); - if alice_room.is_some() { - break; - } - } - - let alice_room = alice_room.unwrap(); + let alice_room = alice.get_room(&room_id).unwrap(); assert_eq!(alice_room.state(), RoomState::Joined); alice_room.enable_encryption().await?; @@ -455,6 +456,7 @@ async fn test_room_notification_count() -> Result<()> { { debug!("Bob joined the room"); + let update = update_observer.next().await.expect("we should get an update when Bob joins the room"); @@ -588,14 +590,14 @@ async fn test_room_notification_count() -> Result<()> { let mut settings_changes = settings.subscribe_to_changes(); - tracing::warn!("Updating room notification mode to mentions and keywords only..."); + warn!("Updating room notification mode to mentions and keywords only..."); settings .set_room_notification_mode( alice_room.room_id(), matrix_sdk::notification_settings::RoomNotificationMode::MentionsAndKeywordsOnly, ) .await?; - tracing::warn!("Done!"); + warn!("Done!"); // Wait for remote echo. timeout(Duration::from_secs(3), settings_changes.recv()) From 83ce4c7ca275146ff0bc3e05cefb5059d86e80ff Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 15:21:21 +0200 Subject: [PATCH 144/979] test(integration): Fix `test_room_notification_count`. This test was failing since the migration from the sliding sync proxy to Synapse. This patch fixes the test. The failing parts were: 1. The `timeline_limit` wasn't set, so Synapse was returning an error, 2. The `unread_notifications` was set to 0 and could not be set to 1 because that's an encrypted room. The fact `timeline_limit` is now mandatory has been mentioned in the MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4186/files#r1775138458 A patch in Ruma has been created. The previous patch in this repository also contains the fix for the SDK side. The assertions around `unread_notifications` have been removed. We no longer use this API anymore (and it should be deprecated by the way). --- .../src/tests/sliding_sync/room.rs | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index baeb3ca8a62..f70288ab459 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -359,7 +359,6 @@ impl UpdateObserver { } #[tokio::test] -#[ignore] async fn test_room_notification_count() -> Result<()> { use tokio::time::timeout; @@ -488,20 +487,6 @@ async fn test_room_notification_count() -> Result<()> { assert_eq!(alice_room.num_unread_notifications(), 1); assert_eq!(alice_room.num_unread_mentions(), 0); - // If the server hasn't updated the server-side notification count yet, wait for - // it and reassert. - if alice_room.unread_notification_counts().notification_count != 1 { - update_observer - .next() - .await - .expect("server should update server-side notification count"); - assert_eq!(alice_room.unread_notification_counts().notification_count, 1); - - assert_eq!(alice_room.num_unread_messages(), 1); - assert_eq!(alice_room.num_unread_notifications(), 1); - assert_eq!(alice_room.num_unread_mentions(), 0); - } - update_observer.assert_is_pending(); } @@ -525,20 +510,6 @@ async fn test_room_notification_count() -> Result<()> { assert_eq!(alice_room.num_unread_notifications(), 2); assert_eq!(alice_room.num_unread_mentions(), 1); - // If the server hasn't updated the server-side notification count yet, wait for - // it and reassert. - if alice_room.unread_notification_counts().notification_count != 2 { - update_observer - .next() - .await - .expect("server should update server-side notification count"); - assert_eq!(alice_room.unread_notification_counts().notification_count, 2); - - assert_eq!(alice_room.num_unread_messages(), 2); - assert_eq!(alice_room.num_unread_notifications(), 2); - assert_eq!(alice_room.num_unread_mentions(), 1); - } - update_observer.assert_is_pending(); } From d2542172177eb81e984764d074df21e38c6f291f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 25 Sep 2024 16:18:41 +0200 Subject: [PATCH 145/979] test(integration): Enable `test_room_preview` and `test_room_avatar_group_conversation`. These tests were failing since the migration from the sliding sync proxy to Synapse. Since the previous fixes to re-enable other tests, these 2 passes for free. --- .../src/tests/sliding_sync/room.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index f70288ab459..012a9b0bb2f 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -122,7 +122,6 @@ async fn test_left_room() -> Result<()> { } #[tokio::test] -#[ignore] async fn test_room_avatar_group_conversation() -> Result<()> { let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; @@ -970,7 +969,6 @@ async fn test_room_info_notable_update_deduplication() -> Result<()> { } #[tokio::test] -#[ignore] async fn test_room_preview() -> Result<()> { let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; From 2bb0c502663db0c1d73d73af60150b8cfe17c65b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Sep 2024 19:26:43 +0200 Subject: [PATCH 146/979] crypto: change withheld code for IdentityBased share strategy --- crates/matrix-sdk-crypto/CHANGELOG.md | 4 ++++ .../src/session_manager/group_sessions/share_strategy.rs | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 7db9cb8c702..f4e3241ee62 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,10 @@ Changes: +- Change the withheld code for keys not shared due to the `IdentityBasedStrategy`, from `m.unauthorised` + to `m.unverified`. + ([#3985](https://github.com/matrix-org/matrix-rust-sdk/pull/3985)) + - Improve logging for undecryptable Megolm events. ([#3989](https://github.com/matrix-org/matrix-rust-sdk/pull/3989)) diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 310b040e3ce..6cd2047b640 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -448,7 +448,7 @@ fn split_recipients_withhelds_for_user_based_on_identity( allowed_devices: Vec::default(), denied_devices_with_code: user_devices .into_values() - .map(|d| (d, WithheldCode::Unauthorised)) + .map(|d| (d, WithheldCode::Unverified)) .collect(), } } @@ -461,7 +461,7 @@ fn split_recipients_withhelds_for_user_based_on_identity( if d.is_cross_signed_by_owner(device_owner_identity) { Either::Left(d) } else { - Either::Right((d, WithheldCode::Unauthorised)) + Either::Right((d, WithheldCode::Unverified)) } }); IdentityBasedRecipientDevices { @@ -1138,7 +1138,7 @@ mod tests { .find(|(d, _)| d.device_id() == KeyDistributionTestData::dan_unsigned_device_id()) .expect("This dan's device should receive a withheld code"); - assert_eq!(code, &WithheldCode::Unauthorised); + assert_eq!(code, &WithheldCode::Unverified); // Check withhelds for others let (_, code) = share_result @@ -1147,7 +1147,7 @@ mod tests { .find(|(d, _)| d.device_id() == KeyDistributionTestData::dave_device_id()) .expect("This dave device should receive a withheld code"); - assert_eq!(code, &WithheldCode::Unauthorised); + assert_eq!(code, &WithheldCode::Unverified); } /// Test key sharing with the identity-based strategy with different From 322c5b3f830ae4967ff07b0e475b37fd4ab5f963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 26 Sep 2024 11:44:32 +0200 Subject: [PATCH 147/979] refactor: Fold the private UserIdentities struct into UserIdentity It doesn't serve any purpose and only confuses people since we have many similarly named types. --- .../src/encryption/identities/users.rs | 141 ++++++++---------- 1 file changed, 65 insertions(+), 76 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index ad47bc4c356..25b8a4e6c0d 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -99,13 +99,13 @@ impl IdentityUpdates { /// [`Encryption::bootstrap_cross_signing()`]: crate::encryption::Encryption::bootstrap_cross_signing #[derive(Debug, Clone)] pub struct UserIdentity { - inner: UserIdentities, + client: Client, + inner: CryptoUserIdentities, } impl UserIdentity { pub(crate) fn new(client: Client, identity: CryptoUserIdentities) -> Self { - let inner = UserIdentities { client, identity }; - Self { inner } + Self { inner: identity, client } } /// The ID of the user this identity belongs to. @@ -128,7 +128,7 @@ impl UserIdentity { /// # anyhow::Ok(()) }; /// ``` pub fn user_id(&self) -> &UserId { - match &self.inner.identity { + match &self.inner { CryptoUserIdentities::Own(identity) => identity.user_id(), CryptoUserIdentities::Other(identity) => identity.user_id(), } @@ -185,7 +185,7 @@ impl UserIdentity { pub async fn request_verification( &self, ) -> Result { - self.inner.request_verification(None).await + self.request_verification_impl(None).await } /// Request an interactive verification with this `UserIdentity` using the @@ -244,8 +244,56 @@ impl UserIdentity { methods: Vec, ) -> Result { assert!(!methods.is_empty(), "The list of verification methods can't be non-empty"); + self.request_verification_impl(Some(methods)).await + } + + async fn request_verification_impl( + &self, + methods: Option>, + ) -> Result { + match &self.inner { + CryptoUserIdentities::Own(identity) => { + let (verification, request) = if let Some(methods) = methods { + identity + .request_verification_with_methods(methods) + .await + .map_err(crate::Error::from)? + } else { + identity.request_verification().await.map_err(crate::Error::from)? + }; - self.inner.request_verification(Some(methods)).await + self.client.send_verification_request(request).await?; + + Ok(VerificationRequest { inner: verification, client: self.client.clone() }) + } + CryptoUserIdentities::Other(i) => { + let content = i.verification_request_content(methods.clone()); + + let room = if let Some(room) = self.client.get_dm_room(i.user_id()) { + // Make sure that the user, to be verified, is still in the room + if !room + .members(RoomMemberships::ACTIVE) + .await? + .iter() + .any(|member| member.user_id() == i.user_id()) + { + room.invite_user_by_id(i.user_id()).await?; + } + room.clone() + } else { + self.client.create_dm(i.user_id()).await? + }; + + let response = room + .send(RoomMessageEventContent::new(MessageType::VerificationRequest(content))) + .await?; + + let verification = + i.request_verification(room.room_id(), &response.event_id, methods); + + Ok(VerificationRequest { inner: verification, client: self.client.clone() }) + } + } } /// Manually verify this [`UserIdentity`]. @@ -308,7 +356,14 @@ impl UserIdentity { /// ``` /// [`Encryption::cross_signing_status()`]: crate::encryption::Encryption::cross_signing_status pub async fn verify(&self) -> Result<(), ManualVerifyError> { - self.inner.verify().await + let request = match &self.inner { + CryptoUserIdentities::Own(identity) => identity.verify().await?, + CryptoUserIdentities::Other(identity) => identity.verify().await?, + }; + + self.client.send(request, None).await?; + + Ok(()) } /// Is the user identity considered to be verified. @@ -350,7 +405,7 @@ impl UserIdentity { /// # anyhow::Ok(()) }; /// ``` pub fn is_verified(&self) -> bool { - self.inner.identity.is_verified() + self.inner.is_verified() } /// Remove the requirement for this identity to be verified. @@ -359,7 +414,7 @@ impl UserIdentity { /// reported to the user. In order to remove this notice users have to /// verify again or to withdraw the verification requirement. pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> { - self.inner.identity.withdraw_verification().await + self.inner.withdraw_verification().await } /// Get the public part of the Master key of this user identity. @@ -404,75 +459,9 @@ impl UserIdentity { /// # anyhow::Ok(()) }; /// ``` pub fn master_key(&self) -> &MasterPubkey { - match &self.inner.identity { + match &self.inner { CryptoUserIdentities::Own(identity) => identity.master_key(), CryptoUserIdentities::Other(identity) => identity.master_key(), } } } - -#[derive(Debug, Clone)] -struct UserIdentities { - client: Client, - identity: CryptoUserIdentities, -} - -impl UserIdentities { - async fn request_verification( - &self, - methods: Option>, - ) -> Result { - match &self.identity { - CryptoUserIdentities::Own(identity) => { - let (verification, request) = if let Some(methods) = methods { - identity - .request_verification_with_methods(methods) - .await - .map_err(crate::Error::from)? - } else { - identity.request_verification().await.map_err(crate::Error::from)? - }; - - self.client.send_verification_request(request).await?; - - Ok(VerificationRequest { inner: verification, client: self.client.clone() }) - } - CryptoUserIdentities::Other(i) => { - let content = i.verification_request_content(methods.clone()); - - let room = if let Some(room) = self.client.get_dm_room(i.user_id()) { - // Make sure that the user, to be verified, is still in the room - if !room - .members(RoomMemberships::ACTIVE) - .await? - .iter() - .any(|member| member.user_id() == i.user_id()) - { - room.invite_user_by_id(i.user_id()).await?; - } - room.clone() - } else { - self.client.create_dm(i.user_id()).await? - }; - - let response = room - .send(RoomMessageEventContent::new(MessageType::VerificationRequest(content))) - .await?; - - let verification = - i.request_verification(room.room_id(), &response.event_id, methods); - - Ok(VerificationRequest { inner: verification, client: self.client.clone() }) - } - } - } - - async fn verify(&self) -> Result<(), ManualVerifyError> { - let request = match &self.identity { - CryptoUserIdentities::Own(identity) => identity.verify().await?, - CryptoUserIdentities::Other(identity) => identity.verify().await?, - }; - self.client.send(request, None).await?; - Ok(()) - } -} From 9b7f89c1839baed597143b96e944b75f0070d735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 26 Sep 2024 16:03:37 +0200 Subject: [PATCH 148/979] ffi: use fork of `tracing` crate with a fix for Android logs rotation --- bindings/matrix-sdk-ffi/Cargo.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index abab2bff692..c994382b663 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -82,3 +82,10 @@ features = [ [lints] workspace = true + +[patch.crates-io] +tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd", default-features = false, features = ["std"] } +tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } +tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } +tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } +paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" } From e61fb45504b0a2f28748e5cc3a653fcd2ef8918d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 26 Sep 2024 15:55:07 +0200 Subject: [PATCH 149/979] ffi: Allow recovery to be enabled using a passphrase --- bindings/matrix-sdk-ffi/src/encryption.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 85468b2cf93..34a0e794941 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -315,6 +315,7 @@ impl Encryption { pub async fn enable_recovery( &self, wait_for_backups_to_upload: bool, + mut passphrase: Option, progress_listener: Box, ) -> Result { let recovery = self.inner.recovery(); @@ -325,6 +326,12 @@ impl Encryption { recovery.enable() }; + let enable = if let Some(passphrase) = &passphrase { + enable.with_passphrase(passphrase) + } else { + enable + }; + let mut progress_stream = enable.subscribe_to_progress(); let task = RUNTIME.spawn(async move { @@ -337,6 +344,7 @@ impl Encryption { let ret = enable.await?; task.abort(); + passphrase.zeroize(); Ok(ret) } From 67df36f73356fe85b2d4004a6a365b77f44e2ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 20 Sep 2024 10:00:51 +0200 Subject: [PATCH 150/979] ffi: Turn `EventTimelineItem` into a record type This improves parsing times in mobile Clients. On Android, this means a 5-10x faster parsing of timeline events. To do that I had to: - Make functions like `edit/redact/forward` take an identifier (EventId/TransactionId) instead of the actual event. This id will be used to look for the actual SDK timeline event in the timeline. This change will make these functions a bit less performant. - Make `InReplyToDetails` an object instead since a record can't recursively contain itself. - Turn `EventTimelineItem` into a record type. Do the same with `Message`, which is now `MessageContent`. --- bindings/matrix-sdk-ffi/src/lib.rs | 2 +- bindings/matrix-sdk-ffi/src/room_list.rs | 4 +- bindings/matrix-sdk-ffi/src/ruma.rs | 12 + .../matrix-sdk-ffi/src/timeline/content.rs | 133 +++++------ bindings/matrix-sdk-ffi/src/timeline/mod.rs | 211 +++++++++--------- .../timeline/event_item/content/message.rs | 10 + 6 files changed, 199 insertions(+), 173 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index f475f7e862d..91c7fb63407 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -1,6 +1,6 @@ // TODO: target-os conditional would be good. -#![allow(unused_qualifications, clippy::new_without_default)] +#![allow(unused_qualifications, clippy::new_without_default, unused_macros)] macro_rules! unwrap_or_clone_arc_into_variant { ( diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 48a6c100397..df6926bc8fe 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -702,8 +702,8 @@ impl RoomListItem { self.inner.is_encrypted().await.unwrap_or(false) } - async fn latest_event(&self) -> Option> { - self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new) + async fn latest_event(&self) -> Option { + self.inner.latest_event().await.map(|e| e.into()) } } diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 89b9c23f741..2302e376860 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -54,6 +54,7 @@ use tracing::info; use crate::{ error::{ClientError, MediaInfoError}, helpers::unwrap_or_clone_arc, + timeline::MessageContent, utils::u64_to_uint, }; @@ -227,6 +228,7 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi } } +#[derive(Clone)] pub struct Mentions { pub user_ids: Vec, pub room: bool, @@ -861,3 +863,13 @@ impl From for PollKind { } } } + +/// Creates a [`RoomMessageEventContentWithoutRelation`] given a +/// [`MessageContent`] value. +#[uniffi::export] +pub fn content_without_relation_from_message( + message: MessageContent, +) -> Result { + let msg_type = message.msg_type.try_into()?; + Ok(RoomMessageEventContentWithoutRelation::new(msg_type)) +} diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index ffe2cecb5b8..0c8723cff67 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -16,45 +16,40 @@ use std::{collections::HashMap, sync::Arc}; use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes}; use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails}; -use ruma::events::room::{message::RoomMessageEventContentWithoutRelation, MediaSource}; -use tracing::warn; +use ruma::events::room::MediaSource; use super::ProfileDetails; -use crate::ruma::{ImageInfo, MessageType, PollKind}; +use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind}; -#[derive(Clone, uniffi::Object)] -pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent); - -#[uniffi::export] -impl TimelineItemContent { - pub fn kind(&self) -> TimelineItemContentKind { +impl From<&matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent { + fn from(value: &matrix_sdk_ui::timeline::TimelineItemContent) -> Self { use matrix_sdk_ui::timeline::TimelineItemContent as Content; - match &self.0 { - Content::Message(_) => TimelineItemContentKind::Message, + match value { + Content::Message(message) => TimelineItemContent::Message { content: message.into() }, - Content::RedactedMessage => TimelineItemContentKind::RedactedMessage, + Content::RedactedMessage => TimelineItemContent::RedactedMessage, Content::Sticker(sticker) => { let content = sticker.content(); - TimelineItemContentKind::Sticker { + TimelineItemContent::Sticker { body: content.body.clone(), info: (&content.info).into(), source: Arc::new(MediaSource::from(content.source.clone())), } } - Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()), + Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()), - Content::CallInvite => TimelineItemContentKind::CallInvite, + Content::CallInvite => TimelineItemContent::CallInvite, - Content::CallNotify => TimelineItemContentKind::CallNotify, + Content::CallNotify => TimelineItemContent::CallNotify, Content::UnableToDecrypt(msg) => { - TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) } + TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(msg) } } - Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership { + Content::MembershipChange(membership) => TimelineItemContent::RoomMembership { user_id: membership.user_id().to_string(), user_display_name: membership.display_name(), change: membership.change().map(Into::into), @@ -74,7 +69,7 @@ impl TimelineItemContent { ) }) .unzip(); - TimelineItemContentKind::ProfileChange { + TimelineItemContent::ProfileChange { display_name: display_name.flatten(), prev_display_name: prev_display_name.flatten(), avatar_url: avatar_url.flatten(), @@ -82,20 +77,20 @@ impl TimelineItemContent { } } - Content::OtherState(state) => TimelineItemContentKind::State { + Content::OtherState(state) => TimelineItemContent::State { state_key: state.state_key().to_owned(), content: state.content().into(), }, Content::FailedToParseMessageLike { event_type, error } => { - TimelineItemContentKind::FailedToParseMessageLike { + TimelineItemContent::FailedToParseMessageLike { event_type: event_type.to_string(), error: error.to_string(), } } Content::FailedToParseState { event_type, state_key, error } => { - TimelineItemContentKind::FailedToParseState { + TimelineItemContent::FailedToParseState { event_type: event_type.to_string(), state_key: state_key.to_string(), error: error.to_string(), @@ -103,16 +98,45 @@ impl TimelineItemContent { } } } +} - pub fn as_message(self: Arc) -> Option> { - use matrix_sdk_ui::timeline::TimelineItemContent as Content; - unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg))) +#[derive(Clone, uniffi::Record)] +pub struct MessageContent { + pub msg_type: MessageType, + pub body: String, + pub in_reply_to: Option>, + pub thread_root: Option, + pub is_edited: bool, + pub mentions: Option, +} + +impl From<&matrix_sdk_ui::timeline::Message> for MessageContent { + fn from(value: &matrix_sdk_ui::timeline::Message) -> Self { + Self { + msg_type: value.msgtype().clone().into(), + body: value.body().to_owned(), + in_reply_to: value.in_reply_to().map(|r| Arc::new(r.into())), + is_edited: value.is_edited(), + thread_root: value.thread_root().map(|id| id.to_string()), + mentions: value.mentions().cloned().map(|m| m.into()), + } + } +} + +impl From for Mentions { + fn from(value: ruma::events::Mentions) -> Self { + Self { + user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(), + room: value.room, + } } } -#[derive(uniffi::Enum)] -pub enum TimelineItemContentKind { - Message, +#[derive(Clone, uniffi::Enum)] +pub enum TimelineItemContent { + Message { + content: MessageContent, + }, RedactedMessage, Sticker { body: String, @@ -160,36 +184,6 @@ pub enum TimelineItemContentKind { } #[derive(Clone, uniffi::Object)] -pub struct Message(matrix_sdk_ui::timeline::Message); - -#[uniffi::export] -impl Message { - pub fn msgtype(&self) -> MessageType { - self.0.msgtype().clone().into() - } - - pub fn body(&self) -> String { - self.0.msgtype().body().to_owned() - } - - pub fn in_reply_to(&self) -> Option { - self.0.in_reply_to().map(InReplyToDetails::from) - } - - pub fn is_threaded(&self) -> bool { - self.0.is_threaded() - } - - pub fn is_edited(&self) -> bool { - self.0.is_edited() - } - - pub fn content(&self) -> Arc { - Arc::new(RoomMessageEventContentWithoutRelation::new(self.0.msgtype().clone())) - } -} - -#[derive(uniffi::Record)] pub struct InReplyToDetails { event_id: String, event: RepliedToEventDetails, @@ -201,6 +195,17 @@ impl InReplyToDetails { } } +#[uniffi::export] +impl InReplyToDetails { + pub fn event_id(&self) -> String { + self.event_id.clone() + } + + pub fn event(&self) -> RepliedToEventDetails { + self.event.clone() + } +} + impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails { fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self { let event_id = inner.event_id.to_string(); @@ -208,7 +213,7 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails { TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable, TimelineDetails::Pending => RepliedToEventDetails::Pending, TimelineDetails::Ready(event) => RepliedToEventDetails::Ready { - content: Arc::new(TimelineItemContent(event.content().to_owned())), + content: event.content().into(), sender: event.sender().to_string(), sender_profile: event.sender_profile().into(), }, @@ -221,11 +226,11 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails { } } -#[derive(uniffi::Enum)] +#[derive(Clone, uniffi::Enum)] pub enum RepliedToEventDetails { Unavailable, Pending, - Ready { content: Arc, sender: String, sender_profile: ProfileDetails }, + Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails }, Error { message: String }, } @@ -419,15 +424,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat } } -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct PollAnswer { pub id: String, pub text: String, } -impl From for TimelineItemContentKind { +impl From for TimelineItemContent { fn from(value: PollResult) -> Self { - TimelineItemContentKind::Poll { + TimelineItemContent::Poll { question: value.question, kind: PollKind::from(value.kind), max_selections: value.max_selections, diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index f28b0e9a039..ccfa0f16e16 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -78,6 +78,8 @@ use crate::{ mod content; +pub use content::MessageContent; + #[derive(uniffi::Object)] #[repr(transparent)] pub struct Timeline { @@ -423,11 +425,11 @@ impl Timeline { pub async fn send_poll_response( self: Arc, - poll_start_id: String, + poll_start_event_id: String, answers: Vec, ) -> Result<(), ClientError> { let poll_start_event_id = - EventId::parse(poll_start_id).context("Failed to parse EventId")?; + EventId::parse(poll_start_event_id).context("Failed to parse EventId")?; let poll_response_event_content = UnstablePollResponseEventContent::new(answers, poll_start_event_id); let event_content = @@ -442,11 +444,11 @@ impl Timeline { pub fn end_poll( self: Arc, - poll_start_id: String, + poll_start_event_id: String, text: String, ) -> Result<(), ClientError> { let poll_start_event_id = - EventId::parse(poll_start_id).context("Failed to parse EventId")?; + EventId::parse(poll_start_event_id).context("Failed to parse EventId")?; let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id); let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content); @@ -486,13 +488,19 @@ impl Timeline { /// /// Returns whether the edit did happen. It can only return false for /// local events that are being processed. - pub async fn edit( - &self, - item: Arc, - new_content: EditedContent, - ) -> Result { - let new_content: SdkEditedContent = new_content.try_into()?; - self.inner.edit(&item.0, new_content).await.map_err(ClientError::from) + pub async fn edit(&self, id: String, new_content: EditedContent) -> Result { + let event = if let Ok(event_id) = EventId::parse(&id) { + self.inner.item_by_event_id(&event_id).await + } else { + let transaction_id: OwnedTransactionId = id.into(); + self.inner.local_item_by_transaction_id(&transaction_id).await + }; + if let Some(event) = event { + let new_content: SdkEditedContent = new_content.try_into()?; + self.inner.edit(&event, new_content).await.map_err(ClientError::from) + } else { + Ok(false) + } } pub async fn send_location( @@ -558,14 +566,14 @@ impl Timeline { pub async fn get_event_timeline_item_by_event_id( &self, event_id: String, - ) -> Result, ClientError> { + ) -> Result { let event_id = EventId::parse(event_id)?; let item = self .inner .item_by_event_id(&event_id) .await .context("Item with given event ID not found")?; - Ok(Arc::new(EventTimelineItem(item))) + Ok(item.into()) } /// Get the current timeline item for the given transaction ID, if any. @@ -578,14 +586,14 @@ impl Timeline { pub async fn get_event_timeline_item_by_transaction_id( &self, transaction_id: String, - ) -> Result, ClientError> { + ) -> Result { let transaction_id: OwnedTransactionId = transaction_id.into(); let item = self .inner .local_item_by_transaction_id(&transaction_id) .await .context("Item with given transaction ID not found")?; - Ok(Arc::new(EventTimelineItem(item))) + Ok(item.into()) } /// Redacts an event from the timeline. @@ -600,16 +608,26 @@ impl Timeline { /// local events that are being processed. pub async fn redact_event( &self, - item: Arc, + id: String, reason: Option, ) -> Result { - let removed = self - .inner - .redact(&item.0, reason.as_deref()) - .await - .map_err(|err| anyhow::anyhow!(err))?; + let event = if let Ok(event_id) = EventId::parse(&id) { + self.inner.item_by_event_id(&event_id).await + } else { + let transaction_id: OwnedTransactionId = id.into(); + self.inner.local_item_by_transaction_id(&transaction_id).await + }; + if let Some(event) = event { + let removed = self + .inner + .redact(&event, reason.as_deref()) + .await + .map_err(|err| anyhow::anyhow!(err))?; - Ok(removed) + Ok(removed) + } else { + Ok(false) + } } /// Load the reply details for the given event id. @@ -640,7 +658,7 @@ impl Timeline { Ok(replied_to) => Ok(InReplyToDetails::new( event_id_str, RepliedToEventDetails::Ready { - content: Arc::new(TimelineItemContent(replied_to.content().clone())), + content: replied_to.content().into(), sender: replied_to.sender().to_string(), sender_profile: replied_to.sender_profile().into(), }, @@ -672,6 +690,14 @@ impl Timeline { let event_id = EventId::parse(event_id).map_err(ClientError::from)?; self.inner.unpin_event(&event_id).await.map_err(ClientError::from) } + + pub fn create_message_content( + &self, + msg_type: crate::ruma::MessageType, + ) -> Option> { + let msg_type: Option = msg_type.try_into().ok(); + msg_type.map(|m| Arc::new(RoomMessageEventContentWithoutRelation::new(m))) + } } #[derive(uniffi::Object)] @@ -871,9 +897,9 @@ impl TimelineItem { #[uniffi::export] impl TimelineItem { - pub fn as_event(self: Arc) -> Option> { + pub fn as_event(self: Arc) -> Option { let event_item = self.0.as_event()?; - Some(Arc::new(EventTimelineItem(event_item.clone()))) + Some(event_item.clone().into()) } pub fn as_virtual(self: Arc) -> Option { @@ -995,7 +1021,7 @@ fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) -> /// Recommended decorations for decrypted messages, representing the message's /// authenticity properties. -#[derive(uniffi::Enum)] +#[derive(uniffi::Enum, Clone)] pub enum ShieldState { /// A red shield with a tooltip containing the associated message should be /// presented. @@ -1021,100 +1047,73 @@ impl From for ShieldState { } } -#[derive(uniffi::Object)] -pub struct EventTimelineItem(pub(crate) matrix_sdk_ui::timeline::EventTimelineItem); - -#[uniffi::export] -impl EventTimelineItem { - pub fn is_local(&self) -> bool { - self.0.is_local_echo() - } - - pub fn is_remote(&self) -> bool { - !self.0.is_local_echo() - } - - pub fn transaction_id(&self) -> Option { - self.0.transaction_id().map(ToString::to_string) - } - - pub fn event_id(&self) -> Option { - self.0.event_id().map(ToString::to_string) - } - - pub fn sender(&self) -> String { - self.0.sender().to_string() - } - - pub fn sender_profile(&self) -> ProfileDetails { - self.0.sender_profile().into() - } - - pub fn is_own(&self) -> bool { - self.0.is_own() - } - - pub fn is_editable(&self) -> bool { - self.0.is_editable() - } - - pub fn content(&self) -> Arc { - Arc::new(TimelineItemContent(self.0.content().clone())) - } - - pub fn timestamp(&self) -> u64 { - self.0.timestamp().0.into() - } +#[derive(Clone, uniffi::Record)] +pub struct EventTimelineItem { + is_local: bool, + is_remote: bool, + transaction_id: Option, + event_id: Option, + sender: String, + sender_profile: ProfileDetails, + is_own: bool, + is_editable: bool, + content: TimelineItemContent, + timestamp: u64, + reactions: Vec, + debug_info: EventTimelineItemDebugInfo, + local_send_state: Option, + read_receipts: HashMap, + origin: Option, + can_be_replied_to: bool, + message_shield: Option, +} - pub fn reactions(&self) -> Vec { - self.0 +impl From for EventTimelineItem { + fn from(value: matrix_sdk_ui::timeline::EventTimelineItem) -> Self { + let reactions = value .reactions() .iter() .map(|(k, v)| Reaction { key: k.to_owned(), senders: v - .iter() + .into_iter() .map(|(sender_id, info)| ReactionSenderData { sender_id: sender_id.to_string(), timestamp: info.timestamp.0.into(), }) .collect(), }) - .collect() - } - - pub fn debug_info(&self) -> EventTimelineItemDebugInfo { - EventTimelineItemDebugInfo { - model: format!("{:#?}", self.0), - original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()), - latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()), + .collect(); + let debug_info = EventTimelineItemDebugInfo { + model: format!("{:#?}", value), + original_json: value.original_json().map(|raw| raw.json().get().to_owned()), + latest_edit_json: value.latest_edit_json().map(|raw| raw.json().get().to_owned()), + }; + let read_receipts = + value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect(); + Self { + is_local: value.is_local_echo(), + is_remote: !value.is_local_echo(), + transaction_id: value.transaction_id().map(|t| t.to_string()), + event_id: value.event_id().map(|e| e.to_string()), + sender: value.sender().to_string(), + sender_profile: value.sender_profile().into(), + is_own: value.is_own(), + is_editable: value.is_editable(), + content: value.content().into(), + timestamp: value.timestamp().0.into(), + reactions, + debug_info, + local_send_state: value.send_state().map(|s| s.into()), + read_receipts, + origin: value.origin(), + can_be_replied_to: value.can_be_replied_to(), + message_shield: value.get_shield(false).map(Into::into), } } - - pub fn local_send_state(&self) -> Option { - self.0.send_state().map(Into::into) - } - - pub fn read_receipts(&self) -> HashMap { - self.0.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect() - } - - pub fn origin(&self) -> Option { - self.0.origin() - } - - pub fn can_be_replied_to(&self) -> bool { - self.0.can_be_replied_to() - } - - /// Gets the [`ShieldState`] which can be used to decorate messages in the - /// recommended way. - pub fn get_shield(&self, strict: bool) -> Option { - self.0.get_shield(strict).map(Into::into) - } } -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct Receipt { pub timestamp: Option, } @@ -1125,14 +1124,14 @@ impl From for Receipt { } } -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct EventTimelineItemDebugInfo { model: String, original_json: Option, latest_edit_json: Option, } -#[derive(uniffi::Enum)] +#[derive(Clone, uniffi::Enum)] pub enum ProfileDetails { Unavailable, Pending, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 808204210b1..8730d9fc850 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -117,6 +117,11 @@ impl Message { Self { msgtype, in_reply_to, thread_root, edited, mentions } } + /// Create a forwarded message from a [`MessageType`]. + pub fn forwarded(msgtype: MessageType) -> Self { + Self { msgtype, in_reply_to: None, thread_root: None, edited: false, mentions: None } + } + /// Get the `msgtype`-specific data of this message. pub fn msgtype(&self) -> &MessageType { &self.msgtype @@ -139,6 +144,11 @@ impl Message { self.thread_root.is_some() } + /// Get the [`OwnedEventId`] of the root event of a thread if it exists. + pub fn thread_root(&self) -> Option { + self.thread_root.clone() + } + /// Get the edit state of this message (has been edited: `true` / /// `false`). pub fn is_edited(&self) -> bool { From 548c66750f5e0cd5322da2b2a67eb2ea73fa9e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 20 Sep 2024 13:27:34 +0200 Subject: [PATCH 151/979] sdk-ui: Move the event-fetching logic for edit and redact functions to the sdk-ui crate where they can be tested, to the `edit_by_id` and `redact_by_id` functions. Added some tests for those, based on the existing ones. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 36 +-- .../src/timeline/controller/mod.rs | 11 +- crates/matrix-sdk-ui/src/timeline/error.rs | 22 +- .../src/timeline/event_item/mod.rs | 34 ++- crates/matrix-sdk-ui/src/timeline/mod.rs | 85 +++++- crates/matrix-sdk-ui/src/timeline/traits.rs | 3 +- .../tests/integration/timeline/edit.rs | 266 +++++++++++++++++- .../tests/integration/timeline/mod.rs | 134 ++++++++- .../tests/integration/timeline/replies.rs | 2 +- 9 files changed, 526 insertions(+), 67 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index ccfa0f16e16..5affb36bdb2 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -489,18 +489,7 @@ impl Timeline { /// Returns whether the edit did happen. It can only return false for /// local events that are being processed. pub async fn edit(&self, id: String, new_content: EditedContent) -> Result { - let event = if let Ok(event_id) = EventId::parse(&id) { - self.inner.item_by_event_id(&event_id).await - } else { - let transaction_id: OwnedTransactionId = id.into(); - self.inner.local_item_by_transaction_id(&transaction_id).await - }; - if let Some(event) = event { - let new_content: SdkEditedContent = new_content.try_into()?; - self.inner.edit(&event, new_content).await.map_err(ClientError::from) - } else { - Ok(false) - } + self.inner.edit_by_id(&(id.into()), new_content.try_into()?).await.map_err(Into::into) } pub async fn send_location( @@ -604,30 +593,13 @@ impl Timeline { /// being sent already. If the event was a remote event, then it will be /// redacted by sending a redaction request to the server. /// - /// Returns whether the redaction did happen. It can only return false for - /// local events that are being processed. + /// Will return an error if the event couldn't be redacted. pub async fn redact_event( &self, id: String, reason: Option, - ) -> Result { - let event = if let Ok(event_id) = EventId::parse(&id) { - self.inner.item_by_event_id(&event_id).await - } else { - let transaction_id: OwnedTransactionId = id.into(); - self.inner.local_item_by_transaction_id(&transaction_id).await - }; - if let Some(event) = event { - let removed = self - .inner - .redact(&event, reason.as_deref()) - .await - .map_err(|err| anyhow::anyhow!(err))?; - - Ok(removed) - } else { - Ok(false) - } + ) -> Result<(), ClientError> { + self.inner.redact_by_id(&(id.into()), reason.as_deref()).await.map_err(Into::into) } /// Load the reply details for the given event id. diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index e543f054074..383d18e8114 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1380,9 +1380,12 @@ impl TimelineController { #[instrument(skip(self))] pub(super) async fn fetch_in_reply_to_details(&self, event_id: &EventId) -> Result<(), Error> { let state = self.state.write().await; - let (index, item) = - rfind_event_by_id(&state.items, event_id).ok_or(Error::RemoteEventNotInTimeline)?; - let remote_item = item.as_remote().ok_or(Error::RemoteEventNotInTimeline)?.clone(); + let (index, item) = rfind_event_by_id(&state.items, event_id) + .ok_or(Error::EventNotInTimeline(TimelineEventItemId::EventId(event_id.to_owned())))?; + let remote_item = item + .as_remote() + .ok_or(Error::EventNotInTimeline(TimelineEventItemId::EventId(event_id.to_owned())))? + .clone(); let TimelineItemContent::Message(message) = item.content().clone() else { debug!("Event is not a message"); @@ -1418,7 +1421,7 @@ impl TimelineController { // changed while waiting for the request. let mut state = self.state.write().await; let (index, item) = rfind_event_by_id(&state.items, &remote_item.event_id) - .ok_or(Error::RemoteEventNotInTimeline)?; + .ok_or(Error::EventNotInTimeline(TimelineEventItemId::EventId(event_id.to_owned())))?; // Check the state of the event again, it might have been redacted while // the request was in-flight. diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index 39f90f8d3a0..b1a89956474 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -18,17 +18,18 @@ use matrix_sdk::{ send_queue::RoomSendQueueError, HttpError, }; +use ruma::OwnedTransactionId; use thiserror::Error; -use crate::timeline::pinned_events_loader::PinnedEventsLoaderError; +use crate::timeline::{pinned_events_loader::PinnedEventsLoaderError, TimelineEventItemId}; /// Errors specific to the timeline. #[derive(Error, Debug)] #[non_exhaustive] pub enum Error { - /// The requested event with a remote echo is not in the timeline. - #[error("Event with remote echo not found in timeline")] - RemoteEventNotInTimeline, + /// The requested event is not in the timeline. + #[error("Event not found in timeline: {0:?}")] + EventNotInTimeline(TimelineEventItemId), /// The event is currently unsupported for this use case.. #[error("Unsupported event")] @@ -76,7 +77,18 @@ pub enum Error { /// An error happened while attempting to redact an event. #[error(transparent)] - RedactError(HttpError), + RedactError(RedactError), +} + +#[derive(Error, Debug)] +pub enum RedactError { + /// Local event to redact wasn't found for transaction id + #[error("Local event to redact wasn't found for transaction {0}")] + LocalEventNotFound(OwnedTransactionId), + + /// An error happened while attempting to redact an event. + #[error(transparent)] + HttpError(#[from] HttpError), } #[derive(Error, Debug)] 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 d742dc3be0e..79a23ee5024 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -99,6 +99,22 @@ pub enum TimelineEventItemId { EventId(OwnedEventId), } +impl From for TimelineEventItemId { + fn from(value: String) -> Self { + value.as_str().into() + } +} + +impl From<&str> for TimelineEventItemId { + fn from(value: &str) -> Self { + if let Ok(event_id) = EventId::parse(value) { + TimelineEventItemId::EventId(event_id) + } else { + TimelineEventItemId::TransactionId(value.into()) + } + } +} + /// An handle that usually allows to perform an action on a timeline event. /// /// If the item represents a remote item, then the event id is usually @@ -251,7 +267,7 @@ impl EventTimelineItem { /// Returns the transaction ID for a local echo item that has not been sent /// and the event ID for a local echo item that has been sent or a /// remote item. - pub(crate) fn identifier(&self) -> TimelineEventItemId { + pub fn identifier(&self) -> TimelineEventItemId { match &self.kind { EventTimelineItemKind::Local(local) => local.identifier(), EventTimelineItemKind::Remote(remote) => { @@ -719,7 +735,7 @@ mod tests { }; use super::{EventTimelineItem, Profile}; - use crate::timeline::TimelineDetails; + use crate::timeline::{TimelineDetails, TimelineEventItemId}; #[async_test] async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() { @@ -833,6 +849,20 @@ mod tests { ); } + #[test] + fn test_raw_event_id_into_timeline_event_item_id_gets_event_id() { + let raw_id = "$123:example.com"; + let id: TimelineEventItemId = raw_id.into(); + assert_matches!(id, TimelineEventItemId::EventId(_)); + } + + #[test] + fn test_raw_str_into_timeline_event_item_id_gets_transaction_id() { + let raw_id = "something something"; + let id: TimelineEventItemId = raw_id.into(); + assert_matches!(id, TimelineEventItemId::TransactionId(_)); + } + fn member_event( room_id: &RoomId, user_id: &UserId, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index bd501f59562..77a3e325a8f 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -230,6 +230,17 @@ impl Timeline { self.controller.retry_event_decryption(self.room(), None).await; } + /// Get the current timeline item for the given [`TimelineEventItemId`], if + /// any. + async fn event_by_timeline_id(&self, id: &TimelineEventItemId) -> Option { + match id { + TimelineEventItemId::EventId(event_id) => self.item_by_event_id(event_id).await, + TimelineEventItemId::TransactionId(transaction_id) => { + self.item_by_transaction_id(transaction_id).await + } + } + } + /// Get the current timeline item for the given event ID, if any. /// /// Will return a remote event, *or* a local echo that has been sent but not @@ -455,6 +466,21 @@ impl Timeline { Some(found.clone()) } + /// Edit an event given its [`TimelineEventItemId`] and some new content. + /// + /// See [`Self::edit`] for more information. + pub async fn edit_by_id( + &self, + id: &TimelineEventItemId, + new_content: EditedContent, + ) -> Result { + let Some(event) = self.event_by_timeline_id(id).await else { + return Err(Error::EventNotInTimeline(id.clone())); + }; + + self.edit(&event, new_content).await + } + /// Edit an event. /// /// Only supports events for which [`EventTimelineItem::is_editable()`] @@ -568,6 +594,38 @@ impl Timeline { SendAttachment::new(self, path.into(), mime_type, config) } + /// Redact an event given its [`TimelineEventItemId`] and an optional + /// reason. + /// + /// See [`Self::redact`] for more info. + pub async fn redact_by_id( + &self, + id: &TimelineEventItemId, + reason: Option<&str>, + ) -> Result<(), Error> { + match id { + TimelineEventItemId::TransactionId(transaction_id) => { + let Some(event) = self.item_by_transaction_id(transaction_id).await else { + return Err(Error::RedactError(RedactError::LocalEventNotFound( + transaction_id.to_owned(), + ))); + }; + let TimelineItemHandle::Local(handle) = event.handle() else { + panic!("If the item is local, this should never happen"); + }; + handle.abort().await.map_err(RoomSendQueueError::StorageError)?; + } + TimelineEventItemId::EventId(event_id) => { + self.room() + .redact(event_id, reason, None) + .await + .map_err(RedactError::HttpError) + .map_err(Error::RedactError)?; + } + } + Ok(()) + } + /// Redact an event. /// /// # Returns @@ -583,28 +641,27 @@ impl Timeline { reason: Option<&str>, ) -> Result { let event_id = match event.identifier() { - TimelineEventItemId::TransactionId(txn_id) => { + TimelineEventItemId::TransactionId(_) => { // See if we have an up-to-date timeline item with that transaction id. - if let Some(item) = self.item_by_transaction_id(&txn_id).await { - match item.handle() { - TimelineItemHandle::Remote(event_id) => event_id.to_owned(), - TimelineItemHandle::Local(handle) => { - return Ok(handle - .abort() - .await - .map_err(RoomSendQueueError::StorageError)?); - } + match event.handle() { + TimelineItemHandle::Remote(event_id) => event_id.to_owned(), + TimelineItemHandle::Local(handle) => { + return Ok(handle + .abort() + .await + .map_err(RoomSendQueueError::StorageError)?); } - } else { - warn!("Couldn't find the local echo anymore, nor a matching remote echo"); - return Ok(false); } } TimelineEventItemId::EventId(event_id) => event_id, }; - self.room().redact(&event_id, reason, None).await.map_err(Error::RedactError)?; + self.room() + .redact(&event_id, reason, None) + .await + .map_err(RedactError::HttpError) + .map_err(Error::RedactError)?; Ok(true) } diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index d8c3bd61dc0..f1aefb2008b 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -36,7 +36,7 @@ use ruma::{ }; use tracing::{debug, error}; -use super::{Profile, TimelineBuilder}; +use super::{Profile, RedactError, TimelineBuilder}; use crate::timeline::{self, pinned_events_loader::PinnedEventsRoom, Timeline}; pub trait RoomExt { @@ -269,6 +269,7 @@ impl RoomDataProvider for Room { let _ = self .redact(event_id, reason, transaction_id) .await + .map_err(RedactError::HttpError) .map_err(super::Error::RedactError)?; Ok(()) } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 8391f7c2de8..512ea98b8c7 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -29,7 +29,9 @@ use matrix_sdk_test::{ async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, }; use matrix_sdk_ui::{ - timeline::{EventSendState, RoomExt, TimelineDetails, TimelineItemContent}, + timeline::{ + Error, EventSendState, RoomExt, TimelineDetails, TimelineEventItemId, TimelineItemContent, + }, Timeline, }; use ruma::{ @@ -46,7 +48,7 @@ use ruma::{ }, AnyMessageLikeEventContent, AnyTimelineEvent, }, - room_id, + owned_event_id, room_id, serde::Raw, OwnedRoomId, }; @@ -1092,3 +1094,263 @@ async fn test_pending_poll_edit() { // And nothing else. assert!(timeline_stream.next().now_or_never().is_none()); } + +#[async_test] +async fn test_send_edit_by_event_id() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = + timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; + + let f = EventFactory::new(); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.text_msg("Hello, World!") + .sender(client.user_id().unwrap()) + .event_id(event_id!("$original_event")), + ), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let hello_world_item = + assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + let hello_world_message = hello_world_item.content().as_message().unwrap(); + assert!(!hello_world_message.is_edited()); + assert!(hello_world_item.is_editable()); + + mock_encryption_state(&server, false).await; + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$edit_event" })), + ) + .expect(1) + .mount(&server) + .await; + + timeline + .edit_by_id( + &hello_world_item.identifier(), + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "Hello, Room!", + )), + ) + .await + .unwrap(); + + // Let the send queue handle the event. + yield_now().await; + + let edit_item = + assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => value); + + // The event itself is already known to the server. We don't currently have + // a separate edit send state. + assert_matches!(edit_item.send_state(), None); + let edit_message = edit_item.content().as_message().unwrap(); + assert_eq!(edit_message.body(), "Hello, Room!"); + assert!(edit_message.is_edited()); + + // The response to the mocked endpoint does not generate further timeline + // updates, so just wait for a bit before verifying that the endpoint was + // called. + sleep(Duration::from_millis(200)).await; + + server.verify().await; +} + +#[async_test] +async fn test_send_edit_by_non_existing_event_id() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + + mock_encryption_state(&server, false).await; + + let error = timeline + .edit_by_id( + &TimelineEventItemId::EventId(owned_event_id!("$123:example.com")), + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "Hello, Room!", + )), + ) + .await + .err() + .unwrap(); + assert_matches!(error, Error::EventNotInTimeline(_)); +} + +#[async_test] +async fn test_edit_local_echo_by_id() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + let mounted_send = Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(413).set_body_json(json!({ + "errcode": "M_TOO_LARGE", + }))) + .expect(1) + .mount_as_scoped(&server) + .await; + + // Redacting a local event works. + timeline.send(RoomMessageEventContent::text_plain("hello, just you").into()).await.unwrap(); + + assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await); + + let internal_id = item.unique_id(); + + let item = item.as_event().unwrap(); + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + + assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); + assert!(day_divider.is_day_divider()); + + // We haven't set a route for sending events, so this will fail. + + assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + + let item = item.as_event().unwrap(); + assert!(item.is_local_echo()); + assert!(item.is_editable()); + + assert_matches!( + item.send_state(), + Some(EventSendState::SendingFailed { is_recoverable: false, .. }) + ); + + assert!(timeline_stream.next().now_or_never().is_none()); + + // Set up the success response before editing, since edit causes an immediate + // retry (the room's send queue is not blocked, since the one event it couldn't + // send failed in an unrecoverable way). + drop(mounted_send); + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$1" }))) + .expect(1) + .mount(&server) + .await; + + // Let's edit the local echo. + let did_edit = timeline + .edit_by_id( + &item.identifier(), + EditedContent::RoomMessage(RoomMessageEventContent::text_plain("hello, world").into()), + ) + .await + .unwrap(); + + // We could edit the local echo, since it was in the failed state. + assert!(did_edit); + + // Observe local echo being replaced. + assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + + assert_eq!(item.unique_id(), internal_id); + + let item = item.as_event().unwrap(); + assert!(item.is_local_echo()); + + // The send state has been reset. + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + + let edit_message = item.content().as_message().unwrap(); + assert_eq!(edit_message.body(), "hello, world"); + + // Observe the event being sent, and replacing the local echo. + assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + + let item = item.as_event().unwrap(); + assert!(item.is_local_echo()); + + let edit_message = item.content().as_message().unwrap(); + assert_eq!(edit_message.body(), "hello, world"); + + // No new updates. + assert!(timeline_stream.next().now_or_never().is_none()); +} + +#[async_test] +async fn test_send_edit_by_non_existing_local_id() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + + mock_encryption_state(&server, false).await; + + let error = timeline + .edit_by_id( + &TimelineEventItemId::TransactionId("something".into()), + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "Hello, Room!", + )), + ) + .await + .err() + .unwrap(); + assert_matches!(error, Error::EventNotInTimeline(_)); +} diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 460badeb2ee..0527382eb56 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -35,18 +35,14 @@ use matrix_sdk_ui::{ }, RoomListService, Timeline, }; -use ruma::{ - event_id, - events::room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, - room_id, user_id, MilliSecondsSinceUnixEpoch, -}; +use ruma::{event_id, events::room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, owned_event_id, room_id, user_id, MilliSecondsSinceUnixEpoch}; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; use wiremock::{ matchers::{header, method, path_regex}, Mock, ResponseTemplate, }; - +use matrix_sdk_ui::timeline::{Error, RedactError, TimelineEventItemId}; use crate::mock_sync; mod echo; @@ -307,6 +303,132 @@ async fn test_redact_message() { assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); } +#[async_test] +async fn test_redact_by_id_message() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + let factory = EventFactory::new(); + factory.set_next_ts(MilliSecondsSinceUnixEpoch::now().get().into()); + + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event( + factory.sender(user_id!("@a:b.com")).text_msg("buy my bitcoins bro"), + ), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_eq!( + first.as_event().unwrap().content().as_message().unwrap().body(), + "buy my bitcoins bro" + ); + + assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); + assert!(day_divider.is_day_divider()); + + // Redacting a remote event works. + mock_redaction(event_id!("$42")).mount(&server).await; + + let event = first.as_event().unwrap(); + + timeline.redact_by_id(&event.identifier(), Some("inapprops")).await.unwrap(); + + // Redacting a local event works. + timeline + .send(RoomMessageEventContent::text_plain("i will disappear soon").into()) + .await + .unwrap(); + + assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + + let second = second.as_event().unwrap(); + assert_matches!(second.send_state(), Some(EventSendState::NotSentYet)); + + // We haven't set a route for sending events, so this will fail. + assert_let!(Some(VectorDiff::Set { index, value: second }) = timeline_stream.next().await); + assert_eq!(index, 2); + + let second = second.as_event().unwrap(); + assert!(second.is_local_echo()); + assert_matches!(second.send_state(), Some(EventSendState::SendingFailed { .. })); + + // Let's redact the local echo. + timeline.redact_by_id(&second.identifier(), None).await.unwrap(); + + // Observe local echo being removed. + assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); +} + +#[async_test] +async fn test_redact_by_id_message_with_no_remote_message_present() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + + let error = timeline + .redact_by_id(&TimelineEventItemId::EventId(owned_event_id!("$123:example.com")), None) + .await + .err(); + assert_matches!(error, Some(Error::RedactError(RedactError::HttpError(_)))) +} + +#[async_test] +async fn test_redact_by_id_message_with_no_local_message_present() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + + let error = timeline + .redact_by_id(&TimelineEventItemId::TransactionId("something".into()), None) + .await + .err(); + assert_matches!(error, Some(Error::RedactError(RedactError::LocalEventNotFound(_)))) +} + #[async_test] async fn test_read_marker() { let room_id = room_id!("!a98sd12bjh:example.org"); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs index e9653b3c89f..f53218d854c 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs @@ -61,7 +61,7 @@ async fn test_in_reply_to_details() { // The event doesn't exist. assert_matches!( timeline.fetch_details_for_event(event_id!("$fakeevent")).await, - Err(TimelineError::RemoteEventNotInTimeline) + Err(TimelineError::EventNotInTimeline(_)) ); // Add an event and a reply to that event to the timeline From 7dcf45562c3646746b6115cafde8a6c2e1fdf40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 20 Sep 2024 14:13:12 +0200 Subject: [PATCH 152/979] ffi: fix bindings not using `Arc` wrappers --- bindings/matrix-sdk-ffi/src/ruma.rs | 4 ++-- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 10 +++++----- .../src/timeline/event_item/content/message.rs | 9 ++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 2302e376860..7072270d8e8 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -869,7 +869,7 @@ impl From for PollKind { #[uniffi::export] pub fn content_without_relation_from_message( message: MessageContent, -) -> Result { +) -> Result, ClientError> { let msg_type = message.msg_type.try_into()?; - Ok(RoomMessageEventContentWithoutRelation::new(msg_type)) + Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msg_type))) } diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 5affb36bdb2..0777f470a8c 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -609,7 +609,7 @@ impl Timeline { pub async fn load_reply_details( &self, event_id_str: String, - ) -> Result { + ) -> Result, ClientError> { let event_id = EventId::parse(&event_id_str)?; let replied_to: Result = @@ -627,19 +627,19 @@ impl Timeline { }; match replied_to { - Ok(replied_to) => Ok(InReplyToDetails::new( + Ok(replied_to) => Ok(Arc::new(InReplyToDetails::new( event_id_str, RepliedToEventDetails::Ready { content: replied_to.content().into(), sender: replied_to.sender().to_string(), sender_profile: replied_to.sender_profile().into(), }, - )), + ))), - Err(e) => Ok(InReplyToDetails::new( + Err(e) => Ok(Arc::new(InReplyToDetails::new( event_id_str, RepliedToEventDetails::Error { message: e.to_string() }, - )), + ))), } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 8730d9fc850..4948e121497 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -117,11 +117,6 @@ impl Message { Self { msgtype, in_reply_to, thread_root, edited, mentions } } - /// Create a forwarded message from a [`MessageType`]. - pub fn forwarded(msgtype: MessageType) -> Self { - Self { msgtype, in_reply_to: None, thread_root: None, edited: false, mentions: None } - } - /// Get the `msgtype`-specific data of this message. pub fn msgtype(&self) -> &MessageType { &self.msgtype @@ -145,8 +140,8 @@ impl Message { } /// Get the [`OwnedEventId`] of the root event of a thread if it exists. - pub fn thread_root(&self) -> Option { - self.thread_root.clone() + pub fn thread_root(&self) -> Option<&OwnedEventId> { + self.thread_root.as_ref() } /// Get the edit state of this message (has been edited: `true` / From bdf303aa577c397d76cae4bb33c3df9e14c91723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 23 Sep 2024 07:51:55 +0200 Subject: [PATCH 153/979] ffi: remove unused `unwrap_or_clone_arc_into_variant` macro --- bindings/matrix-sdk-ffi/src/lib.rs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 91c7fb63407..47ef7240d3d 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -1,24 +1,6 @@ // TODO: target-os conditional would be good. -#![allow(unused_qualifications, clippy::new_without_default, unused_macros)] - -macro_rules! unwrap_or_clone_arc_into_variant { - ( - $arc:ident $(, .$field:tt)?, $pat:pat => $body:expr - ) => { - #[allow(unused_variables)] - match &(*$arc)$(.$field)? { - $pat => { - #[warn(unused_variables)] - match crate::helpers::unwrap_or_clone_arc($arc)$(.$field)? { - $pat => Some($body), - _ => unreachable!(), - } - }, - _ => None, - } - }; -} +#![allow(unused_qualifications, clippy::new_without_default)] mod authentication; mod chunk_iterator; From 281a79ffc6869690e6ab8b03012b0751408e36c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 24 Sep 2024 17:11:55 +0200 Subject: [PATCH 154/979] ffi: make `From` implementations for some event types use values, not references --- .../matrix-sdk-ffi/src/timeline/content.rs | 20 +++++++++---------- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index 0c8723cff67..3705e3ccd59 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -21,8 +21,8 @@ use ruma::events::room::MediaSource; use super::ProfileDetails; use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind}; -impl From<&matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent { - fn from(value: &matrix_sdk_ui::timeline::TimelineItemContent) -> Self { +impl From for TimelineItemContent { + fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self { use matrix_sdk_ui::timeline::TimelineItemContent as Content; match value { @@ -46,7 +46,7 @@ impl From<&matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent Content::CallNotify => TimelineItemContent::CallNotify, Content::UnableToDecrypt(msg) => { - TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(msg) } + TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) } } Content::MembershipChange(membership) => TimelineItemContent::RoomMembership { @@ -92,7 +92,7 @@ impl From<&matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent Content::FailedToParseState { event_type, state_key, error } => { TimelineItemContent::FailedToParseState { event_type: event_type.to_string(), - state_key: state_key.to_string(), + state_key, error: error.to_string(), } } @@ -110,12 +110,12 @@ pub struct MessageContent { pub mentions: Option, } -impl From<&matrix_sdk_ui::timeline::Message> for MessageContent { - fn from(value: &matrix_sdk_ui::timeline::Message) -> Self { +impl From for MessageContent { + fn from(value: matrix_sdk_ui::timeline::Message) -> Self { Self { msg_type: value.msgtype().clone().into(), body: value.body().to_owned(), - in_reply_to: value.in_reply_to().map(|r| Arc::new(r.into())), + in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())), is_edited: value.is_edited(), thread_root: value.thread_root().map(|id| id.to_string()), mentions: value.mentions().cloned().map(|m| m.into()), @@ -206,14 +206,14 @@ impl InReplyToDetails { } } -impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails { - fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self { +impl From for InReplyToDetails { + fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self { let event_id = inner.event_id.to_string(); let event = match &inner.event { TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable, TimelineDetails::Pending => RepliedToEventDetails::Pending, TimelineDetails::Ready(event) => RepliedToEventDetails::Ready { - content: event.content().into(), + content: event.content().clone().into(), sender: event.sender().to_string(), sender_profile: event.sender_profile().into(), }, diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 0777f470a8c..3d0b6224efe 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -630,7 +630,7 @@ impl Timeline { Ok(replied_to) => Ok(Arc::new(InReplyToDetails::new( event_id_str, RepliedToEventDetails::Ready { - content: replied_to.content().into(), + content: replied_to.content().clone().into(), sender: replied_to.sender().to_string(), sender_profile: replied_to.sender_profile().into(), }, @@ -1072,7 +1072,7 @@ impl From for EventTimelineItem { sender_profile: value.sender_profile().into(), is_own: value.is_own(), is_editable: value.is_editable(), - content: value.content().into(), + content: value.content().clone().into(), timestamp: value.timestamp().0.into(), reactions, debug_info, From 0082fbc0b41bb7f56e3957342cba58f36093b04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 25 Sep 2024 13:31:43 +0200 Subject: [PATCH 155/979] ffi: add `EventTimelineItemDebugInfoProvider` to lazily retrieve an event's debug info --- bindings/matrix-sdk-ffi/src/room_list.rs | 2 +- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 26 +++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index df6926bc8fe..7b518ec93b5 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -703,7 +703,7 @@ impl RoomListItem { } async fn latest_event(&self) -> Option { - self.inner.latest_event().await.map(|e| e.into()) + self.inner.latest_event().await.map(Into::into) } } diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 3d0b6224efe..ed8c2e4a2cf 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1032,7 +1032,7 @@ pub struct EventTimelineItem { content: TimelineItemContent, timestamp: u64, reactions: Vec, - debug_info: EventTimelineItemDebugInfo, + debug_info_provider: Arc, local_send_state: Option, read_receipts: HashMap, origin: Option, @@ -1056,11 +1056,7 @@ impl From for EventTimelineItem { .collect(), }) .collect(); - let debug_info = EventTimelineItemDebugInfo { - model: format!("{:#?}", value), - original_json: value.original_json().map(|raw| raw.json().get().to_owned()), - latest_edit_json: value.latest_edit_json().map(|raw| raw.json().get().to_owned()), - }; + let debug_info_provider = Arc::new(EventTimelineItemDebugInfoProvider(value.clone())); let read_receipts = value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect(); Self { @@ -1075,7 +1071,7 @@ impl From for EventTimelineItem { content: value.content().clone().into(), timestamp: value.timestamp().0.into(), reactions, - debug_info, + debug_info_provider, local_send_state: value.send_state().map(|s| s.into()), read_receipts, origin: value.origin(), @@ -1096,6 +1092,22 @@ impl From for Receipt { } } +/// Wrapper to retrieve the debug info lazily instead of immediately +/// transforming it for each timeline event. +#[derive(uniffi::Object)] +pub struct EventTimelineItemDebugInfoProvider(matrix_sdk_ui::timeline::EventTimelineItem); + +#[uniffi::export] +impl EventTimelineItemDebugInfoProvider { + fn get(&self) -> EventTimelineItemDebugInfo { + EventTimelineItemDebugInfo { + model: format!("{:#?}", self.0), + original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()), + latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()), + } + } +} + #[derive(Clone, uniffi::Record)] pub struct EventTimelineItemDebugInfo { model: String, From 263386ea53296321b9399428d965c77b5e9d93ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 25 Sep 2024 14:00:26 +0200 Subject: [PATCH 156/979] ffi: use `event_or_transaction_id` parameter name for Timeline functions that can take both --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index ed8c2e4a2cf..24d51b493d5 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -488,8 +488,15 @@ impl Timeline { /// /// Returns whether the edit did happen. It can only return false for /// local events that are being processed. - pub async fn edit(&self, id: String, new_content: EditedContent) -> Result { - self.inner.edit_by_id(&(id.into()), new_content.try_into()?).await.map_err(Into::into) + pub async fn edit( + &self, + event_or_transaction_id: String, + new_content: EditedContent, + ) -> Result { + self.inner + .edit_by_id(&(event_or_transaction_id.into()), new_content.try_into()?) + .await + .map_err(Into::into) } pub async fn send_location( @@ -596,10 +603,13 @@ impl Timeline { /// Will return an error if the event couldn't be redacted. pub async fn redact_event( &self, - id: String, + event_or_transaction_id: String, reason: Option, ) -> Result<(), ClientError> { - self.inner.redact_by_id(&(id.into()), reason.as_deref()).await.map_err(Into::into) + self.inner + .redact_by_id(&(event_or_transaction_id.into()), reason.as_deref()) + .await + .map_err(Into::into) } /// Load the reply details for the given event id. From 52898fa526ccf3715c16b44923a268d13cc475f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 27 Sep 2024 15:48:39 +0200 Subject: [PATCH 157/979] ffi: create `EventOrTransactionId` enum for functions that can receive both --- bindings/matrix-sdk-ffi/src/event.rs | 50 ++++++++++++++++++--- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 15 +++---- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index cd7f1392b0d..3cd4304a281 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -1,9 +1,14 @@ use anyhow::{bail, Context}; -use ruma::events::{ - room::{message::Relation, redaction::SyncRoomRedactionEvent}, - AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, - MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent, - RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent, +use matrix_sdk::IdParseError; +use matrix_sdk_ui::timeline::TimelineEventItemId; +use ruma::{ + events::{ + room::{message::Relation, redaction::SyncRoomRedactionEvent}, + AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, + MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent, + RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent, + }, + EventId, }; use crate::{ @@ -350,3 +355,38 @@ impl From for ruma::events::MessageLikeEventType { } } } + +/// Contains the 2 possible identifiers of an event, either it has a remote +/// event id or a local transaction id, never both or none. +#[derive(Clone, uniffi::Enum)] +pub enum EventOrTransactionId { + EventId { event_id: String }, + TransactionId { transaction_id: String }, +} + +impl From for EventOrTransactionId { + fn from(value: TimelineEventItemId) -> Self { + match value { + TimelineEventItemId::EventId(event_id) => { + EventOrTransactionId::EventId { event_id: event_id.to_string() } + } + TimelineEventItemId::TransactionId(transaction_id) => { + EventOrTransactionId::TransactionId { transaction_id: transaction_id.to_string() } + } + } + } +} + +impl TryFrom for TimelineEventItemId { + type Error = IdParseError; + fn try_from(value: EventOrTransactionId) -> Result { + match value { + EventOrTransactionId::EventId { event_id } => { + Ok(TimelineEventItemId::EventId(EventId::parse(event_id)?)) + } + EventOrTransactionId::TransactionId { transaction_id } => { + Ok(TimelineEventItemId::TransactionId(transaction_id.into())) + } + } + } +} diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 24d51b493d5..db5d17a7b06 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -67,6 +67,7 @@ use crate::client_builder::ClientBuilder; use crate::{ client::ProgressWatcher, error::{ClientError, RoomError}, + event::EventOrTransactionId, helpers::unwrap_or_clone_arc, ruma::{ AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, PollKind, ThumbnailInfo, @@ -490,11 +491,11 @@ impl Timeline { /// local events that are being processed. pub async fn edit( &self, - event_or_transaction_id: String, + event_or_transaction_id: EventOrTransactionId, new_content: EditedContent, ) -> Result { self.inner - .edit_by_id(&(event_or_transaction_id.into()), new_content.try_into()?) + .edit_by_id(&(event_or_transaction_id.try_into()?), new_content.try_into()?) .await .map_err(Into::into) } @@ -603,11 +604,11 @@ impl Timeline { /// Will return an error if the event couldn't be redacted. pub async fn redact_event( &self, - event_or_transaction_id: String, + event_or_transaction_id: EventOrTransactionId, reason: Option, ) -> Result<(), ClientError> { self.inner - .redact_by_id(&(event_or_transaction_id.into()), reason.as_deref()) + .redact_by_id(&(event_or_transaction_id.try_into()?), reason.as_deref()) .await .map_err(Into::into) } @@ -1033,8 +1034,7 @@ impl From for ShieldState { pub struct EventTimelineItem { is_local: bool, is_remote: bool, - transaction_id: Option, - event_id: Option, + event_or_transaction_id: EventOrTransactionId, sender: String, sender_profile: ProfileDetails, is_own: bool, @@ -1072,8 +1072,7 @@ impl From for EventTimelineItem { Self { is_local: value.is_local_echo(), is_remote: !value.is_local_echo(), - transaction_id: value.transaction_id().map(|t| t.to_string()), - event_id: value.event_id().map(|e| e.to_string()), + event_or_transaction_id: value.identifier().into(), sender: value.sender().to_string(), sender_profile: value.sender_profile().into(), is_own: value.is_own(), From ac61fc88304878575a8b3b70d284bbcf9dc875e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 27 Sep 2024 15:51:55 +0200 Subject: [PATCH 158/979] ffi: create `EventShieldsProvider` to load shields on demand in the clients --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 19 ++++++++++--- crates/matrix-sdk-ui/src/timeline/error.rs | 3 +++ crates/matrix-sdk-ui/src/timeline/mod.rs | 27 +++++++++++-------- .../tests/integration/timeline/mod.rs | 12 ++++++--- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index db5d17a7b06..8ca950f5c02 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1047,7 +1047,7 @@ pub struct EventTimelineItem { read_receipts: HashMap, origin: Option, can_be_replied_to: bool, - message_shield: Option, + shields_provider: Arc, } impl From for EventTimelineItem { @@ -1066,7 +1066,9 @@ impl From for EventTimelineItem { .collect(), }) .collect(); + let value = Arc::new(value); let debug_info_provider = Arc::new(EventTimelineItemDebugInfoProvider(value.clone())); + let shields_provider = Arc::new(EventShieldsProvider(value.clone())); let read_receipts = value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect(); Self { @@ -1085,7 +1087,7 @@ impl From for EventTimelineItem { read_receipts, origin: value.origin(), can_be_replied_to: value.can_be_replied_to(), - message_shield: value.get_shield(false).map(Into::into), + shields_provider, } } } @@ -1104,7 +1106,7 @@ impl From for Receipt { /// Wrapper to retrieve the debug info lazily instead of immediately /// transforming it for each timeline event. #[derive(uniffi::Object)] -pub struct EventTimelineItemDebugInfoProvider(matrix_sdk_ui::timeline::EventTimelineItem); +pub struct EventTimelineItemDebugInfoProvider(Arc); #[uniffi::export] impl EventTimelineItemDebugInfoProvider { @@ -1267,3 +1269,14 @@ impl TryFrom for SdkEditedContent { } } } + +/// Wrapper to retrieve the shields info lazily. +#[derive(Clone, uniffi::Object)] +pub struct EventShieldsProvider(Arc); + +#[uniffi::export] +impl EventShieldsProvider { + fn get_shields(&self, strict: bool) -> Option { + self.0.get_shield(strict).map(Into::into) + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index b1a89956474..2afb365e8b8 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -86,6 +86,9 @@ pub enum RedactError { #[error("Local event to redact wasn't found for transaction {0}")] LocalEventNotFound(OwnedTransactionId), + #[error("Local event with transaction id {0} had a remote `TimelineItemHandle`. This should never happen.")] + InvalidTimelineItemHandle(OwnedTransactionId), + /// An error happened while attempting to redact an event. #[error(transparent)] HttpError(#[from] HttpError), diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 77a3e325a8f..d8b3920044f 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -605,25 +605,30 @@ impl Timeline { ) -> Result<(), Error> { match id { TimelineEventItemId::TransactionId(transaction_id) => { - let Some(event) = self.item_by_transaction_id(transaction_id).await else { - return Err(Error::RedactError(RedactError::LocalEventNotFound( + let item = self.item_by_transaction_id(transaction_id).await; + + match item.as_ref().map(|i| i.handle()) { + Some(TimelineItemHandle::Local(handle)) => { + handle.abort().await.map_err(RoomSendQueueError::StorageError)?; + Ok(()) + } + Some(TimelineItemHandle::Remote(_)) => Err(Error::RedactError( + RedactError::InvalidTimelineItemHandle(transaction_id.to_owned()), + )), + None => Err(Error::RedactError(RedactError::LocalEventNotFound( transaction_id.to_owned(), - ))); - }; - let TimelineItemHandle::Local(handle) = event.handle() else { - panic!("If the item is local, this should never happen"); - }; - handle.abort().await.map_err(RoomSendQueueError::StorageError)?; + ))), + } } TimelineEventItemId::EventId(event_id) => { self.room() .redact(event_id, reason, None) .await - .map_err(RedactError::HttpError) - .map_err(Error::RedactError)?; + .map_err(|e| Error::RedactError(RedactError::HttpError(e)))?; + + Ok(()) } } - Ok(()) } /// Redact an event. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 0527382eb56..c05f83498ec 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -30,19 +30,23 @@ use matrix_sdk_test::{ }; use matrix_sdk_ui::{ timeline::{ - AnyOtherFullStateEventContent, EventSendState, RoomExt, TimelineItemContent, - VirtualTimelineItem, + AnyOtherFullStateEventContent, Error, EventSendState, RedactError, RoomExt, + TimelineEventItemId, TimelineItemContent, VirtualTimelineItem, }, RoomListService, Timeline, }; -use ruma::{event_id, events::room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, owned_event_id, room_id, user_id, MilliSecondsSinceUnixEpoch}; +use ruma::{ + event_id, + events::room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, + owned_event_id, room_id, user_id, MilliSecondsSinceUnixEpoch, +}; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; use wiremock::{ matchers::{header, method, path_regex}, Mock, ResponseTemplate, }; -use matrix_sdk_ui::timeline::{Error, RedactError, TimelineEventItemId}; + use crate::mock_sync; mod echo; From 743799fbd2670c506490bc8c605f9764f2676757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 30 Sep 2024 08:42:17 +0200 Subject: [PATCH 159/979] ffi: move the dependency override from `ffi/Cargo.toml` to the root one This seems to be the only way to make the log rotation fix work and avoid build warnings like: ``` warning: patch for the non root package will be ignored, specify patch at the workspace root: package: matrix-rust-sdk/bindings/matrix-sdk-ffi/Cargo.toml workspace: = matrix-rust-sdk/Cargo.toml Finished `dev` profile [unoptimized] target(s) in 0.30s ``` --- Cargo.lock | 21 +++++++-------------- Cargo.toml | 6 ++++++ bindings/matrix-sdk-ffi/Cargo.toml | 7 ------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be76deb7752..b983c7002ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4058,8 +4058,7 @@ dependencies = [ [[package]] name = "paranoid-android" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "101795d63d371b43e38d6e7254677657be82f17022f7f7893c268f33ac0caadc" +source = "git+https://github.com/element-hq/paranoid-android.git?rev=69388ac5b4afeed7be4401c70ce17f6d9a2cf19b#69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" dependencies = [ "lazy_static", "ndk-sys", @@ -6172,8 +6171,7 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd#ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" dependencies = [ "log", "pin-project-lite", @@ -6184,8 +6182,7 @@ dependencies = [ [[package]] name = "tracing-appender" version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd#ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" dependencies = [ "crossbeam-channel", "thiserror", @@ -6196,8 +6193,7 @@ dependencies = [ [[package]] name = "tracing-attributes" version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd#ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" dependencies = [ "proc-macro2", "quote", @@ -6207,8 +6203,7 @@ dependencies = [ [[package]] name = "tracing-core" version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd#ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" dependencies = [ "once_cell", "valuable", @@ -6227,8 +6222,7 @@ dependencies = [ [[package]] name = "tracing-log" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd#ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" dependencies = [ "log", "once_cell", @@ -6254,8 +6248,7 @@ dependencies = [ [[package]] name = "tracing-subscriber" version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd#ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" dependencies = [ "matchers", "nu-ansi-term", diff --git a/Cargo.toml b/Cargo.toml index 531d3d59158..3bced02cd96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,6 +122,12 @@ opt-level = 3 [patch.crates-io] async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" } const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" } +# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937) +tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd"} +tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } +tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } +tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } +paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" } [workspace.lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index c994382b663..abab2bff692 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -82,10 +82,3 @@ features = [ [lints] workspace = true - -[patch.crates-io] -tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd", default-features = false, features = ["std"] } -tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } -tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } -tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } -paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" } From fe648d9cb5b82a107e3d1830f3d50e6b162ac78e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 30 Sep 2024 11:23:22 +0200 Subject: [PATCH 160/979] event cache(refactoring): don't have related event rely on ordering This was because we used a `BTreeSet`, which doesn't make sense anymore since the data part of the key got mangled with some value unrelated to the key itself. --- crates/matrix-sdk/src/event_cache/mod.rs | 42 +++++------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 46ba056b80d..d3f1bcf2703 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -42,8 +42,7 @@ #![forbid(missing_docs)] use std::{ - cmp::Ordering, - collections::{BTreeMap, BTreeSet}, + collections::BTreeMap, fmt::Debug, sync::{Arc, OnceLock}, }; @@ -322,32 +321,7 @@ impl EventCache { } type AllEventsMap = BTreeMap; -type RelationsMap = BTreeMap>; - -/// Contains relationship information for a related event. -#[derive(Clone, Eq, PartialEq)] -struct RelatedEvent { - related_event_id: OwnedEventId, - relation_type: RelationType, -} - -impl RelatedEvent { - fn new(related_event_id: OwnedEventId, relation_type: RelationType) -> Self { - RelatedEvent { related_event_id, relation_type } - } -} - -impl PartialOrd for RelatedEvent { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for RelatedEvent { - fn cmp(&self, other: &Self) -> Ordering { - self.related_event_id.cmp(&other.related_event_id) - } -} +type RelationsMap = BTreeMap>; /// Cache wrapper containing both copies of received events and lists of event /// ids related to them. @@ -577,7 +551,7 @@ impl RoomEventCache { results: &mut Vec, ) { if let Some(related_event_ids) = cache.relations.get(event_id) { - for RelatedEvent { related_event_id, relation_type } in related_event_ids { + for (related_event_id, relation_type) in related_event_ids { if let Some(filter) = filter { if !filter.contains(relation_type) { continue; @@ -853,9 +827,11 @@ impl RoomEventCacheInner { { if let Some(redacted_event_id) = ev.content.redacts.as_ref().or(ev.redacts.as_ref()) { - cache.relations.entry(redacted_event_id.to_owned()).or_default().insert( - RelatedEvent::new(ev.event_id.to_owned(), RelationType::Replacement), - ); + cache + .relations + .entry(redacted_event_id.to_owned()) + .or_default() + .insert(ev.event_id.to_owned(), RelationType::Replacement); } } else { let relationship = match ev.original_content() { @@ -901,7 +877,7 @@ impl RoomEventCacheInner { .relations .entry(relationship.0) .or_default() - .insert(RelatedEvent::new(ev.event_id().to_owned(), relationship.1)); + .insert(ev.event_id().to_owned(), relationship.1); } } } From 68bc14e5677ae9a655c66225146ea3de42b00bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 30 Sep 2024 09:37:22 +0200 Subject: [PATCH 161/979] base: Expose room avatar info from RoomInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk-base/src/rooms/normal.rs | 15 +++- .../tests/integration/room/common.rs | 74 ++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 2c399a97856..399328edc6e 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -34,7 +34,7 @@ use ruma::{ ignored_user_list::IgnoredUserListEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ - avatar::RoomAvatarEventContent, + avatar::{self, RoomAvatarEventContent}, encryption::RoomEncryptionEventContent, guest_access::GuestAccess, history_visibility::HistoryVisibility, @@ -375,6 +375,11 @@ impl Room { self.inner.read().avatar_url().map(ToOwned::to_owned) } + /// Get information about the avatar of this room. + pub fn avatar_info(&self) -> Option { + self.inner.read().avatar_info().map(ToOwned::to_owned) + } + /// Get the canonical alias of this room. pub fn canonical_alias(&self) -> Option { self.inner.read().canonical_alias().map(ToOwned::to_owned) @@ -1298,6 +1303,14 @@ impl RoomInfo { }); } + /// Returns information about the current room avatar. + pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> { + self.base_info + .avatar + .as_ref() + .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref())) + } + /// Update the notifications count. pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) { self.notification_counts = notification_counts; diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index 50403965b66..c8096c1099e 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -1,6 +1,6 @@ use std::{iter, time::Duration}; -use assert_matches2::assert_let; +use assert_matches2::{assert_let, assert_matches}; use js_int::uint; use matrix_sdk::{ config::SyncSettings, room::RoomMember, test_utils::events::EventFactory, DisplayName, @@ -14,10 +14,14 @@ use matrix_sdk_test::{ use ruma::{ event_id, events::{ - room::{member::MembershipState, message::RoomMessageEventContent}, + room::{ + avatar::{self, RoomAvatarEventContent}, + member::MembershipState, + message::RoomMessageEventContent, + }, AnyStateEvent, AnySyncStateEvent, AnyTimelineEvent, StateEventType, }, - room_id, + mxc_uri, room_id, }; use serde_json::json; use wiremock::{ @@ -858,3 +862,67 @@ async fn test_is_direct() { assert!(room.direct_targets().is_empty()); assert!(!room.is_direct().await.unwrap()); } + +#[async_test] +async fn test_room_avatar() { + let (client, server) = logged_in_client_with_server().await; + let own_user_id = client.user_id().unwrap(); + + // Room without avatar. + mock_sync(&server, &*test_json::SYNC, None).await; + + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + assert_eq!(client.rooms().len(), 1); + let room_id = *DEFAULT_TEST_ROOM_ID; + let room = client.get_room(room_id).unwrap(); + + assert_eq!(room.avatar_url(), None); + assert_matches!(room.avatar_info(), None); + + let factory = EventFactory::new().room(room_id).sender(own_user_id); + + // Set the avatar, but not the info. + let avatar_url_1 = mxc_uri!("mxc://server.local/abcdef"); + + let mut content = RoomAvatarEventContent::new(); + content.url = Some(avatar_url_1.to_owned()); + let event = factory.event(content).state_key("").into_raw_sync(); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(event)); + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + assert_eq!(room.avatar_url().as_deref(), Some(avatar_url_1)); + assert_matches!(room.avatar_info(), None); + + // Set the avatar and the info. + let avatar_url_2 = mxc_uri!("mxc://server.local/ghijkl"); + let mut avatar_info_2 = avatar::ImageInfo::new(); + avatar_info_2.height = Some(uint!(200)); + avatar_info_2.width = Some(uint!(200)); + avatar_info_2.mimetype = Some("image/png".to_owned()); + avatar_info_2.size = Some(uint!(5243)); + + let mut content = RoomAvatarEventContent::new(); + content.url = Some(avatar_url_2.to_owned()); + content.info = Some(avatar_info_2.into()); + let event = factory.event(content).state_key("").into_raw_sync(); + + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(event)); + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + assert_eq!(room.avatar_url().as_deref(), Some(avatar_url_2)); + let avatar_info = room.avatar_info().unwrap(); + assert_eq!(avatar_info.height, Some(uint!(200))); + assert_eq!(avatar_info.width, Some(uint!(200))); + assert_eq!(avatar_info.mimetype.as_deref(), Some("image/png")); + assert_eq!(avatar_info.size, Some(uint!(5243))); +} From 99fe49bbac2a6abad6aab08fc98a41be6e39dd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 30 Sep 2024 11:18:02 +0200 Subject: [PATCH 162/979] sdk: Remove deprecated Room APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk-base/src/rooms/normal.rs | 14 ------ crates/matrix-sdk/src/room/mod.rs | 54 ---------------------- 2 files changed, 68 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 399328edc6e..2424907a466 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -814,20 +814,6 @@ impl Room { self.inner.read().heroes().to_vec() } - /// Get the list of `RoomMember`s that are considered to be joined members - /// of this room. - #[deprecated = "Use members with RoomMemberships::JOIN instead"] - pub async fn joined_members(&self) -> StoreResult> { - self.members(RoomMemberships::JOIN).await - } - - /// Get the list of `RoomMember`s that are considered to be joined or - /// invited members of this room. - #[deprecated = "Use members with RoomMemberships::ACTIVE instead"] - pub async fn active_members(&self) -> StoreResult> { - self.members(RoomMemberships::ACTIVE).await - } - /// Returns the number of members who have joined or been invited to the /// room. pub fn active_members_count(&self) -> u64 { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 9e4a94de6c1..1c7f6050d12 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -582,60 +582,6 @@ impl Room { } } - /// Get active members for this room, includes invited, joined members. - /// - /// *Note*: This method will fetch the members from the homeserver if the - /// member list isn't synchronized due to member lazy loading. Because of - /// that, it might panic if it isn't run on a tokio thread. - /// - /// Use [active_members_no_sync()](#method.active_members_no_sync) if you - /// want a method that doesn't do any requests. - #[deprecated = "Use members with RoomMemberships::ACTIVE instead"] - pub async fn active_members(&self) -> Result> { - self.sync_members().await?; - self.members_no_sync(RoomMemberships::ACTIVE).await - } - - /// Get active members for this room, includes invited, joined members. - /// - /// *Note*: This method will not fetch the members from the homeserver if - /// the member list isn't synchronized due to member lazy loading. Thus, - /// members could be missing from the list. - /// - /// Use [active_members()](#method.active_members) if you want to ensure to - /// always get the full member list. - #[deprecated = "Use members_no_sync with RoomMemberships::ACTIVE instead"] - pub async fn active_members_no_sync(&self) -> Result> { - self.members_no_sync(RoomMemberships::ACTIVE).await - } - - /// Get all the joined members of this room. - /// - /// *Note*: This method will fetch the members from the homeserver if the - /// member list isn't synchronized due to member lazy loading. Because of - /// that it might panic if it isn't run on a tokio thread. - /// - /// Use [joined_members_no_sync()](#method.joined_members_no_sync) if you - /// want a method that doesn't do any requests. - #[deprecated = "Use members with RoomMemberships::JOIN instead"] - pub async fn joined_members(&self) -> Result> { - self.sync_members().await?; - self.members_no_sync(RoomMemberships::JOIN).await - } - - /// Get all the joined members of this room. - /// - /// *Note*: This method will not fetch the members from the homeserver if - /// the member list isn't synchronized due to member lazy loading. Thus, - /// members could be missing from the list. - /// - /// Use [joined_members()](#method.joined_members) if you want to ensure to - /// always get the full member list. - #[deprecated = "Use members_no_sync with RoomMemberships::JOIN instead"] - pub async fn joined_members_no_sync(&self) -> Result> { - self.members_no_sync(RoomMemberships::JOIN).await - } - /// Get a specific member of this room. /// /// *Note*: This method will fetch the members from the homeserver if the From 1e0e815fab48146833bfedfb729d01d4ba8d360d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 30 Sep 2024 11:18:48 +0200 Subject: [PATCH 163/979] base: Remove deprecated StateStore APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/store/integration_tests.rs | 36 -------------- .../matrix-sdk-base/src/store/memory_store.rs | 21 +------- crates/matrix-sdk-base/src/store/traits.rs | 31 ------------ .../src/state_store/mod.rs | 38 +------------- crates/matrix-sdk-sqlite/src/state_store.rs | 49 +++---------------- 5 files changed, 8 insertions(+), 167 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 97b98d9e107..23dbddd70b7 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -71,8 +71,6 @@ pub trait StateStoreIntegrationTests { async fn test_receipts_saving(&self); /// Test custom storage. async fn test_custom_storage(&self) -> Result<()>; - /// Test invited room saving. - async fn test_persist_invited_room(&self) -> Result<()>; /// Test stripped and non-stripped room member saving. async fn test_stripped_non_stripped(&self) -> Result<()>; /// Test room removal. @@ -259,9 +257,6 @@ impl StateStoreIntegrationTests for DynStateStore { assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some()); assert!(self.get_presence_event(user_id).await?.is_some()); assert_eq!(self.get_room_infos().await?.len(), 2, "Expected to find 2 room infos"); - #[allow(deprecated)] - let stripped_rooms = self.get_stripped_room_infos().await?; - assert_eq!(stripped_rooms.len(), 1, "Expected to find 1 stripped room info"); assert!(self .get_account_data_event(GlobalAccountDataEventType::PushRules) .await? @@ -918,25 +913,12 @@ impl StateStoreIntegrationTests for DynStateStore { Ok(()) } - async fn test_persist_invited_room(&self) -> Result<()> { - self.populate().await?; - - #[allow(deprecated)] - let stripped_rooms = self.get_stripped_room_infos().await?; - assert_eq!(stripped_rooms.len(), 1); - - Ok(()) - } - async fn test_stripped_non_stripped(&self) -> Result<()> { let room_id = room_id!("!test_stripped_non_stripped:localhost"); let user_id = user_id(); assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none()); assert_eq!(self.get_room_infos().await.unwrap().len(), 0); - #[allow(deprecated)] - let stripped_rooms = self.get_stripped_room_infos().await?; - assert_eq!(stripped_rooms.len(), 0); let mut changes = StateChanges::default(); changes @@ -953,9 +935,6 @@ impl StateStoreIntegrationTests for DynStateStore { self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap(); assert!(matches!(member_event, MemberEvent::Sync(_))); assert_eq!(self.get_room_infos().await.unwrap().len(), 1); - #[allow(deprecated)] - let stripped_rooms = self.get_stripped_room_infos().await?; - assert_eq!(stripped_rooms.len(), 0); let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap(); assert_eq!(members, vec![user_id.to_owned()]); @@ -969,9 +948,6 @@ impl StateStoreIntegrationTests for DynStateStore { self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap(); assert!(matches!(member_event, MemberEvent::Stripped(_))); assert_eq!(self.get_room_infos().await.unwrap().len(), 1); - #[allow(deprecated)] - let stripped_rooms = self.get_stripped_room_infos().await?; - assert_eq!(stripped_rooms.len(), 1); let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap(); assert_eq!(members, vec![user_id.to_owned()]); @@ -989,9 +965,6 @@ impl StateStoreIntegrationTests for DynStateStore { self.remove_room(room_id).await?; assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there"); - #[allow(deprecated)] - let stripped_rooms = self.get_stripped_room_infos().await?; - assert_eq!(stripped_rooms.len(), 1); assert!(self.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none()); assert!( @@ -1044,9 +1017,6 @@ impl StateStoreIntegrationTests for DynStateStore { self.remove_room(stripped_room_id).await?; assert!(self.get_room_infos().await?.is_empty(), "still room info found"); - #[allow(deprecated)] - let stripped_rooms = self.get_stripped_room_infos().await?; - assert!(stripped_rooms.is_empty(), "still stripped room info found"); Ok(()) } @@ -1584,12 +1554,6 @@ macro_rules! statestore_integration_tests { store.test_custom_storage().await } - #[async_test] - async fn test_persist_invited_room() -> StoreResult<()> { - let store = get_store().await?.into_state_store(); - store.test_persist_invited_room().await - } - #[async_test] async fn test_stripped_non_stripped() -> StoreResult<()> { let store = get_store().await.unwrap().into_state_store(); diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 46c9f090aae..995c7113870 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -45,7 +45,7 @@ use super::{ }; use crate::{ deserialized_responses::RawAnySyncOrStrippedState, MinimalRoomMemberEvent, RoomMemberships, - RoomState, StateStoreDataKey, StateStoreDataValue, + StateStoreDataKey, StateStoreDataValue, }; /// In-memory, non-persistent implementation of the `StateStore`. @@ -696,29 +696,10 @@ impl StateStore for MemoryStore { Ok(get_user_ids_inner(&self.members.read().unwrap(), room_id, memberships)) } - async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result> { - StateStore::get_user_ids(self, room_id, RoomMemberships::INVITE).await - } - - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result> { - StateStore::get_user_ids(self, room_id, RoomMemberships::JOIN).await - } - async fn get_room_infos(&self) -> Result> { Ok(self.room_info.read().unwrap().values().cloned().collect()) } - async fn get_stripped_room_infos(&self) -> Result> { - Ok(self - .room_info - .read() - .unwrap() - .values() - .filter(|r| matches!(r.state(), RoomState::Invited)) - .cloned() - .collect()) - } - async fn get_users_with_display_name( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 1a6e3105efc..d96e3f24cb0 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -190,24 +190,9 @@ pub trait StateStore: AsyncTraitDeps { memberships: RoomMemberships, ) -> Result, Self::Error>; - /// Get all the user ids of members that are in the invited state for a - /// given room, for stripped and regular rooms alike. - #[deprecated = "Use get_user_ids with RoomMemberships::INVITE instead."] - async fn get_invited_user_ids(&self, room_id: &RoomId) - -> Result, Self::Error>; - - /// Get all the user ids of members that are in the joined state for a - /// given room, for stripped and regular rooms alike. - #[deprecated = "Use get_user_ids with RoomMemberships::JOIN instead."] - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result, Self::Error>; - /// Get all the pure `RoomInfo`s the store knows about. async fn get_room_infos(&self) -> Result, Self::Error>; - /// Get all the pure `RoomInfo`s the store knows about. - #[deprecated = "Use get_room_infos instead and filter by RoomState"] - async fn get_stripped_room_infos(&self) -> Result, Self::Error>; - /// Get all the users that use the given display name in the given room. /// /// # Arguments @@ -559,26 +544,10 @@ impl StateStore for EraseStateStoreError { self.0.get_user_ids(room_id, memberships).await.map_err(Into::into) } - async fn get_invited_user_ids( - &self, - room_id: &RoomId, - ) -> Result, Self::Error> { - self.0.get_user_ids(room_id, RoomMemberships::INVITE).await.map_err(Into::into) - } - - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result, Self::Error> { - self.0.get_user_ids(room_id, RoomMemberships::JOIN).await.map_err(Into::into) - } - async fn get_room_infos(&self) -> Result, Self::Error> { self.0.get_room_infos().await.map_err(Into::into) } - #[allow(deprecated)] - async fn get_stripped_room_infos(&self) -> Result, Self::Error> { - self.0.get_stripped_room_infos().await.map_err(Into::into) - } - async fn get_users_with_display_name( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 0d338dbaeb0..9f4c705aec9 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -29,8 +29,7 @@ use matrix_sdk_base::{ QueuedEvent, SerializableEventContent, ServerCapabilities, StateChanges, StateStore, StoreError, }, - MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateStoreDataKey, - StateStoreDataValue, + MinimalRoomMemberEvent, RoomInfo, RoomMemberships, StateStoreDataKey, StateStoreDataValue, }; use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ @@ -1087,33 +1086,6 @@ impl_state_store!({ Ok(entries) } - async fn get_stripped_room_infos(&self) -> Result> { - let txn = self - .inner - .transaction_on_one_with_mode(keys::ROOM_INFOS, IdbTransactionMode::Readonly)?; - let store = txn.object_store(keys::ROOM_INFOS)?; - - let mut infos = Vec::new(); - let cursor = store.open_cursor()?.await?; - - if let Some(cursor) = cursor { - loop { - let value = cursor.value(); - let info = self.deserialize_value::(&value)?; - - if info.state() == RoomState::Invited { - infos.push(info); - } - - if !cursor.continue_cursor()?.await? { - break; - } - } - } - - Ok(infos) - } - async fn get_users_with_display_name( &self, room_id: &RoomId, @@ -1324,14 +1296,6 @@ impl_state_store!({ self.get_user_ids_inner(room_id, memberships, false).await } - async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result> { - self.get_user_ids(room_id, RoomMemberships::INVITE).await - } - - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result> { - self.get_user_ids(room_id, RoomMemberships::JOIN).await - } - async fn save_send_queue_event( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 610974337ea..21682db911b 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -699,26 +699,12 @@ trait SqliteObjectStateStoreExt: SqliteAsyncConnExt { Ok(()) } - async fn get_room_infos(&self, states: Vec) -> Result>> { - if states.is_empty() { - Ok(self - .prepare("SELECT data FROM room_info", move |mut stmt| { - stmt.query_map((), |row| row.get(0))?.collect() - }) - .await?) - } else { - self.chunk_large_query_over(states, None, |txn, states| { - let sql_params = repeat_vars(states.len()); - let sql = format!("SELECT data FROM room_info WHERE state IN ({sql_params})"); - - Ok(txn - .prepare(&sql)? - .query(rusqlite::params_from_iter(states))? - .mapped(|row| row.get(0)) - .collect::>()?) + async fn get_room_infos(&self) -> Result>> { + Ok(self + .prepare("SELECT data FROM room_info", move |mut stmt| { + stmt.query_map((), |row| row.get(0))?.collect() }) - .await - } + .await?) } async fn get_maybe_stripped_state_events_for_keys( @@ -1444,30 +1430,10 @@ impl StateStore for SqliteStateStore { .collect() } - async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result> { - self.get_user_ids(room_id, RoomMemberships::INVITE).await - } - - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result> { - self.get_user_ids(room_id, RoomMemberships::JOIN).await - } - async fn get_room_infos(&self) -> Result> { self.acquire() .await? - .get_room_infos(Vec::new()) - .await? - .into_iter() - .map(|data| self.deserialize_json(&data)) - .collect() - } - - async fn get_stripped_room_infos(&self) -> Result> { - let states = - vec![self.encode_key(keys::ROOM_INFO, serde_json::to_string(&RoomState::Invited)?)]; - self.acquire() - .await? - .get_room_infos(states) + .get_room_infos() .await? .into_iter() .map(|data| self.deserialize_json(&data)) @@ -2115,9 +2081,6 @@ mod migration_tests { // Check all room infos are there. assert_eq!(store.get_room_infos().await.unwrap().len(), 5); - #[allow(deprecated)] - let stripped_rooms = store.get_stripped_room_infos().await.unwrap(); - assert_eq!(stripped_rooms.len(), 2); } // Add a room in version 2 format of the state store. From 866b6e5f2d84b33eb4592e628fccfcebe04e8f13 Mon Sep 17 00:00:00 2001 From: reivilibre Date: Mon, 30 Sep 2024 11:43:25 +0000 Subject: [PATCH 164/979] client builder: return a `ClientBuildError` when failing to build, instead of filtering out unexpected errors (#4016) This old method was checking invariants that were spooky-action-at-a-distance: these invariants have changed since then, so this would panic instead of returning a proper error to the caller. Signed-off-by: oliverw@element.io --------- Co-authored-by: Benjamin Bouvier --- crates/matrix-sdk/src/client/builder/mod.rs | 13 ------------- crates/matrix-sdk/src/client/mod.rs | 8 ++------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index cb67511d64a..0dfe938110b 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -709,19 +709,6 @@ pub enum ClientBuildError { SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError), } -impl ClientBuildError { - /// Assert that a valid homeserver URL was given to the builder and no other - /// invalid options were specified, which means the only possible error - /// case is [`Self::Http`]. - #[doc(hidden)] - pub fn assert_valid_builder_args(self) -> HttpError { - match self { - ClientBuildError::Http(e) => e, - _ => unreachable!("homeserver URL was asserted to be valid"), - } - } -} - // The http mocking library is not supported for wasm32 #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod tests { diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 18c190907d4..9905e158441 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -386,12 +386,8 @@ impl Client { /// # Arguments /// /// * `homeserver_url` - The homeserver that the client should connect to. - pub async fn new(homeserver_url: Url) -> Result { - Self::builder() - .homeserver_url(homeserver_url) - .build() - .await - .map_err(ClientBuildError::assert_valid_builder_args) + pub async fn new(homeserver_url: Url) -> Result { + Self::builder().homeserver_url(homeserver_url).build().await } /// Returns a subscriber that publishes an event every time the ignore user From 7efee5a5af767fccf8ea38ea258a656a366791b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 26 Sep 2024 12:14:36 +0200 Subject: [PATCH 165/979] refactor: Rename UserIdentity to OtherUserIdentity in the crypto crate --- crates/matrix-sdk-crypto/src/error.rs | 2 +- crates/matrix-sdk-crypto/src/identities/mod.rs | 2 +- crates/matrix-sdk-crypto/src/identities/user.rs | 16 ++++++++-------- crates/matrix-sdk-crypto/src/lib.rs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 849538b94c5..3cae36746fc 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -27,7 +27,7 @@ use crate::{ types::{events::room_key_withheld::WithheldCode, SignedKey}, }; #[cfg(doc)] -use crate::{CollectStrategy, Device, LocalTrust, UserIdentity}; +use crate::{CollectStrategy, Device, LocalTrust, OtherUserIdentity}; pub type OlmResult = Result; pub type MegolmResult = Result; diff --git a/crates/matrix-sdk-crypto/src/identities/mod.rs b/crates/matrix-sdk-crypto/src/identities/mod.rs index 3708a31b81a..1f51cdf0333 100644 --- a/crates/matrix-sdk-crypto/src/identities/mod.rs +++ b/crates/matrix-sdk-crypto/src/identities/mod.rs @@ -53,7 +53,7 @@ pub use device::{Device, DeviceData, LocalTrust, UserDevices}; pub(crate) use manager::IdentityManager; use serde::{Deserialize, Deserializer, Serializer}; pub use user::{ - OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData, UserIdentities, UserIdentity, + OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData, UserIdentities, OtherUserIdentity, UserIdentityData, }; diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 7b5af637cc9..e07057acbd0 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -47,7 +47,7 @@ pub enum UserIdentities { /// Our own user identity. Own(OwnUserIdentity), /// An identity belonging to another user. - Other(UserIdentity), + Other(OtherUserIdentity), } impl UserIdentities { @@ -59,7 +59,7 @@ impl UserIdentities { /// Destructure the enum into an `UserIdentity` if it's of the correct /// type. - pub fn other(self) -> Option { + pub fn other(self) -> Option { as_variant!(self, Self::Other) } @@ -82,7 +82,7 @@ impl UserIdentities { Self::Own(OwnUserIdentity { inner: i, verification_machine, store }) } UserIdentityData::Other(i) => { - Self::Other(UserIdentity { inner: i, own_identity, verification_machine }) + Self::Other(OtherUserIdentity { inner: i, own_identity, verification_machine }) } } } @@ -140,8 +140,8 @@ impl From for UserIdentities { } } -impl From for UserIdentities { - fn from(i: UserIdentity) -> Self { +impl From for UserIdentities { + fn from(i: OtherUserIdentity) -> Self { Self::Other(i) } } @@ -268,13 +268,13 @@ impl OwnUserIdentity { /// This struct wraps a read-only version of the struct and allows verifications /// to be requested to verify our own device with the user identity. #[derive(Debug, Clone)] -pub struct UserIdentity { +pub struct OtherUserIdentity { pub(crate) inner: OtherUserIdentityData, pub(crate) own_identity: Option, pub(crate) verification_machine: VerificationMachine, } -impl Deref for UserIdentity { +impl Deref for OtherUserIdentity { type Target = OtherUserIdentityData; fn deref(&self) -> &Self::Target { @@ -282,7 +282,7 @@ impl Deref for UserIdentity { } } -impl UserIdentity { +impl OtherUserIdentity { /// Is this user identity verified. pub fn is_verified(&self) -> bool { self.own_identity diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 41c110a3b60..8a4a22f1cec 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -81,7 +81,7 @@ pub use file_encryption::{ pub use gossiping::{GossipRequest, GossippedSecret}; pub use identities::{ Device, DeviceData, LocalTrust, OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData, - UserDevices, UserIdentities, UserIdentity, UserIdentityData, + UserDevices, UserIdentities, OtherUserIdentity, UserIdentityData, }; pub use machine::{CrossSigningBootstrapRequests, EncryptionSyncChanges, OlmMachine}; #[cfg(feature = "qrcode")] From e7bc5103137d37d8a73720cc9576422cceba5e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 26 Sep 2024 12:51:28 +0200 Subject: [PATCH 166/979] refactor: Rename the UserIdentities enum into UserIdentity --- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 6 ++-- bindings/matrix-sdk-crypto-ffi/src/users.rs | 8 +++--- .../src/identities/manager.rs | 4 +-- .../matrix-sdk-crypto/src/identities/mod.rs | 2 +- .../matrix-sdk-crypto/src/identities/user.rs | 28 +++++++++---------- crates/matrix-sdk-crypto/src/lib.rs | 4 +-- crates/matrix-sdk-crypto/src/machine/mod.rs | 4 +-- .../tests/decryption_verification_state.rs | 6 ++-- .../group_sessions/share_strategy.rs | 2 +- crates/matrix-sdk-crypto/src/store/mod.rs | 14 +++++----- .../src/encryption/identities/users.rs | 2 +- 11 files changed, 40 insertions(+), 40 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index b8920f6dd90..3cfabeaf49a 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -18,7 +18,7 @@ use matrix_sdk_crypto::{ olm::ExportedRoomKey, store::{BackupDecryptionKey, Changes}, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, TrustRequirement, - UserIdentities, + UserIdentity as SdkUserIdentity, }; use ruma::{ api::{ @@ -314,8 +314,8 @@ impl OlmMachine { if let Some(user_identity) = user_identity { Ok(match user_identity { - UserIdentities::Own(i) => self.runtime.block_on(i.verify())?, - UserIdentities::Other(i) => self.runtime.block_on(i.verify())?, + SdkUserIdentity::Own(i) => self.runtime.block_on(i.verify())?, + SdkUserIdentity::Other(i) => self.runtime.block_on(i.verify())?, } .into()) } else { diff --git a/bindings/matrix-sdk-crypto-ffi/src/users.rs b/bindings/matrix-sdk-crypto-ffi/src/users.rs index 485d4be7ced..6120f4c8319 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/users.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/users.rs @@ -1,4 +1,4 @@ -use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentities}; +use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity}; use crate::CryptoStoreError; @@ -31,9 +31,9 @@ pub enum UserIdentity { } impl UserIdentity { - pub(crate) async fn from_rust(i: UserIdentities) -> Result { + pub(crate) async fn from_rust(i: SdkUserIdentity) -> Result { Ok(match i { - UserIdentities::Own(i) => { + SdkUserIdentity::Own(i) => { let master: CrossSigningKey = i.master_key().as_ref().to_owned(); let user_signing: CrossSigningKey = i.user_signing_key().as_ref().to_owned(); let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned(); @@ -46,7 +46,7 @@ impl UserIdentity { self_signing_key: serde_json::to_string(&self_signing)?, } } - UserIdentities::Other(i) => { + SdkUserIdentity::Other(i) => { let master: CrossSigningKey = i.master_key().as_ref().to_owned(); let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned(); diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index 7819d7acfac..512669731c2 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -39,7 +39,7 @@ use crate::{ Result as StoreResult, Store, StoreCache, StoreCacheGuard, UserKeyQueryResult, }, types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey}, - CryptoStoreError, LocalTrust, OwnUserIdentity, SignatureError, UserIdentities, + CryptoStoreError, LocalTrust, OwnUserIdentity, SignatureError, UserIdentity, }; enum DeviceChange { @@ -749,7 +749,7 @@ impl IdentityManager { .store .get_identity(self.user_id()) .await? - .and_then(UserIdentities::own) + .and_then(UserIdentity::own) .filter(|own| own.is_verified()); for (user_id, master_key) in &response.master_keys { diff --git a/crates/matrix-sdk-crypto/src/identities/mod.rs b/crates/matrix-sdk-crypto/src/identities/mod.rs index 1f51cdf0333..fac4e8e02ac 100644 --- a/crates/matrix-sdk-crypto/src/identities/mod.rs +++ b/crates/matrix-sdk-crypto/src/identities/mod.rs @@ -53,7 +53,7 @@ pub use device::{Device, DeviceData, LocalTrust, UserDevices}; pub(crate) use manager::IdentityManager; use serde::{Deserialize, Deserializer, Serializer}; pub use user::{ - OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData, UserIdentities, OtherUserIdentity, + OtherUserIdentity, OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData, UserIdentity, UserIdentityData, }; diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index e07057acbd0..f7830c9dcbe 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -43,14 +43,14 @@ use crate::{ /// Enum over the different user identity types we can have. #[derive(Debug, Clone)] -pub enum UserIdentities { +pub enum UserIdentity { /// Our own user identity. Own(OwnUserIdentity), /// An identity belonging to another user. Other(OtherUserIdentity), } -impl UserIdentities { +impl UserIdentity { /// Destructure the enum into an `OwnUserIdentity` if it's of the correct /// type. pub fn own(self) -> Option { @@ -66,8 +66,8 @@ impl UserIdentities { /// Get the ID of the user this identity belongs to. pub fn user_id(&self) -> &UserId { match self { - UserIdentities::Own(u) => u.user_id(), - UserIdentities::Other(u) => u.user_id(), + UserIdentity::Own(u) => u.user_id(), + UserIdentity::Other(u) => u.user_id(), } } @@ -99,8 +99,8 @@ impl UserIdentities { /// by our own user-signing key. pub fn is_verified(&self) -> bool { match self { - UserIdentities::Own(u) => u.is_verified(), - UserIdentities::Other(u) => u.is_verified(), + UserIdentity::Own(u) => u.is_verified(), + UserIdentity::Other(u) => u.is_verified(), } } @@ -110,8 +110,8 @@ impl UserIdentities { /// [`UserIdentities::withdraw_verification()`]. pub fn was_previously_verified(&self) -> bool { match self { - UserIdentities::Own(u) => u.was_previously_verified(), - UserIdentities::Other(u) => u.was_previously_verified(), + UserIdentity::Own(u) => u.was_previously_verified(), + UserIdentity::Other(u) => u.was_previously_verified(), } } @@ -120,27 +120,27 @@ impl UserIdentities { /// [`Self::has_verification_violation`]. pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> { match self { - UserIdentities::Own(u) => u.withdraw_verification().await, - UserIdentities::Other(u) => u.withdraw_verification().await, + UserIdentity::Own(u) => u.withdraw_verification().await, + UserIdentity::Other(u) => u.withdraw_verification().await, } } /// Was this identity previously verified, and is no longer? pub fn has_verification_violation(&self) -> bool { match self { - UserIdentities::Own(u) => u.has_verification_violation(), - UserIdentities::Other(u) => u.has_verification_violation(), + UserIdentity::Own(u) => u.has_verification_violation(), + UserIdentity::Other(u) => u.has_verification_violation(), } } } -impl From for UserIdentities { +impl From for UserIdentity { fn from(i: OwnUserIdentity) -> Self { Self::Own(i) } } -impl From for UserIdentities { +impl From for UserIdentity { fn from(i: OtherUserIdentity) -> Self { Self::Other(i) } diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 8a4a22f1cec..d46fbbbe523 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -80,8 +80,8 @@ pub use file_encryption::{ }; pub use gossiping::{GossipRequest, GossippedSecret}; pub use identities::{ - Device, DeviceData, LocalTrust, OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData, - UserDevices, UserIdentities, OtherUserIdentity, UserIdentityData, + Device, DeviceData, LocalTrust, OtherUserIdentity, OtherUserIdentityData, OwnUserIdentity, + OwnUserIdentityData, UserDevices, UserIdentity, UserIdentityData, }; pub use machine::{CrossSigningBootstrapRequests, EncryptionSyncChanges, OlmMachine}; #[cfg(feature = "qrcode")] diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 95a435a25dd..81aadb2e572 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -63,7 +63,7 @@ use crate::{ dehydrated_devices::{DehydratedDevices, DehydrationError}, error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult, SetRoomSettingsError}, gossiping::GossipMachine, - identities::{user::UserIdentities, Device, IdentityManager, UserDevices}, + identities::{user::UserIdentity, Device, IdentityManager, UserDevices}, olm::{ Account, CrossSigningStatus, EncryptionSettings, IdentityKeys, InboundGroupSession, KnownSenderData, OlmDecryptionInfo, PrivateCrossSigningIdentity, SenderData, @@ -2098,7 +2098,7 @@ impl OlmMachine { &self, user_id: &UserId, timeout: Option, - ) -> StoreResult> { + ) -> StoreResult> { self.wait_if_user_pending(user_id, timeout).await?; self.store().get_identity(user_id).await } diff --git a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs index 8ce5cd12075..2187b615215 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs @@ -46,7 +46,7 @@ use crate::{ }, utilities::json_convert, CryptoStoreError, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, OlmMachine, - OtherUserIdentityData, OutgoingRequests, TrustRequirement, UserIdentities, + OtherUserIdentityData, OutgoingRequests, TrustRequirement, UserIdentity, }; #[async_test] @@ -152,9 +152,9 @@ async fn test_decryption_verification_state() { tests::setup_cross_signing_for_machine_test_helper(&alice, &bob).await; let bob_id_from_alice = alice.get_identity(bob.user_id(), None).await.unwrap(); - assert_matches!(bob_id_from_alice, Some(UserIdentities::Other(_))); + assert_matches!(bob_id_from_alice, Some(UserIdentity::Other(_))); let alice_id_from_bob = bob.get_identity(alice.user_id(), None).await.unwrap(); - assert_matches!(alice_id_from_bob, Some(UserIdentities::Other(_))); + assert_matches!(alice_id_from_bob, Some(UserIdentity::Other(_))); // we setup cross signing but nothing is signed yet let encryption_info = bob.get_room_event_encryption_info(&event, room_id).await.unwrap(); diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 6cd2047b640..9565ad4f168 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -31,7 +31,7 @@ use crate::{ DeviceData, EncryptionSettings, LocalTrust, OlmError, OwnUserIdentityData, UserIdentityData, }; #[cfg(doc)] -use crate::{Device, UserIdentities}; +use crate::{Device, UserIdentity}; /// Strategy to collect the devices that should receive room keys for the /// current discussion. diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index c06de547612..5d322b5b45d 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -65,7 +65,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{backups::BackupMachine, identities::OwnUserIdentity}; use crate::{ gossiping::GossippedSecret, - identities::{user::UserIdentities, Device, DeviceData, UserDevices, UserIdentityData}, + identities::{user::UserIdentity, Device, DeviceData, UserDevices, UserIdentityData}, olm::{ Account, ExportedRoomKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, Session, StaticAccountData, @@ -700,11 +700,11 @@ pub struct IdentityUpdates { /// A identity being in this list does not necessarily mean that the /// identity was just created, it just means that it's the first time /// we're seeing this identity. - pub new: BTreeMap, + pub new: BTreeMap, /// The list of changed identities. - pub changed: BTreeMap, + pub changed: BTreeMap, /// The list of unchanged identities. - pub unchanged: BTreeMap, + pub unchanged: BTreeMap, } /// The private part of a backup key. @@ -1212,7 +1212,7 @@ impl Store { } /// Get the Identity of `user_id` - pub(crate) async fn get_identity(&self, user_id: &UserId) -> Result> { + pub(crate) async fn get_identity(&self, user_id: &UserId) -> Result> { let own_identity = self .inner .store @@ -1221,7 +1221,7 @@ impl Store { .and_then(as_variant!(UserIdentityData::Own)); Ok(self.inner.store.get_user_identity(user_id).await?.map(|i| { - UserIdentities::new( + UserIdentity::new( self.clone(), i, self.inner.verification_machine.to_owned(), @@ -1581,7 +1581,7 @@ impl Store { let map_identity = |(user_id, identity)| { ( user_id, - UserIdentities::new( + UserIdentity::new( this.clone(), identity, verification_machine.to_owned(), diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index 25b8a4e6c0d..aa869d483ba 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -15,7 +15,7 @@ use std::collections::BTreeMap; use matrix_sdk_base::{ - crypto::{types::MasterPubkey, CryptoStoreError, UserIdentities as CryptoUserIdentities}, + crypto::{types::MasterPubkey, CryptoStoreError, UserIdentity as CryptoUserIdentities}, RoomMemberships, }; use ruma::{ From c2ab79512241b5143607db8dd8a6315f08252e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 26 Sep 2024 13:06:09 +0200 Subject: [PATCH 167/979] doc: Fix some doc links for the user identities --- crates/matrix-sdk-crypto/src/error.rs | 2 +- .../matrix-sdk-crypto/src/identities/user.rs | 31 +++++++++---------- .../group_sessions/share_strategy.rs | 4 +-- crates/matrix-sdk-crypto/src/store/mod.rs | 2 +- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 3cae36746fc..89fe5c5edaf 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -405,7 +405,7 @@ pub enum SessionRecipientCollectionError { /// * re-verify the problematic recipients, or /// /// * withdraw verification of the problematic recipients with - /// [`UserIdentity::withdraw_verification`], or + /// [`OtherUserIdentity::withdraw_verification`], or /// /// * set the trust level of all of the devices belonging to the problematic /// recipients to [`LocalTrust::Ignored`] or [`LocalTrust::BlackListed`] diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index f7830c9dcbe..6a2e0b06faf 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -51,14 +51,14 @@ pub enum UserIdentity { } impl UserIdentity { - /// Destructure the enum into an `OwnUserIdentity` if it's of the correct + /// Destructure the enum into an [`OwnUserIdentity`] if it's of the correct /// type. pub fn own(self) -> Option { as_variant!(self, Self::Own) } - /// Destructure the enum into an `UserIdentity` if it's of the correct - /// type. + /// Destructure the enum into an [`OtherUserIdentity`] if it's of the + /// correct type. pub fn other(self) -> Option { as_variant!(self, Self::Other) } @@ -107,7 +107,7 @@ impl UserIdentity { /// True if we verified this identity at some point in the past. /// /// To reset this latch back to `false`, one must call - /// [`UserIdentities::withdraw_verification()`]. + /// [`UserIdentity::withdraw_verification()`]. pub fn was_previously_verified(&self) -> bool { match self { UserIdentity::Own(u) => u.was_previously_verified(), @@ -116,8 +116,8 @@ impl UserIdentity { } /// Reset the flag that records that the identity has been verified, thus - /// clearing [`Self::was_previously_verified`] and - /// [`Self::has_verification_violation`]. + /// clearing [`UserIdentity::was_previously_verified`] and + /// [`UserIdentity::has_verification_violation`]. pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> { match self { UserIdentity::Own(u) => u.withdraw_verification().await, @@ -315,7 +315,7 @@ impl OtherUserIdentity { } } - /// Create a `VerificationRequest` object after the verification request + /// Create a [`VerificationRequest`] object after the verification request /// content has been sent out. pub fn request_verification( &self, @@ -336,10 +336,8 @@ impl OtherUserIdentity { /// The returned content needs to be sent out into a DM room with the given /// user. /// - /// After the content has been sent out a `VerificationRequest` can be - /// started with the [`request_verification()`] method. - /// - /// [`request_verification()`]: #method.request_verification + /// After the content has been sent out a [`VerificationRequest`] can be + /// started with the [`OtherUserIdentity::request_verification()`] method. pub fn verification_request_content( &self, methods: Option>, @@ -374,9 +372,9 @@ impl OtherUserIdentity { /// This situation can be resolved by: /// /// - Verifying the new identity with - /// [`UserIdentity::request_verification`], or: + /// [`OtherUserIdentity::request_verification`], or: /// - Updating the pin to the new identity with - /// [`UserIdentity::pin_current_master_key`]. + /// [`OtherUserIdentity::pin_current_master_key`]. pub fn identity_needs_user_approval(&self) -> bool { // First check if the current identity is verified. if self.is_verified() { @@ -417,9 +415,10 @@ impl OtherUserIdentity { /// Such a violation should be reported to the local user by the /// application, and resolved by /// - /// - Verifying the new identity with [`UserIdentity::request_verification`] + /// - Verifying the new identity with + /// [`OtherUserIdentity::request_verification`] /// - Or by withdrawing the verification requirement - /// [`UserIdentity::withdraw_verification`]. + /// [`OtherUserIdentity::withdraw_verification`]. pub fn has_verification_violation(&self) -> bool { if !self.inner.was_previously_verified() { // If that identity has never been verified it cannot be in violation. @@ -488,7 +487,7 @@ impl UserIdentityData { /// True if we verified our own identity at some point in the past. /// /// To reset this latch back to `false`, one must call - /// [`UserIdentities::withdraw_verification()`]. + /// [`UserIdentity::withdraw_verification()`]. pub fn was_previously_verified(&self) -> bool { match self { UserIdentityData::Own(i) => i.was_previously_verified(), diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 9565ad4f168..71af660c0b2 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -486,9 +486,9 @@ fn is_unsigned_device_of_verified_user( /// Check if the user was previously verified, but they have now changed their /// identity so that they are no longer verified. /// -/// This is much the same as [`UserIdentities::has_verification_violation`], but +/// This is much the same as [`UserIdentity::has_verification_violation`], but /// works with a low-level [`UserIdentityData`] rather than higher-level -/// [`UserIdentities`]. +/// [`UserIdentity`]. fn has_identity_verification_violation( own_identity: Option<&OwnUserIdentityData>, device_owner_identity: Option<&UserIdentityData>, diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 5d322b5b45d..de19b08a4db 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -691,7 +691,7 @@ pub struct DeviceUpdates { pub changed: BTreeMap>, } -/// Updates about [`UserIdentities`] which got received over the `/keys/query` +/// Updates about [`UserIdentity`]s which got received over the `/keys/query` /// endpoint. #[derive(Clone, Debug, Default)] pub struct IdentityUpdates { From dc055c632c106c6392eeea8efd24c3bb7d756fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 26 Sep 2024 13:09:42 +0200 Subject: [PATCH 168/979] chore: Update the changelog to mention the UserIdentity renames --- crates/matrix-sdk-crypto/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index f4e3241ee62..c45f2fc52da 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,12 @@ Changes: +- The `UserIdentity` struct has been renamed to `OtherUserIdentity` + ([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036])) + +- The `UserIdentities` enum has been renamed to `UserIdentity` + ([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036])) + - Change the withheld code for keys not shared due to the `IdentityBasedStrategy`, from `m.unauthorised` to `m.unverified`. ([#3985](https://github.com/matrix-org/matrix-rust-sdk/pull/3985)) From 3fd2f5794e2cb7f7272de0597fa3df7b97c59055 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 30 Sep 2024 09:13:07 +0200 Subject: [PATCH 169/979] crypto: Expose `with_decryption_trust_requirement` for ClientBuilder --- crates/matrix-sdk-base/src/client.rs | 12 ++++++++++-- crates/matrix-sdk/CHANGELOG.md | 1 + crates/matrix-sdk/src/client/builder/mod.rs | 17 ++++++++++++++++- crates/matrix-sdk/src/room/mod.rs | 7 ++++--- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 8be5a1f1ca0..67e58324844 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -117,6 +117,10 @@ pub struct BaseClient { /// encrypted message. #[cfg(feature = "e2e-encryption")] pub room_key_recipient_strategy: CollectStrategy, + + /// The trust requirement to use for decrypting events. + #[cfg(feature = "e2e-encryption")] + pub decryption_trust_requirement: TrustRequirement, } #[cfg(not(tarpaulin_include))] @@ -156,6 +160,8 @@ impl BaseClient { room_info_notable_update_sender, #[cfg(feature = "e2e-encryption")] room_key_recipient_strategy: Default::default(), + #[cfg(feature = "e2e-encryption")] + decryption_trust_requirement: TrustRequirement::Untrusted, } } @@ -180,6 +186,7 @@ impl BaseClient { ignore_user_list_changes: Default::default(), room_info_notable_update_sender: self.room_info_notable_update_sender.clone(), room_key_recipient_strategy: self.room_key_recipient_strategy.clone(), + decryption_trust_requirement: self.decryption_trust_requirement, }; if let Some(session_meta) = self.session_meta().cloned() { @@ -345,8 +352,9 @@ impl BaseClient { let olm = self.olm_machine().await; let Some(olm) = olm.as_ref() else { return Ok(None) }; - let decryption_settings = - DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decryption_settings = DecryptionSettings { + sender_device_trust_requirement: self.decryption_trust_requirement, + }; let event: SyncTimelineEvent = olm.decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await?.into(); diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 3705158a858..95c6e781d30 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -27,6 +27,7 @@ Breaking changes: Additions: +- new `ClientBuilder::with_decryption_trust_requirement` method. - new `ClientBuilder::with_room_key_recipient_strategy` method - new `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData setters, analogous to the GlobalAccountData - new `RequestConfig.max_concurrent_requests` which allows to limit the maximum number of concurrent requests the internal HTTP client issues (all others have to wait until the number drops below that threshold again) diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 0dfe938110b..01125de9187 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -29,7 +29,7 @@ use tracing::{debug, field::debug, instrument, Span}; use super::{Client, ClientInner}; #[cfg(feature = "e2e-encryption")] -use crate::crypto::CollectStrategy; +use crate::crypto::{CollectStrategy, TrustRequirement}; #[cfg(feature = "e2e-encryption")] use crate::encryption::EncryptionSettings; #[cfg(not(target_arch = "wasm32"))] @@ -99,6 +99,8 @@ pub struct ClientBuilder { encryption_settings: EncryptionSettings, #[cfg(feature = "e2e-encryption")] room_key_recipient_strategy: CollectStrategy, + #[cfg(feature = "e2e-encryption")] + decryption_trust_requirement: TrustRequirement, } impl ClientBuilder { @@ -118,6 +120,8 @@ impl ClientBuilder { encryption_settings: Default::default(), #[cfg(feature = "e2e-encryption")] room_key_recipient_strategy: Default::default(), + #[cfg(feature = "e2e-encryption")] + decryption_trust_requirement: TrustRequirement::Untrusted, } } @@ -407,6 +411,16 @@ impl ClientBuilder { self } + /// Set the trust requirement to be used when decrypting events. + #[cfg(feature = "e2e-encryption")] + pub fn with_decryption_trust_requirement( + mut self, + trust_requirement: TrustRequirement, + ) -> Self { + self.decryption_trust_requirement = trust_requirement; + self + } + /// Create a [`Client`] with the options set on this builder. /// /// # Errors @@ -445,6 +459,7 @@ impl ClientBuilder { #[cfg(feature = "e2e-encryption")] { client.room_key_recipient_strategy = self.room_key_recipient_strategy; + client.decryption_trust_requirement = self.decryption_trust_requirement; } client }; diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 1c7f6050d12..20ad04d6adb 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -15,7 +15,7 @@ use futures_util::{ stream::FuturesUnordered, }; #[cfg(feature = "e2e-encryption")] -use matrix_sdk_base::crypto::{DecryptionSettings, TrustRequirement}; +use matrix_sdk_base::crypto::DecryptionSettings; use matrix_sdk_base::{ deserialized_responses::{ RawAnySyncOrStrippedState, RawSyncOrStrippedState, SyncOrStrippedState, TimelineEvent, @@ -1183,8 +1183,9 @@ impl Room { let machine = self.client.olm_machine().await; let machine = machine.as_ref().ok_or(Error::NoOlmMachine)?; - let decryption_settings = - DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decryption_settings = DecryptionSettings { + sender_device_trust_requirement: self.client.base_client().decryption_trust_requirement, + }; let mut event = match machine .decrypt_room_event(event.cast_ref(), self.inner.room_id(), &decryption_settings) .await From 806ee13aa08da50342c4ae99c0aaa4e4951f2531 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 30 Sep 2024 11:19:10 +0200 Subject: [PATCH 170/979] ffi: Expose `room_decryption_trust_requirement` for ClientBuilder --- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 5 ++--- bindings/matrix-sdk-ffi/src/client_builder.rs | 17 +++++++++++++++-- crates/matrix-sdk-crypto/src/lib.rs | 2 ++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 3cfabeaf49a..9e978fbb4f6 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -17,7 +17,7 @@ use matrix_sdk_crypto::{ decrypt_room_key_export, encrypt_room_key_export, olm::ExportedRoomKey, store::{BackupDecryptionKey, Changes}, - DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, TrustRequirement, + DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentity as SdkUserIdentity, }; use ruma::{ @@ -867,6 +867,7 @@ impl OlmMachine { room_id: String, handle_verification_events: bool, strict_shields: bool, + decryption_settings: DecryptionSettings, ) -> Result { // Element Android wants only the content and the type and will create a // decrypted event with those two itself, this struct makes sure we @@ -882,8 +883,6 @@ impl OlmMachine { let event: Raw<_> = serde_json::from_str(&event)?; let room_id = RoomId::parse(room_id)?; - let decryption_settings = - DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; let decrypted = self.runtime.block_on(self.inner.decrypt_room_event( &event, &room_id, diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index f619daedf1a..ec0522fafd2 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -5,7 +5,7 @@ use matrix_sdk::{ authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason}, crypto::{ types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData}, - CollectStrategy, + CollectStrategy, TrustRequirement, }, encryption::{BackupDownloadStrategy, EncryptionSettings}, reqwest::Certificate, @@ -266,6 +266,7 @@ pub struct ClientBuilder { disable_built_in_root_certificates: bool, encryption_settings: EncryptionSettings, room_key_recipient_strategy: CollectStrategy, + decryption_trust_requirement: TrustRequirement, request_config: Option, } @@ -294,6 +295,7 @@ impl ClientBuilder { auto_enable_backups: false, }, room_key_recipient_strategy: Default::default(), + decryption_trust_requirement: TrustRequirement::Untrusted, request_config: Default::default(), }) } @@ -449,6 +451,16 @@ impl ClientBuilder { Arc::new(builder) } + /// Set the trust requirement to be used when decrypting events. + pub fn room_decryption_trust_requirement( + self: Arc, + trust_requirement: TrustRequirement, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.decryption_trust_requirement = trust_requirement; + Arc::new(builder) + } + /// Add a default request config to this client. pub fn request_config(self: Arc, config: RequestConfig) -> Arc { let mut builder = unwrap_or_clone_arc(self); @@ -548,7 +560,8 @@ impl ClientBuilder { inner_builder = inner_builder .with_encryption_settings(builder.encryption_settings) - .with_room_key_recipient_strategy(builder.room_key_recipient_strategy); + .with_room_key_recipient_strategy(builder.room_key_recipient_strategy) + .with_decryption_trust_requirement(builder.decryption_trust_requirement); match builder.sliding_sync_version_builder { SlidingSyncVersionBuilder::None => { diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index d46fbbbe523..afe19c12e35 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -117,6 +117,7 @@ uniffi::setup_scaffolding!(); /// The trust level in the sender's device that is required to decrypt an /// event. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum TrustRequirement { /// Decrypt events from everyone regardless of trust. Untrusted, @@ -129,6 +130,7 @@ pub enum TrustRequirement { /// Settings for decrypting messages #[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct DecryptionSettings { /// The trust level in the sender's device that is required to decrypt the /// event. If the sender's device is not sufficiently trusted, From 740356a3500422f934d861e88f55e69773d3771d Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 30 Sep 2024 11:35:46 +0200 Subject: [PATCH 171/979] test: ClientBuilder test for decryption trust requirement --- crates/matrix-sdk/src/client/builder/mod.rs | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 01125de9187..4f17a458158 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -1033,6 +1033,31 @@ pub(crate) mod tests { assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native); } + #[async_test] + #[cfg(feature = "e2e-encryption")] + async fn test_setup_decryption_trust_requirements() { + let homeserver = make_mock_homeserver().await; + let builder = ClientBuilder::new() + .server_name_or_homeserver_url(homeserver.uri()) + .with_decryption_trust_requirement(TrustRequirement::CrossSigned); + + let client = builder.build().await.unwrap(); + assert_matches!( + client.base_client().decryption_trust_requirement, + TrustRequirement::CrossSigned + ); + + let builder = ClientBuilder::new() + .server_name_or_homeserver_url(homeserver.uri()) + .with_decryption_trust_requirement(TrustRequirement::Untrusted); + + let client = builder.build().await.unwrap(); + assert_matches!( + client.base_client().decryption_trust_requirement, + TrustRequirement::Untrusted + ); + } + /* Helper functions */ async fn make_mock_homeserver() -> MockServer { From 60319914e1669cd7c425ef4fb9ae3b6951da46ae Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Oct 2024 09:50:42 +0200 Subject: [PATCH 172/979] code review | quick doc and test cleaning --- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 1 + crates/matrix-sdk/src/client/builder/mod.rs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 9e978fbb4f6..52bdee8f548 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -861,6 +861,7 @@ impl OlmMachine { /// * `strict_shields` - If `true`, messages will be decorated with strict /// warnings (use `false` to match legacy behaviour where unsafe keys have /// lower severity warnings and unverified identities are not decorated). + /// * `decryption_settings` - The setting for decrypting messages. pub fn decrypt_room_event( &self, event: String, diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 4f17a458158..6c22d148019 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -1035,7 +1035,7 @@ pub(crate) mod tests { #[async_test] #[cfg(feature = "e2e-encryption")] - async fn test_setup_decryption_trust_requirements() { + async fn test_set_up_decryption_trust_requirement_cross_signed() { let homeserver = make_mock_homeserver().await; let builder = ClientBuilder::new() .server_name_or_homeserver_url(homeserver.uri()) @@ -1046,6 +1046,12 @@ pub(crate) mod tests { client.base_client().decryption_trust_requirement, TrustRequirement::CrossSigned ); + } + + #[async_test] + #[cfg(feature = "e2e-encryption")] + async fn test_set_up_decryption_trust_requirement_untrusted() { + let homeserver = make_mock_homeserver().await; let builder = ClientBuilder::new() .server_name_or_homeserver_url(homeserver.uri()) From f7d99cc506d5704083ea56e1bf947b836e9ed4e7 Mon Sep 17 00:00:00 2001 From: Pratik Deshpande Date: Tue, 1 Oct 2024 10:51:45 +0530 Subject: [PATCH 173/979] Added a binding for custom login using JWT --- bindings/matrix-sdk-ffi/src/client.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 2414d32c44c..d5f0f07024c 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -60,7 +60,7 @@ use ruma::{ OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName, }; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{json, Value}; use tokio::sync::broadcast::error::RecvError; use tracing::{debug, error}; use url::Url; @@ -295,6 +295,31 @@ impl Client { Ok(()) } + /// Login using JWT + /// This is an implementation of the custom_login https://docs.rs/matrix-sdk/latest/matrix_sdk/matrix_auth/struct.MatrixAuth.html#method.login_custom + /// For more information on logging in with JWT: https://element-hq.github.io/synapse/latest/jwt.html + pub async fn custom_login_with_jwt( + &self, + jwt: String, + initial_device_name: Option, + device_id: Option, + ) -> Result<(), ClientError> { + let data = json!({ "token": jwt }).as_object().unwrap().clone(); + + let mut builder = self.inner.matrix_auth().login_custom("org.matrix.login.jwt", data)?; + + if let Some(initial_device_name) = initial_device_name.as_ref() { + builder = builder.initial_device_display_name(initial_device_name); + } + + if let Some(device_id) = device_id.as_ref() { + builder = builder.device_id(device_id); + } + + builder.send().await?; + Ok(()) + } + /// Login using an email and password. pub async fn login_with_email( &self, From 2fc4aacdd0026d3e2de4ed93f314b563e6386337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 25 Sep 2024 15:26:55 +0200 Subject: [PATCH 174/979] feat: Prefer room keys with better SenderData when comparing duplicate room keys --- .../src/olm/group_sessions/inbound.rs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index f43e013e442..c42d7ba889d 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::{ + cmp::Ordering, fmt, ops::Deref, sync::{ @@ -375,8 +376,8 @@ impl InboundGroupSession { self.imported } - /// Check if the `InboundGroupSession` is better than the given other - /// `InboundGroupSession` + /// Check if the [`InboundGroupSession`] is better than the given other + /// [`InboundGroupSession`] pub async fn compare(&self, other: &InboundGroupSession) -> SessionOrdering { // If this is the same object the ordering is the same, we can't compare because // we would deadlock while trying to acquire the same lock twice. @@ -389,8 +390,18 @@ impl InboundGroupSession { { SessionOrdering::Unconnected } else { - let mut other = other.inner.lock().await; - self.inner.lock().await.compare(&mut other) + let mut other_inner = other.inner.lock().await; + + match self.inner.lock().await.compare(&mut other_inner) { + SessionOrdering::Equal => { + match self.sender_data.compare_trust_level(&other.sender_data) { + Ordering::Less => SessionOrdering::Worse, + Ordering::Equal => SessionOrdering::Equal, + Ordering::Greater => SessionOrdering::Better, + } + } + result => result, + } } } @@ -648,7 +659,7 @@ mod tests { }; use crate::{ - olm::{InboundGroupSession, SenderData}, + olm::{InboundGroupSession, KnownSenderData, SenderData}, types::EventEncryptionAlgorithm, Account, }; @@ -868,6 +879,29 @@ mod tests { assert_eq!(inbound.compare(©).await, SessionOrdering::Unconnected); } + #[async_test] + async fn test_session_comparison_sender_data() { + let alice = Account::with_device_id(alice_id(), alice_device_id()); + let room_id = room_id!("!test:localhost"); + + let (_, mut inbound) = alice.create_group_session_pair_with_defaults(room_id).await; + + let sender_data = SenderData::SenderVerified(KnownSenderData { + user_id: alice.user_id().into(), + device_id: Some(alice.device_id().into()), + master_key: alice.identity_keys().ed25519.into(), + }); + + let mut better = InboundGroupSession::from_pickle(inbound.pickle().await).unwrap(); + better.sender_data = sender_data.clone(); + + assert_eq!(inbound.compare(&better).await, SessionOrdering::Worse); + assert_eq!(better.compare(&inbound).await, SessionOrdering::Better); + + inbound.sender_data = sender_data; + assert_eq!(better.compare(&inbound).await, SessionOrdering::Equal); + } + fn create_session_key() -> SessionKey { SessionKey::from_base64( "\ From e5bd7602e44acb92d6299d7ea027b4a5fc9870a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 25 Sep 2024 15:54:32 +0200 Subject: [PATCH 175/979] refactor: Use a match arm when evaluating session comparison results --- crates/matrix-sdk-crypto/src/machine/mod.rs | 30 ++++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 81aadb2e572..f0b9c6c7cbe 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -857,9 +857,9 @@ impl OlmMachine { #[instrument( skip_all, - // This function is only ever called by add_room_key via - // handle_decrypted_to_device_event, so sender, sender_key, and algorithm are - // already recorded. + // This function is only ever called by add_room_key via + // handle_decrypted_to_device_event, so sender, sender_key, and algorithm are + // already recorded. fields(room_id = ? content.room_id, session_id) )] async fn handle_key( @@ -888,17 +888,21 @@ impl OlmMachine { session.sender_data = sender_data; - if self.store().compare_group_session(&session).await? == SessionOrdering::Better { - info!("Received a new megolm room key"); + match self.store().compare_group_session(&session).await? { + SessionOrdering::Better => { + info!("Received a new megolm room key"); - Ok(Some(session)) - } else { - warn!( - "Received a megolm room key that we already have a better version of, \ - discarding", - ); - - Ok(None) + Ok(Some(session)) + } + comparison_result => { + warn!( + ?comparison_result, + "Received a megolm room key that we already have a better version \ + of, discarding" + ); + + Ok(None) + } } } Err(e) => { From 5cc7730dd97e592d72617366a4f6e5aa2aa558b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 1 Oct 2024 13:02:01 +0200 Subject: [PATCH 176/979] chore: Configure dependabot to notify us about outdated github actions --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..d202a332d29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" From 0ff63d30089103fb637facc319dcdc7b859f176b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:23:45 +0000 Subject: [PATCH 177/979] chore(deps): bump crate-ci/typos from 1.20.10 to 1.25.0 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.25.0. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.20.10...v1.25.0) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4067e8e7fc2..a1d54054ad8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -289,7 +289,7 @@ jobs: uses: actions/checkout@v4 - name: Check the spelling of the files in our repo - uses: crate-ci/typos@v1.20.10 + uses: crate-ci/typos@v1.25.0 clippy: name: Run clippy From 06e9f01a4ad503f81a78f9117fad9a4a69dfce51 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 13:48:39 +0200 Subject: [PATCH 178/979] chore: fix new typos --- crates/matrix-sdk-crypto/src/olm/account.rs | 2 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 2 +- crates/matrix-sdk/src/event_cache/mod.rs | 2 +- examples/oidc_cli/src/main.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 7627ed6eba0..27fc53ab73d 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -1480,7 +1480,7 @@ impl Account { /// that we don't want the inner state to be shared. #[doc(hidden)] pub fn deep_clone(&self) -> Self { - // `vodozemac::Account` isn't really clonable, but... Don't tell anyone. + // `vodozemac::Account` isn't really cloneable, but... Don't tell anyone. Self::from_pickle(self.pickle()).unwrap() } } diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index d8b3920044f..d45b90acb4d 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -148,7 +148,7 @@ pub enum ReplyContent { /// messages. #[derive(Debug)] pub struct Timeline { - /// Clonable, inner fields of the `Timeline`, shared with some background + /// Cloneable, inner fields of the `Timeline`, shared with some background /// tasks. controller: TimelineController, diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index d3f1bcf2703..1de623c4b86 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -609,7 +609,7 @@ impl RoomEventCache { } } -/// The (non-clonable) details of the `RoomEventCache`. +/// The (non-cloneable) details of the `RoomEventCache`. struct RoomEventCacheInner { /// The room id for this room. room_id: OwnedRoomId, diff --git a/examples/oidc_cli/src/main.rs b/examples/oidc_cli/src/main.rs index f431a49686b..3724d3bf519 100644 --- a/examples/oidc_cli/src/main.rs +++ b/examples/oidc_cli/src/main.rs @@ -171,7 +171,7 @@ impl OidcCli { // The client registration data should be persisted separately than the user // session, to be reused for other sessions or user accounts with the same // issuer. - // Also, client metadata should be persisted as it might change dependending on + // Also, client metadata should be persisted as it might change depending on // the provider metadata. let client_credentials = Credentials { client_id }; From 59d3608c32c94b7bf210a9bee1ba2733d37b2cec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:08:00 +0000 Subject: [PATCH 179/979] chore(deps): bump actions/checkout from 2.0.0 to 4.2.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.0.0 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v4.2.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/audit.yml | 2 +- .github/workflows/benchmarks.yml | 2 +- .github/workflows/bindings_ci.yml | 10 +++++----- .github/workflows/ci.yml | 18 +++++++++--------- .github/workflows/coverage.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/fixup-block.yml | 2 +- .github/workflows/msrv.yaml | 2 +- .github/workflows/upload_coverage.yml | 2 +- .github/workflows/xtask.yml | 2 +- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 7dd74fe1968..a508a769765 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -7,7 +7,7 @@ jobs: audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.2.0 - uses: actions-rust-lang/audit@v1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 9f6f4e6e2a4..6135378adad 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install Rust uses: dtolnay/rust-toolchain@master diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 28a630b6c84..a8b1636ef1b 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install protoc uses: taiki-e/install-action@v2 @@ -69,10 +69,10 @@ jobs: steps: - name: Checkout Rust SDK - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Checkout Kotlin Rust Components project - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 with: repository: matrix-org/matrix-rust-components-kotlin path: rust-components-kotlin @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 # install protoc in case we end up rebuilding opentelemetry-proto - name: Install protoc @@ -191,7 +191,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 # install protoc in case we end up rebuilding opentelemetry-proto - name: Install protoc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1d54054ad8..6de235b408c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -81,7 +81,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -112,7 +112,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -158,7 +158,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install protoc uses: taiki-e/install-action@v2 @@ -220,7 +220,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -268,7 +268,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -286,7 +286,7 @@ jobs: steps: - name: Checkout Actions Repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Check the spelling of the files in our repo uses: crate-ci/typos@v1.25.0 @@ -298,7 +298,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install protoc uses: taiki-e/install-action@v2 @@ -365,7 +365,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8a9e24da0c2..5e1b198951b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -58,7 +58,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 with: ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index a9ebd1dcd4b..e9ddc2066ec 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Install protoc uses: taiki-e/install-action@v2 diff --git a/.github/workflows/fixup-block.yml b/.github/workflows/fixup-block.yml index b42aeb491e1..5e5a06547f7 100644 --- a/.github/workflows/fixup-block.yml +++ b/.github/workflows/fixup-block.yml @@ -7,6 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.0.0 + - uses: actions/checkout@v4.2.0 - name: Block Fixup Commit Merge uses: 13rac1/block-fixup-merge-action@v2.0.0 diff --git a/.github/workflows/msrv.yaml b/.github/workflows/msrv.yaml index 46d1485927b..53c45db644e 100644 --- a/.github/workflows/msrv.yaml +++ b/.github/workflows/msrv.yaml @@ -16,6 +16,6 @@ jobs: msrv: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.2.0 - uses: taiki-e/install-action@cargo-hack - run: cargo hack check --rust-version --workspace --all-targets --ignore-private diff --git a/.github/workflows/upload_coverage.yml b/.github/workflows/upload_coverage.yml index eda15459eed..057660dd702 100644 --- a/.github/workflows/upload_coverage.yml +++ b/.github/workflows/upload_coverage.yml @@ -58,7 +58,7 @@ jobs: echo "override_commit=$(> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 with: ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }} path: repo_root diff --git a/.github/workflows/xtask.yml b/.github/workflows/xtask.yml index f359c20a274..a5398d580a5 100644 --- a/.github/workflows/xtask.yml +++ b/.github/workflows/xtask.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 - name: Calculate cache key id: cachekey From 2283c28503a50ca4c23168a8a9f4eef3a414c959 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 15:40:18 +0200 Subject: [PATCH 180/979] base: tidy up sliding sync code around e2ee --- crates/matrix-sdk-base/src/client.rs | 18 ++++---------- .../matrix-sdk-base/src/sliding_sync/mod.rs | 24 +++++++++---------- crates/matrix-sdk/src/sliding_sync/client.rs | 16 +++++++++---- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 8be5a1f1ca0..648d83cf4e4 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -789,17 +789,8 @@ impl BaseClient { pub(crate) async fn preprocess_to_device_events( &self, encryption_sync_changes: EncryptionSyncChanges<'_>, - #[cfg(feature = "experimental-sliding-sync")] changes: &mut StateChanges, - #[cfg(not(feature = "experimental-sliding-sync"))] _changes: &mut StateChanges, - #[cfg(feature = "experimental-sliding-sync")] room_info_notable_updates: &mut BTreeMap< - OwnedRoomId, - RoomInfoNotableUpdateReasons, - >, - #[cfg(not(feature = "experimental-sliding-sync"))] - _room_info_notable_updates: &mut BTreeMap< - OwnedRoomId, - RoomInfoNotableUpdateReasons, - >, + changes: &mut StateChanges, + room_info_notable_updates: &mut BTreeMap, ) -> Result>> { if let Some(o) = self.olm_machine().await.as_ref() { // Let the crypto machine handle the sync response, this @@ -815,8 +806,9 @@ impl BaseClient { self.decrypt_latest_events(&room, changes, room_info_notable_updates).await; } } - #[cfg(not(feature = "experimental-sliding-sync"))] - drop(room_key_updates); // Silence unused variable warning + + #[cfg(not(feature = "experimental-sliding-sync"))] // Silence unused variable warnings. + let _ = (room_key_updates, changes, room_info_notable_updates); Ok(events) } else { diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 5aa0993b09d..a5b774bb1d5 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -23,6 +23,8 @@ use std::{borrow::Cow, collections::BTreeMap}; #[cfg(feature = "e2e-encryption")] use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; #[cfg(feature = "e2e-encryption")] +use ruma::api::client::sync::sync_events::v5; +#[cfg(feature = "e2e-encryption")] use ruma::events::AnyToDeviceEvent; use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom}, @@ -59,16 +61,17 @@ impl BaseClient { /// In addition to writes to the crypto store, this may also write into the /// state store, in particular it may write latest-events to the state /// store. + /// + /// Returns whether any change happened. pub async fn process_sliding_sync_e2ee( &self, - extensions: &http::response::Extensions, - ) -> Result>> { - if extensions.is_empty() { - return Ok(Default::default()); + to_device: Option<&v5::response::ToDevice>, + e2ee: &v5::response::E2EE, + ) -> Result>>> { + if to_device.is_none() && e2ee.is_empty() { + return Ok(None); } - let http::response::Extensions { to_device, e2ee, .. } = extensions; - let to_device_events = to_device.as_ref().map(|to_device| to_device.events.clone()).unwrap_or_default(); @@ -87,9 +90,6 @@ impl BaseClient { // 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. - // Passing in the default empty maps and vecs for this is completely fine, since - // the `OlmMachine` assumes empty maps/vecs mean no change in the one-time key - // counts. let to_device = self .preprocess_to_device_events( matrix_sdk_crypto::EncryptionSyncChanges { @@ -106,12 +106,12 @@ impl BaseClient { ) .await?; - trace!("ready to submit changes to store"); + trace!("ready to submit e2ee changes to store"); self.store.save_changes(&changes).await?; self.apply_changes(&changes, room_info_notable_updates); - trace!("applied changes"); + trace!("applied e2ee changes"); - Ok(to_device) + Ok(Some(to_device)) } /// Process a response from a sliding sync call. diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index 5310ccc0277..eee13306665 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -289,11 +289,19 @@ impl<'a> SlidingSyncResponseProcessor<'a> { // `handle_room_response` before this function), so panic is fine. assert!(self.response.is_none()); - self.to_device_events = - self.client.base_client().process_sliding_sync_e2ee(extensions).await?; + self.to_device_events = if let Some(to_device_events) = self + .client + .base_client() + .process_sliding_sync_e2ee(extensions.to_device.as_ref(), &extensions.e2ee) + .await? + { + // Some new keys might have been received, so trigger a backup if needed. + self.client.encryption().backups().maybe_trigger_backup(); - // Some new keys might have been received, so trigger a backup if needed. - self.client.encryption().backups().maybe_trigger_backup(); + to_device_events + } else { + Vec::new() + }; Ok(()) } From 8f4fcf62999b598dccfe5ff4ab2c2ae5a677f039 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 15:42:11 +0200 Subject: [PATCH 181/979] base: experiment with handling global account data as a separate processor --- crates/matrix-sdk-base/src/client.rs | 33 +---------- crates/matrix-sdk-base/src/lib.rs | 1 + .../src/response_processors.rs | 59 +++++++++++++++++++ .../matrix-sdk-base/src/sliding_sync/mod.rs | 5 +- 4 files changed, 65 insertions(+), 33 deletions(-) create mode 100644 crates/matrix-sdk-base/src/response_processors.rs diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 648d83cf4e4..037abf1bccd 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -72,6 +72,7 @@ use crate::{ deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent}, error::{Error, Result}, event_cache_store::DynEventCacheStore, + response_processors::AccountDataProcessor, rooms::{ normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons}, Room, RoomInfo, RoomState, @@ -688,36 +689,6 @@ impl BaseClient { } } - /// Parses and stores any raw global account data events into the - /// [`StateChanges`]. - /// - /// Returns a list with the parsed account data events. - #[instrument(skip_all)] - pub(crate) async fn handle_account_data( - &self, - events: &[Raw], - changes: &mut StateChanges, - ) -> Vec { - let mut account_data = BTreeMap::new(); - let mut parsed_events = Vec::new(); - - for raw_event in events { - let event = match raw_event.deserialize() { - Ok(e) => e, - Err(e) => { - let event_type: Option = raw_event.get_field("type").ok().flatten(); - warn!(event_type, "Failed to deserialize a global account data event: {e}"); - continue; - } - }; - account_data.insert(event.event_type(), raw_event.clone()); - parsed_events.push(event); - } - - changes.account_data = account_data; - parsed_events - } - /// Processes the direct rooms in a sync response: /// /// Given a [`StateChanges`] instance, processes any direct room info @@ -983,7 +954,7 @@ impl BaseClient { let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); let global_account_data_events = - self.handle_account_data(&response.account_data.events, &mut changes).await; + ProcessAccountData::process(&response.account_data.events).apply(&mut changes); let push_rules = self.get_push_rules(&changes).await?; diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index e657fdd090f..1b2fdbf6526 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -31,6 +31,7 @@ pub mod event_cache_store; pub mod latest_event; pub mod media; pub mod notification_settings; +mod response_processors; mod rooms; pub mod read_receipts; diff --git a/crates/matrix-sdk-base/src/response_processors.rs b/crates/matrix-sdk-base/src/response_processors.rs new file mode 100644 index 00000000000..25a8b253a22 --- /dev/null +++ b/crates/matrix-sdk-base/src/response_processors.rs @@ -0,0 +1,59 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::BTreeMap, mem}; + +use ruma::{ + events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType}, + serde::Raw, +}; +use tracing::warn; + +use crate::StateChanges; + +#[must_use] +pub(crate) struct AccountDataProcessor { + parsed_events: Vec, + raw_by_type: BTreeMap>, +} + +impl AccountDataProcessor { + /// Creates a new processor for global account data. + pub fn process(events: &[Raw]) -> Self { + let mut raw_by_type = BTreeMap::new(); + let mut parsed_events = Vec::new(); + + for raw_event in events { + let event = match raw_event.deserialize() { + Ok(e) => e, + Err(e) => { + let event_type: Option = raw_event.get_field("type").ok().flatten(); + warn!(event_type, "Failed to deserialize a global account data event: {e}"); + continue; + } + }; + + raw_by_type.insert(event.event_type(), raw_event.clone()); + parsed_events.push(event); + } + + Self { raw_by_type, parsed_events } + } + + /// Applies the processed data to the state changes. + pub fn apply(mut self, changes: &mut StateChanges) -> Vec { + mem::swap(&mut changes.account_data, &mut self.raw_by_type); + self.parsed_events + } +} diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index a5b774bb1d5..680e0b37e47 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -45,6 +45,7 @@ use crate::RoomMemberships; use crate::{ error::Result, read_receipts::{compute_unread_counts, PreviousEventsProvider}, + response_processors::AccountDataProcessor, rooms::{ normal::{RoomHero, RoomInfoNotableUpdateReasons}, RoomState, @@ -146,7 +147,7 @@ impl BaseClient { trace!( rooms = rooms.len(), lists = lists.len(), - extensions = !extensions.is_empty(), + has_extensions = !extensions.is_empty(), "Processing sliding sync room events" ); @@ -165,7 +166,7 @@ impl BaseClient { let account_data = &extensions.account_data; let global_account_data_events = if !account_data.is_empty() { - self.handle_account_data(&account_data.global, &mut changes).await + ProcessAccountData::process(&account_data.global).apply(&mut changes) } else { Vec::new() }; From 40e6e9f028de33a8ee953b5b7b3d9d00c76aa6ce Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 16:34:27 +0200 Subject: [PATCH 182/979] base: avoid double-contains check in `apply_changes` Calling `contains_key` and then doing `if let Some() = .get` have the same effect. --- crates/matrix-sdk-base/src/client.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 037abf1bccd..0fa57e8d92a 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1233,20 +1233,17 @@ impl BaseClient { changes: &StateChanges, room_info_notable_updates: BTreeMap, ) { - if changes.account_data.contains_key(&GlobalAccountDataEventType::IgnoredUserList) { - if let Some(event) = - changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList) - { - match event.deserialize_as::() { - Ok(event) => { - let user_ids: Vec = - event.content.ignored_users.keys().map(|id| id.to_string()).collect(); - - self.ignore_user_list_changes.set(user_ids); - } - Err(error) => { - warn!("Failed to deserialize ignored user list event: {error}") - } + if let Some(event) = changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList) + { + match event.deserialize_as::() { + Ok(event) => { + let user_ids: Vec = + event.content.ignored_users.keys().map(|id| id.to_string()).collect(); + + self.ignore_user_list_changes.set(user_ids); + } + Err(error) => { + warn!("Failed to deserialize ignored user list event: {error}") } } } From 0a854cdbf713ebcb94b7558856842bd5aa48d335 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 16:40:05 +0200 Subject: [PATCH 183/979] base: get rid of `StateChanges::add_account_data` --- crates/matrix-sdk-base/src/store/integration_tests.rs | 2 +- crates/matrix-sdk-base/src/store/mod.rs | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 23dbddd70b7..7fd99b32a28 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -113,7 +113,7 @@ impl StateStoreIntegrationTests for DynStateStore { serde_json::from_value::>(pushrules_json.clone()) .unwrap(); let pushrules_event = pushrules_raw.deserialize().unwrap(); - changes.add_account_data(pushrules_event, pushrules_raw); + changes.account_data.insert(pushrules_event.event_type(), pushrules_raw); let mut room = RoomInfo::new(room_id, RoomState::Joined); room.mark_as_left(); diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 5e532396cc0..9e496aa11a1 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -360,15 +360,6 @@ impl StateChanges { self.room_infos.insert(room.room_id.clone(), room); } - /// Update the `StateChanges` struct with the given `AnyBasicEvent`. - pub fn add_account_data( - &mut self, - event: AnyGlobalAccountDataEvent, - raw_event: Raw, - ) { - self.account_data.insert(event.event_type(), raw_event); - } - /// Update the `StateChanges` struct with the given room with a new /// `AnyBasicEvent`. pub fn add_room_account_data( From 759a9b0e1853bd52c3520ff8130feb47ed37be22 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 16:50:57 +0200 Subject: [PATCH 184/979] base: make the dependency to push rules explicit when processing a room's subpart of a response --- crates/matrix-sdk-base/src/client.rs | 23 ++++++++++--------- .../src/response_processors.rs | 5 ++++ .../matrix-sdk-base/src/sliding_sync/mod.rs | 16 ++++++------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 0fa57e8d92a..cdebfdc9132 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -953,10 +953,9 @@ impl BaseClient { let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); - let global_account_data_events = - ProcessAccountData::process(&response.account_data.events).apply(&mut changes); + let account_data = AccountDataProcessor::process(&response.account_data.events); - let push_rules = self.get_push_rules(&changes).await?; + let push_rules = self.get_push_rules(&account_data).await?; let mut new_rooms = RoomUpdates::default(); let mut notifications = Default::default(); @@ -1173,6 +1172,7 @@ impl BaseClient { // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. + let global_account_data_events = account_data.apply(&mut changes); let has_new_direct_room_data = global_account_data_events .iter() .any(|event| event.event_type() == GlobalAccountDataEventType::Direct); @@ -1463,14 +1463,15 @@ impl BaseClient { /// Get the push rules. /// - /// Gets the push rules from `changes` if they have been updated, otherwise - /// get them from the store. As a fallback, uses - /// `Ruleset::server_default` if the user is logged in. - pub async fn get_push_rules(&self, changes: &StateChanges) -> Result { - if let Some(event) = changes - .account_data - .get(&GlobalAccountDataEventType::PushRules) - .and_then(|ev| ev.deserialize_as::().ok()) + /// Gets the push rules previously processed, otherwise get them from the + /// store. As a fallback, uses [`Ruleset::server_default`] if the user + /// is logged in. + pub(crate) async fn get_push_rules( + &self, + account_data: &AccountDataProcessor, + ) -> Result { + if let Some(event) = + account_data.push_rules().and_then(|ev| ev.deserialize_as::().ok()) { Ok(event.content.global) } else if let Some(event) = self diff --git a/crates/matrix-sdk-base/src/response_processors.rs b/crates/matrix-sdk-base/src/response_processors.rs index 25a8b253a22..40b9b0785bf 100644 --- a/crates/matrix-sdk-base/src/response_processors.rs +++ b/crates/matrix-sdk-base/src/response_processors.rs @@ -51,6 +51,11 @@ impl AccountDataProcessor { Self { raw_by_type, parsed_events } } + /// Returns the push rules found by this processor. + pub fn push_rules(&self) -> Option<&Raw> { + self.raw_by_type.get(&GlobalAccountDataEventType::PushRules) + } + /// Applies the processed data to the state changes. pub fn apply(mut self, changes: &mut StateChanges) -> Vec { mem::swap(&mut changes.account_data, &mut self.raw_by_type); diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 680e0b37e47..75435fc68ca 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -164,16 +164,11 @@ impl BaseClient { let store = self.store.clone(); let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone()); - let account_data = &extensions.account_data; - let global_account_data_events = if !account_data.is_empty() { - ProcessAccountData::process(&account_data.global).apply(&mut changes) - } else { - Vec::new() - }; + let account_data = AccountDataProcessor::process(&extensions.account_data.global); let mut new_rooms = RoomUpdates::default(); let mut notifications = Default::default(); - let mut rooms_account_data = account_data.rooms.clone(); + let mut rooms_account_data = extensions.account_data.rooms.clone(); for (room_id, response_room_data) in rooms { let (room_info, joined_room, left_room, invited_room) = self @@ -182,6 +177,7 @@ impl BaseClient { response_room_data, &mut rooms_account_data, &store, + &account_data, &mut changes, &mut room_info_notable_updates, &mut notifications, @@ -309,6 +305,7 @@ impl BaseClient { // because we want to have the push rules in place before we process // rooms and their events, but we want to create the rooms before we // process the `m.direct` account data event. + let global_account_data_events = account_data.apply(&mut changes); let has_new_direct_room_data = global_account_data_events .iter() .any(|event| event.event_type() == GlobalAccountDataEventType::Direct); @@ -354,7 +351,7 @@ impl BaseClient { notifications, // FIXME not yet supported by sliding sync. presence: Default::default(), - account_data: account_data.global.clone(), + account_data: extensions.account_data.global.clone(), to_device: Default::default(), }) } @@ -366,6 +363,7 @@ impl BaseClient { room_data: &http::response::Room, rooms_account_data: &mut BTreeMap>>, store: &Store, + account_data: &AccountDataProcessor, changes: &mut StateChanges, room_info_notable_updates: &mut BTreeMap, notifications: &mut BTreeMap>, @@ -451,7 +449,7 @@ impl BaseClient { Default::default() }; - let push_rules = self.get_push_rules(changes).await?; + let push_rules = self.get_push_rules(account_data).await?; if let Some(invite_state) = &room_data.invite_state { self.handle_invited_state( From 4f265ccd22dcb9bfa4ddfb2f72fb239188c84344 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 17:08:32 +0200 Subject: [PATCH 185/979] base: move processing of the direct room inside the `AccountDataProcessor` --- crates/matrix-sdk-base/src/client.rs | 96 +---------------- .../src/response_processors.rs | 101 +++++++++++++++++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 26 +---- 3 files changed, 103 insertions(+), 120 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index cdebfdc9132..6d9ea22d3b7 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -16,7 +16,7 @@ #[cfg(feature = "e2e-encryption")] use std::ops::Deref; use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + collections::{BTreeMap, BTreeSet}, fmt, iter, sync::Arc, }; @@ -49,10 +49,9 @@ use ruma::{ RoomPowerLevelsEvent, RoomPowerLevelsEventContent, StrippedRoomPowerLevelsEvent, }, }, - AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent, - AnySyncEphemeralRoomEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, - AnySyncTimelineEvent, GlobalAccountDataEventType, StateEvent, StateEventType, - SyncStateEvent, + AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, + AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, + GlobalAccountDataEventType, StateEvent, StateEventType, SyncStateEvent, }, push::{Action, PushConditionRoomCtx, Ruleset}, serde::Raw, @@ -689,72 +688,6 @@ impl BaseClient { } } - /// Processes the direct rooms in a sync response: - /// - /// Given a [`StateChanges`] instance, processes any direct room info - /// from the global account data and adds it to the room infos to - /// save. - #[instrument(skip_all)] - pub(crate) async fn process_direct_rooms( - &self, - events: &[AnyGlobalAccountDataEvent], - changes: &mut StateChanges, - ) { - for event in events { - let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue }; - let mut new_dms = HashMap::<&RoomId, HashSet>::new(); - for (user_id, rooms) in direct_event.content.iter() { - for room_id in rooms { - new_dms.entry(room_id).or_default().insert(user_id.clone()); - } - } - - let rooms = self.store.rooms(); - let mut old_dms = rooms - .iter() - .filter_map(|r| { - let direct_targets = r.direct_targets(); - (!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets)) - }) - .collect::>(); - - // Update the direct targets of rooms if they changed. - for (room_id, new_direct_targets) in new_dms { - if let Some(old_direct_targets) = old_dms.remove(&room_id) { - if old_direct_targets == new_direct_targets { - continue; - } - } - - trace!( - ?room_id, targets = ?new_direct_targets, - "Marking room as direct room" - ); - - if let Some(info) = changes.room_infos.get_mut(room_id) { - info.base_info.dm_targets = new_direct_targets; - } else if let Some(room) = self.store.room(room_id) { - let mut info = room.clone_info(); - info.base_info.dm_targets = new_direct_targets; - changes.add_room(info); - } - } - - // Remove the targets of old direct chats. - for room_id in old_dms.keys() { - trace!(?room_id, "Unmarking room as direct room"); - - if let Some(info) = changes.room_infos.get_mut(*room_id) { - info.base_info.dm_targets.clear(); - } else if let Some(room) = self.store.room(room_id) { - let mut info = room.clone_info(); - info.base_info.dm_targets.clear(); - changes.add_room(info); - } - } - } - } - #[cfg(feature = "e2e-encryption")] #[instrument(skip_all)] pub(crate) async fn preprocess_to_device_events( @@ -1168,26 +1101,7 @@ impl BaseClient { new_rooms.invite.insert(room_id, new_info); } - // We're processing direct state events here separately - // because we want to have the push rules in place before we process - // rooms and their events, but we want to create the rooms before we - // process the `m.direct` account data event. - let global_account_data_events = account_data.apply(&mut changes); - let has_new_direct_room_data = global_account_data_events - .iter() - .any(|event| event.event_type() == GlobalAccountDataEventType::Direct); - if has_new_direct_room_data { - self.process_direct_rooms(&global_account_data_events, &mut changes).await; - } else if let Ok(Some(direct_account_data)) = - self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await - { - debug!("Found direct room data in the Store, applying it"); - if let Ok(direct_account_data) = direct_account_data.deserialize() { - self.process_direct_rooms(&[direct_account_data], &mut changes).await; - } else { - warn!("Failed to deserialize direct room account data"); - } - } + account_data.apply(&mut changes, &self.store).await; changes.presence = response .presence diff --git a/crates/matrix-sdk-base/src/response_processors.rs b/crates/matrix-sdk-base/src/response_processors.rs index 40b9b0785bf..cc62ee385b9 100644 --- a/crates/matrix-sdk-base/src/response_processors.rs +++ b/crates/matrix-sdk-base/src/response_processors.rs @@ -12,15 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, mem}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + mem, +}; use ruma::{ events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType}, serde::Raw, + OwnedUserId, RoomId, }; -use tracing::warn; +use tracing::{debug, instrument, trace, warn}; -use crate::StateChanges; +use crate::{store::Store, StateChanges}; #[must_use] pub(crate) struct AccountDataProcessor { @@ -56,9 +60,96 @@ impl AccountDataProcessor { self.raw_by_type.get(&GlobalAccountDataEventType::PushRules) } + /// Processes the direct rooms in a sync response: + /// + /// Given a [`StateChanges`] instance, processes any direct room info + /// from the global account data and adds it to the room infos to + /// save. + #[instrument(skip_all)] + pub(crate) fn process_direct_rooms( + &self, + events: &[AnyGlobalAccountDataEvent], + store: &Store, + changes: &mut StateChanges, + ) { + for event in events { + let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue }; + + let mut new_dms = HashMap::<&RoomId, HashSet>::new(); + for (user_id, rooms) in direct_event.content.iter() { + for room_id in rooms { + new_dms.entry(room_id).or_default().insert(user_id.clone()); + } + } + + let rooms = store.rooms(); + let mut old_dms = rooms + .iter() + .filter_map(|r| { + let direct_targets = r.direct_targets(); + (!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets)) + }) + .collect::>(); + + // Update the direct targets of rooms if they changed. + for (room_id, new_direct_targets) in new_dms { + if let Some(old_direct_targets) = old_dms.remove(&room_id) { + if old_direct_targets == new_direct_targets { + continue; + } + } + + trace!( + ?room_id, targets = ?new_direct_targets, + "Marking room as direct room" + ); + + if let Some(info) = changes.room_infos.get_mut(room_id) { + info.base_info.dm_targets = new_direct_targets; + } else if let Some(room) = store.room(room_id) { + let mut info = room.clone_info(); + info.base_info.dm_targets = new_direct_targets; + changes.add_room(info); + } + } + + // Remove the targets of old direct chats. + for room_id in old_dms.keys() { + trace!(?room_id, "Unmarking room as direct room"); + + if let Some(info) = changes.room_infos.get_mut(*room_id) { + info.base_info.dm_targets.clear(); + } else if let Some(room) = store.room(room_id) { + let mut info = room.clone_info(); + info.base_info.dm_targets.clear(); + changes.add_room(info); + } + } + } + } + /// Applies the processed data to the state changes. - pub fn apply(mut self, changes: &mut StateChanges) -> Vec { + pub async fn apply(mut self, changes: &mut StateChanges, store: &Store) { + // Fill in the content of `changes.account_data`. mem::swap(&mut changes.account_data, &mut self.raw_by_type); - self.parsed_events + + // Process direct rooms. + let has_new_direct_room_data = self + .parsed_events + .iter() + .any(|event| event.event_type() == GlobalAccountDataEventType::Direct); + + if has_new_direct_room_data { + self.process_direct_rooms(&self.parsed_events, store, changes); + } else if let Ok(Some(direct_account_data)) = + store.get_account_data_event(GlobalAccountDataEventType::Direct).await + { + debug!("Found direct room data in the Store, applying it"); + if let Ok(direct_account_data) = direct_account_data.deserialize() { + self.process_direct_rooms(&[direct_account_data], store, changes); + } else { + warn!("Failed to deserialize direct room account data"); + } + } } } diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 75435fc68ca..6c5bd1aaa68 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -28,10 +28,7 @@ use ruma::api::client::sync::sync_events::v5; use ruma::events::AnyToDeviceEvent; use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom}, - events::{ - AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent, - GlobalAccountDataEventType, - }, + events::{AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent}, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, }; @@ -301,26 +298,7 @@ impl BaseClient { } } - // We're processing direct state events here separately - // because we want to have the push rules in place before we process - // rooms and their events, but we want to create the rooms before we - // process the `m.direct` account data event. - let global_account_data_events = account_data.apply(&mut changes); - let has_new_direct_room_data = global_account_data_events - .iter() - .any(|event| event.event_type() == GlobalAccountDataEventType::Direct); - if has_new_direct_room_data { - self.process_direct_rooms(&global_account_data_events, &mut changes).await; - } else if let Ok(Some(direct_account_data)) = - self.store.get_account_data_event(GlobalAccountDataEventType::Direct).await - { - debug!("Found direct room data in the Store, applying it"); - if let Ok(direct_account_data) = direct_account_data.deserialize() { - self.process_direct_rooms(&[direct_account_data], &mut changes).await; - } else { - warn!("Failed to deserialize direct room account data"); - } - } + account_data.apply(&mut changes, &store).await; // FIXME not yet supported by sliding sync. // changes.presence = presence From 96765cad28a965e07c00e273cc34d309400b676f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 17:18:33 +0200 Subject: [PATCH 186/979] base: add helper to process data on a room info from state changes or store --- .../src/response_processors.rs | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-base/src/response_processors.rs b/crates/matrix-sdk-base/src/response_processors.rs index cc62ee385b9..4fee7ffe87f 100644 --- a/crates/matrix-sdk-base/src/response_processors.rs +++ b/crates/matrix-sdk-base/src/response_processors.rs @@ -24,7 +24,26 @@ use ruma::{ }; use tracing::{debug, instrument, trace, warn}; -use crate::{store::Store, StateChanges}; +use crate::{store::Store, RoomInfo, StateChanges}; + +/// Applies a function to an existing `RoomInfo` if present in changes, or one +/// loaded from the database. +fn map_info( + room_id: &RoomId, + changes: &mut StateChanges, + store: &Store, + f: F, +) { + if let Some(info) = changes.room_infos.get_mut(room_id) { + f(info); + } else if let Some(room) = store.room(room_id) { + let mut info = room.clone_info(); + f(&mut info); + changes.add_room(info); + } else { + warn!(room = %room_id, "couldn't find room in state changes or store"); + } +} #[must_use] pub(crate) struct AccountDataProcessor { @@ -98,32 +117,18 @@ impl AccountDataProcessor { continue; } } - - trace!( - ?room_id, targets = ?new_direct_targets, - "Marking room as direct room" - ); - - if let Some(info) = changes.room_infos.get_mut(room_id) { - info.base_info.dm_targets = new_direct_targets; - } else if let Some(room) = store.room(room_id) { - let mut info = room.clone_info(); + trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room"); + map_info(room_id, changes, store, |info| { info.base_info.dm_targets = new_direct_targets; - changes.add_room(info); - } + }); } // Remove the targets of old direct chats. for room_id in old_dms.keys() { trace!(?room_id, "Unmarking room as direct room"); - - if let Some(info) = changes.room_infos.get_mut(*room_id) { + map_info(room_id, changes, store, |info| { info.base_info.dm_targets.clear(); - } else if let Some(room) = store.room(room_id) { - let mut info = room.clone_info(); - info.base_info.dm_targets.clear(); - changes.add_room(info); - } + }); } } } From 06f60e3b626b400d0701815889eacbe3e040ff60 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 11:38:06 +0200 Subject: [PATCH 187/979] base: rename account_data to account_data_processor and other review comments --- crates/matrix-sdk-base/src/client.rs | 17 +++++++++-------- crates/matrix-sdk-base/src/sliding_sync/mod.rs | 10 +++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 6d9ea22d3b7..519dd0b8568 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -61,7 +61,7 @@ use ruma::{ use tokio::sync::{broadcast, Mutex}; #[cfg(feature = "e2e-encryption")] use tokio::sync::{RwLock, RwLockReadGuard}; -use tracing::{debug, info, instrument, trace, warn}; +use tracing::{debug, error, info, instrument, trace, warn}; #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent}; @@ -886,9 +886,9 @@ impl BaseClient { let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); - let account_data = AccountDataProcessor::process(&response.account_data.events); + let account_data_processor = AccountDataProcessor::process(&response.account_data.events); - let push_rules = self.get_push_rules(&account_data).await?; + let push_rules = self.get_push_rules(&account_data_processor).await?; let mut new_rooms = RoomUpdates::default(); let mut notifications = Default::default(); @@ -1101,7 +1101,7 @@ impl BaseClient { new_rooms.invite.insert(room_id, new_info); } - account_data.apply(&mut changes, &self.store).await; + account_data_processor.apply(&mut changes, &self.store).await; changes.presence = response .presence @@ -1157,7 +1157,7 @@ impl BaseClient { self.ignore_user_list_changes.set(user_ids); } Err(error) => { - warn!("Failed to deserialize ignored user list event: {error}") + error!("Failed to deserialize ignored user list event: {error}") } } } @@ -1382,10 +1382,11 @@ impl BaseClient { /// is logged in. pub(crate) async fn get_push_rules( &self, - account_data: &AccountDataProcessor, + account_data_processor: &AccountDataProcessor, ) -> Result { - if let Some(event) = - account_data.push_rules().and_then(|ev| ev.deserialize_as::().ok()) + if let Some(event) = account_data_processor + .push_rules() + .and_then(|ev| ev.deserialize_as::().ok()) { Ok(event.content.global) } else if let Some(event) = self diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 6c5bd1aaa68..2d88ceef713 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -161,7 +161,7 @@ impl BaseClient { let store = self.store.clone(); let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone()); - let account_data = AccountDataProcessor::process(&extensions.account_data.global); + let account_data_processor = AccountDataProcessor::process(&extensions.account_data.global); let mut new_rooms = RoomUpdates::default(); let mut notifications = Default::default(); @@ -174,7 +174,7 @@ impl BaseClient { response_room_data, &mut rooms_account_data, &store, - &account_data, + &account_data_processor, &mut changes, &mut room_info_notable_updates, &mut notifications, @@ -298,7 +298,7 @@ impl BaseClient { } } - account_data.apply(&mut changes, &store).await; + account_data_processor.apply(&mut changes, &store).await; // FIXME not yet supported by sliding sync. // changes.presence = presence @@ -341,7 +341,7 @@ impl BaseClient { room_data: &http::response::Room, rooms_account_data: &mut BTreeMap>>, store: &Store, - account_data: &AccountDataProcessor, + account_data_processor: &AccountDataProcessor, changes: &mut StateChanges, room_info_notable_updates: &mut BTreeMap, notifications: &mut BTreeMap>, @@ -427,7 +427,7 @@ impl BaseClient { Default::default() }; - let push_rules = self.get_push_rules(account_data).await?; + let push_rules = self.get_push_rules(account_data_processor).await?; if let Some(invite_state) = &room_data.invite_state { self.handle_invited_state( From c9fd5a0787bb2a53f4b8b91354b5528bbc6bb9a0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 3 Oct 2024 11:42:43 +0100 Subject: [PATCH 188/979] Do not log full keys query response which can be very large (#4065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Erik Johnston Co-authored-by: Damir Jelić --- crates/matrix-sdk/src/encryption/recovery/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/encryption/recovery/mod.rs b/crates/matrix-sdk/src/encryption/recovery/mod.rs index 0d051216c63..a8ee851575b 100644 --- a/crates/matrix-sdk/src/encryption/recovery/mod.rs +++ b/crates/matrix-sdk/src/encryption/recovery/mod.rs @@ -629,7 +629,7 @@ impl Recovery { } } - #[instrument] + #[instrument(skip_all)] pub(crate) async fn update_state_after_keys_query(&self, response: &get_keys::v3::Response) { if let Some(user_id) = self.client.user_id() { if response.master_keys.contains_key(user_id) { From e85e50b185c8ce5acf02dfd95accf969933bd501 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 20 Sep 2024 16:02:11 +0100 Subject: [PATCH 189/979] crypto: Provide DerefMut on OwnUserIdentity and UserIdentity --- crates/matrix-sdk-crypto/src/identities/user.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 6a2e0b06faf..44d68c3fb67 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -14,7 +14,7 @@ use std::{ collections::HashMap, - ops::Deref, + ops::{Deref, DerefMut}, sync::{ atomic::{AtomicBool, Ordering}, Arc, RwLock, @@ -169,6 +169,12 @@ impl Deref for OwnUserIdentity { } } +impl DerefMut for OwnUserIdentity { + fn deref_mut(&mut self) -> &mut ::Target { + &mut self.inner + } +} + impl OwnUserIdentity { /// Mark our user identity as verified. /// @@ -282,6 +288,12 @@ impl Deref for OtherUserIdentity { } } +impl DerefMut for OtherUserIdentity { + fn deref_mut(&mut self) -> &mut ::Target { + &mut self.inner + } +} + impl OtherUserIdentity { /// Is this user identity verified. pub fn is_verified(&self) -> bool { From 6b357de947b85278e7e2ce092e42140481595eae Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 20 Sep 2024 16:03:40 +0100 Subject: [PATCH 190/979] crypto: Allow accessing the underlying identity on a UserIdentity --- crates/matrix-sdk/src/encryption/identities/users.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index aa869d483ba..8663b78e2ce 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -108,6 +108,10 @@ impl UserIdentity { Self { inner: identity, client } } + pub(crate) fn underlying_identity(&self) -> CryptoUserIdentities { + self.inner.clone() + } + /// The ID of the user this identity belongs to. /// /// # Examples From 9b36a04bb9dc587f213815f6dbeb7ccc8b5af753 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 20 Sep 2024 16:09:00 +0100 Subject: [PATCH 191/979] crypto: Provide the core logic about how identities change in a room when changes occur --- .../matrix-sdk-crypto/src/identities/mod.rs | 1 + .../src/identities/room_identity_state.rs | 1082 +++++++++++++++++ .../matrix-sdk-crypto/src/identities/user.rs | 12 +- crates/matrix-sdk-crypto/src/lib.rs | 4 + .../src/room/identity_status_changes.rs | 652 ++++++++++ 5 files changed, 1750 insertions(+), 1 deletion(-) create mode 100644 crates/matrix-sdk-crypto/src/identities/room_identity_state.rs create mode 100644 crates/matrix-sdk/src/room/identity_status_changes.rs diff --git a/crates/matrix-sdk-crypto/src/identities/mod.rs b/crates/matrix-sdk-crypto/src/identities/mod.rs index fac4e8e02ac..1a46fb090d5 100644 --- a/crates/matrix-sdk-crypto/src/identities/mod.rs +++ b/crates/matrix-sdk-crypto/src/identities/mod.rs @@ -42,6 +42,7 @@ //! `/keys/query` API call. pub(crate) mod device; pub(crate) mod manager; +pub(crate) mod room_identity_state; pub(crate) mod user; use std::sync::{ diff --git a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs new file mode 100644 index 00000000000..b7f1185160f --- /dev/null +++ b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs @@ -0,0 +1,1082 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use async_trait::async_trait; +use ruma::{ + events::{ + room::member::{MembershipState, SyncRoomMemberEvent}, + SyncStateEvent, + }, + OwnedUserId, UserId, +}; + +use super::UserIdentity; +use crate::store::IdentityUpdates; + +/// Something that can answer questions about the membership of a room and the +/// identities of users. +/// +/// This is implemented by `matrix_sdk::Room` and is a trait here so we can +/// supply a mock when needed. +#[async_trait] +pub trait RoomIdentityProvider: core::fmt::Debug { + /// Is the user with the supplied ID a member of this room? + async fn is_member(&self, user_id: &UserId) -> bool; + + /// Return a list of the [`UserIdentity`] of all members of this room + async fn member_identities(&self) -> Vec; + + /// Return the [`UserIdentity`] of the user with the supplied ID (even if + /// they are not a member of this room) or None if this user does not + /// exist. + async fn user_identity(&self, user_id: &UserId) -> Option; +} + +/// The state of the identities in a given room - whether they are: +/// +/// * in pin violation (the identity changed after we accepted their identity), +/// * verified (we manually did the emoji dance), +/// * previously verified (we did the emoji dance and then their identity +/// changed), +/// * otherwise, they are pinned. +#[derive(Debug)] +pub struct RoomIdentityState { + room: R, + known_states: KnownStates, +} + +impl RoomIdentityState { + /// Create a new RoomIdentityState using the provided room to check whether + /// users are members. + pub async fn new(room: R) -> Self { + let known_states = KnownStates::from_identities(room.member_identities().await); + Self { room, known_states } + } + + /// Provide the current state of the room: a list of all the non-pinned + /// identities and their status. + pub fn current_state(&self) -> Vec { + self.known_states + .known_states + .iter() + .map(|(user_id, state)| IdentityStatusChange { + user_id: user_id.clone(), + changed_to: state.clone(), + }) + .collect() + } + + /// Deal with an incoming event - either someone's identity changed, or some + /// changes happened to a room's membership. + /// + /// Returns the changes (if any) to the list of valid/invalid identities in + /// the room. + pub async fn process_change(&mut self, item: RoomIdentityChange) -> Vec { + match item { + RoomIdentityChange::IdentityUpdates(identity_updates) => { + self.process_identity_changes(identity_updates).await + } + RoomIdentityChange::SyncRoomMemberEvent(sync_room_member_event) => { + self.process_membership_change(sync_room_member_event).await + } + } + } + + async fn process_identity_changes( + &mut self, + identity_updates: IdentityUpdates, + ) -> Vec { + let mut ret = vec![]; + + for user_identity in identity_updates.new.values().chain(identity_updates.changed.values()) + { + // Ignore updates to our own identity + let user_id = user_identity.user_id(); + if self.room.is_member(user_id).await { + let update = self.update_user_state(user_id, user_identity); + if let Some(identity_status_change) = update { + ret.push(identity_status_change); + } + } + } + + ret + } + + async fn process_membership_change( + &mut self, + sync_room_member_event: SyncRoomMemberEvent, + ) -> Vec { + // Ignore redacted events - memberships should come through as new events, not + // redactions. + if let SyncStateEvent::Original(event) = sync_room_member_event { + // Ignore invalid user IDs + let user_id: Result<&UserId, _> = event.state_key.as_str().try_into(); + if let Ok(user_id) = user_id { + // Ignore non-existent users + if let Some(user_identity) = self.room.user_identity(user_id).await { + match event.content.membership { + MembershipState::Join | MembershipState::Invite => { + if let Some(update) = self.update_user_state(user_id, &user_identity) { + return vec![update]; + } + } + MembershipState::Leave | MembershipState::Ban => { + let leaving_state = state_of(&user_identity); + if leaving_state == IdentityState::PinViolation { + // If a user with bad state leaves the room, set them to Pinned, + // which effectively removes them + return vec![self.set_state(user_id, IdentityState::Pinned)]; + } + } + MembershipState::Knock => { + // No need to do anything when someone is knocking + } + _ => {} + } + } + } + } + + // We didn't find a relevant update, so return an empty list + vec![] + } + + fn update_user_state( + &mut self, + user_id: &UserId, + user_identity: &UserIdentity, + ) -> Option { + if let UserIdentity::Other(_) = &user_identity { + // If the user's state has changed + let new_state = state_of(user_identity); + let old_state = self.known_states.get(user_id); + if new_state != old_state { + Some(self.set_state(user_identity.user_id(), new_state)) + } else { + // Nothing changed + None + } + } else { + // Ignore updates to our own identity + None + } + } + + fn set_state(&mut self, user_id: &UserId, new_state: IdentityState) -> IdentityStatusChange { + // Remember the new state of the user + self.known_states.set(user_id, &new_state); + + // And return the update + IdentityStatusChange { user_id: user_id.to_owned(), changed_to: new_state } + } +} + +fn state_of(user_identity: &UserIdentity) -> IdentityState { + if user_identity.is_verified() { + IdentityState::Verified + } else if user_identity.has_verification_violation() { + IdentityState::VerificationViolation + } else if let UserIdentity::Other(u) = user_identity { + if u.identity_needs_user_approval() { + IdentityState::PinViolation + } else { + IdentityState::Pinned + } + } else { + IdentityState::Pinned + } +} + +/// A change in the status of the identity of a member of the room. Returned by +/// [`RoomIdentityState::process_change`] to indicate that something changed in +/// this room and we should either show or hide a warning. +#[derive(Clone, Debug, PartialEq)] +pub struct IdentityStatusChange { + /// The user ID of the user whose identity status changed + pub user_id: OwnedUserId, + + /// The new state of the identity of the user + pub changed_to: IdentityState, +} + +/// The state of an identity - verified, pinned etc. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +pub enum IdentityState { + /// The user is verified with us + Verified, + + /// Either this is the first identity we have seen for this user, or the + /// user has acknowledged a change of identity explicitly e.g. by + /// clicking OK on a notification. + Pinned, + + /// The user's identity has changed since it was pinned. The user should be + /// notified about this and given the opportunity to acknowledge the + /// change, which will make the new identity pinned. + /// When the user acknowledges the change, the app should call + /// [`crate::OtherUserIdentity::pin_current_master_key`]. + PinViolation, + + /// The user's identity has changed, and before that it was verified. This + /// is a serious problem. The user can either verify again to make this + /// identity verified, or withdraw verification + /// [`UserIdentity::withdraw_verification`] to make it pinned. + VerificationViolation, +} + +/// The type of update that can be received by +/// [`RoomIdentityState::process_change`] - either a change of someone's +/// identity, or a change of room membership. +#[derive(Debug)] +pub enum RoomIdentityChange { + /// Someone's identity changed + IdentityUpdates(IdentityUpdates), + + /// Someone joined or left a room + SyncRoomMemberEvent(SyncRoomMemberEvent), +} + +/// What we know about the states of users in this room. +/// Only stores users who _not_ in the Pinned stated. +#[derive(Debug)] +struct KnownStates { + known_states: HashMap, +} + +impl KnownStates { + fn from_identities(member_identities: impl IntoIterator) -> Self { + let mut known_states = HashMap::new(); + for user_identity in member_identities { + let state = state_of(&user_identity); + if state != IdentityState::Pinned { + known_states.insert(user_identity.user_id().to_owned(), state); + } + } + Self { known_states } + } + + /// Return the known state of the supplied user, or IdentityState::Pinned if + /// we don't know. + fn get(&self, user_id: &UserId) -> IdentityState { + self.known_states.get(user_id).cloned().unwrap_or(IdentityState::Pinned) + } + + /// Set the supplied user's state to the state given. If identity_state is + /// IdentityState::Pinned, forget this user. + fn set(&mut self, user_id: &UserId, identity_state: &IdentityState) { + if let IdentityState::Pinned = identity_state { + self.known_states.remove(user_id); + } else { + self.known_states.insert(user_id.to_owned(), identity_state.clone()); + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use async_trait::async_trait; + use matrix_sdk_test::async_test; + use ruma::{ + device_id, + events::{ + room::member::{ + MembershipState, RoomMemberEventContent, RoomMemberUnsigned, SyncRoomMemberEvent, + }, + OriginalSyncStateEvent, + }, + owned_event_id, owned_user_id, user_id, MilliSecondsSinceUnixEpoch, OwnedUserId, UInt, + UserId, + }; + use tokio::sync::Mutex; + + use super::{IdentityState, RoomIdentityChange, RoomIdentityProvider, RoomIdentityState}; + use crate::{ + identities::user::testing::own_identity_wrapped, + olm::PrivateCrossSigningIdentity, + store::{IdentityUpdates, Store}, + Account, IdentityStatusChange, OtherUserIdentity, OtherUserIdentityData, OwnUserIdentity, + OwnUserIdentityData, UserIdentity, + }; + + #[async_test] + async fn test_unpinning_a_pinned_identity_in_the_room_notifies() { + // Given someone in the room is pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When their identity changes to unpinned + let updates = identity_change(user_id, IdentityState::PinViolation, false, false).await; + let update = state.process_change(updates).await; + + // Then we emit an update saying they became unpinned + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::PinViolation + }] + ); + } + + #[async_test] + async fn test_pinning_an_unpinned_identity_in_the_room_notifies() { + // Given someone in the room is unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When their identity changes to pinned + let updates = identity_change(user_id, IdentityState::Pinned, false, false).await; + let update = state.process_change(updates).await; + + // Then we emit an update saying they became pinned + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::Pinned + }] + ); + } + + #[async_test] + async fn test_unpinning_an_identity_not_in_the_room_does_nothing() { + // Given an empty room + let user_id = user_id!("@u:s.co"); + let room = FakeRoom::new(); + let mut state = RoomIdentityState::new(room).await; + + // When a new unpinned user identity appears but they are not in the room + let updates = identity_change(user_id, IdentityState::PinViolation, true, false).await; + let update = state.process_change(updates).await; + + // Then we emit no update + assert_eq!(update, vec![]); + } + + #[async_test] + async fn test_pinning_an_identity_not_in_the_room_does_nothing() { + // Given an empty room + let user_id = user_id!("@u:s.co"); + let room = FakeRoom::new(); + let mut state = RoomIdentityState::new(room).await; + + // When a new pinned user appears but is not in the room + let updates = identity_change(user_id, IdentityState::Pinned, true, false).await; + let update = state.process_change(updates).await; + + // Then we emit no update + assert_eq!(update, []); + } + + #[async_test] + async fn test_pinning_an_already_pinned_identity_in_the_room_does_nothing() { + // Given someone in the room is pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When we are told they are pinned + let updates = identity_change(user_id, IdentityState::Pinned, false, false).await; + let update = state.process_change(updates).await; + + // Then we emit no update + assert_eq!(update, []); + } + + #[async_test] + async fn test_unpinning_an_already_unpinned_identity_in_the_room_does_nothing() { + // Given someone in the room is unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When we are told they are unpinned + let updates = identity_change(user_id, IdentityState::PinViolation, false, false).await; + let update = state.process_change(updates).await; + + // Then we emit no update + assert_eq!(update, []); + } + + #[async_test] + async fn test_a_pinned_identity_joining_the_room_does_nothing() { + // Given an empty room and we know of a user who is pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(other_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When the pinned user joins the room + let updates = room_change(user_id, MembershipState::Join); + let update = state.process_change(updates).await; + + // Then we emit no update because they are pinned + assert_eq!(update, []); + } + + #[async_test] + async fn test_an_unpinned_identity_joining_the_room_notifies() { + // Given an empty room and we know of a user who is unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(other_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When the unpinned user joins the room + let updates = room_change(user_id, MembershipState::Join); + let update = state.process_change(updates).await; + + // Then we emit an update saying they became unpinned + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::PinViolation + }] + ); + } + + #[async_test] + async fn test_a_pinned_identity_invited_to_the_room_does_nothing() { + // Given an empty room and we know of a user who is pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(other_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When the pinned user is invited to the room + let updates = room_change(user_id, MembershipState::Invite); + let update = state.process_change(updates).await; + + // Then we emit no update because they are pinned + assert_eq!(update, []); + } + + #[async_test] + async fn test_an_unpinned_identity_invited_to_the_room_notifies() { + // Given an empty room and we know of a user who is unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(other_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When the unpinned user is invited to the room + let updates = room_change(user_id, MembershipState::Invite); + let update = state.process_change(updates).await; + + // Then we emit an update saying they became unpinned + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::PinViolation + }] + ); + } + + #[async_test] + async fn test_own_identity_becoming_unpinned_is_ignored() { + // Given I am pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(own_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When I become unpinned + let updates = identity_change(user_id, IdentityState::PinViolation, false, true).await; + let update = state.process_change(updates).await; + + // Then we do nothing because own identities are ignored + assert_eq!(update, vec![]); + } + + #[async_test] + async fn test_own_identity_becoming_pinned_is_ignored() { + // Given I am unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(own_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When I become unpinned + let updates = identity_change(user_id, IdentityState::Pinned, false, true).await; + let update = state.process_change(updates).await; + + // Then we do nothing because own identities are ignored + assert_eq!(update, vec![]); + } + + #[async_test] + async fn test_own_pinned_identity_joining_room_is_ignored() { + // Given an empty room and we know of a user who is pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(own_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When the pinned user joins the room + let updates = room_change(user_id, MembershipState::Join); + let update = state.process_change(updates).await; + + // Then we emit no update because this is our own identity + assert_eq!(update, []); + } + + #[async_test] + async fn test_own_unpinned_identity_joining_room_is_ignored() { + // Given an empty room and we know of a user who is unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(own_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When the unpinned user joins the room + let updates = room_change(user_id, MembershipState::Join); + let update = state.process_change(updates).await; + + // Then we emit no update because this is our own identity + assert_eq!(update, vec![]); + } + + #[async_test] + async fn test_a_pinned_identity_leaving_the_room_does_nothing() { + // Given a pinned user is in the room + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When the pinned user leaves the room + let updates = room_change(user_id, MembershipState::Leave); + let update = state.process_change(updates).await; + + // Then we emit no update because they are pinned + assert_eq!(update, []); + } + + #[async_test] + async fn test_an_unpinned_identity_leaving_the_room_notifies() { + // Given an unpinned user is in the room + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When the unpinned user leaves the room + let updates = room_change(user_id, MembershipState::Leave); + let update = state.process_change(updates).await; + + // Then we emit an update saying they became unpinned + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::Pinned + }] + ); + } + + #[async_test] + async fn test_a_pinned_identity_being_banned_does_nothing() { + // Given a pinned user is in the room + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When the pinned user is banned + let updates = room_change(user_id, MembershipState::Ban); + let update = state.process_change(updates).await; + + // Then we emit no update because they are pinned + assert_eq!(update, []); + } + + #[async_test] + async fn test_an_unpinned_identity_being_banned_notifies() { + // Given an unpinned user is in the room + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await; + + // When the unpinned user is banned + let updates = room_change(user_id, MembershipState::Ban); + let update = state.process_change(updates).await; + + // Then we emit an update saying they became unpinned + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::Pinned + }] + ); + } + + #[async_test] + async fn test_multiple_simultaneous_identity_updates_are_all_notified() { + // Given several people in the room with different states + let user1 = user_id!("@u1:s.co"); + let user2 = user_id!("@u2:s.co"); + let user3 = user_id!("@u3:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user1, IdentityState::Pinned).await); + room.member(other_user_identities(user2, IdentityState::PinViolation).await); + room.member(other_user_identities(user3, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When they all change state simultaneously + let updates = identity_changes(&[ + IdentityChangeSpec { + user_id: user1.to_owned(), + changed_to: IdentityState::PinViolation, + new: false, + own: false, + }, + IdentityChangeSpec { + user_id: user2.to_owned(), + changed_to: IdentityState::Pinned, + new: false, + own: false, + }, + IdentityChangeSpec { + user_id: user3.to_owned(), + changed_to: IdentityState::PinViolation, + new: false, + own: false, + }, + ]) + .await; + let update = state.process_change(updates).await; + + // Then we emit updates for each of them + assert_eq!( + update, + vec![ + IdentityStatusChange { + user_id: user1.to_owned(), + changed_to: IdentityState::PinViolation + }, + IdentityStatusChange { + user_id: user2.to_owned(), + changed_to: IdentityState::Pinned + }, + IdentityStatusChange { + user_id: user3.to_owned(), + changed_to: IdentityState::PinViolation + } + ] + ); + } + + #[async_test] + async fn test_multiple_changes_are_notified() { + // Given someone in the room is pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user_id, IdentityState::Pinned).await); + let mut state = RoomIdentityState::new(room).await; + + // When they change state multiple times + let update1 = state + .process_change( + identity_change(user_id, IdentityState::PinViolation, false, false).await, + ) + .await; + let update2 = state + .process_change( + identity_change(user_id, IdentityState::PinViolation, false, false).await, + ) + .await; + let update3 = state + .process_change(identity_change(user_id, IdentityState::Pinned, false, false).await) + .await; + let update4 = state + .process_change( + identity_change(user_id, IdentityState::PinViolation, false, false).await, + ) + .await; + + // Then we emit updates each time + assert_eq!( + update1, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::PinViolation + }] + ); + // (Except update2 where nothing changed) + assert_eq!(update2, vec![]); + assert_eq!( + update3, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::Pinned + }] + ); + assert_eq!( + update4, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::PinViolation + }] + ); + } + + #[async_test] + async fn test_current_state_of_all_pinned_room_is_empty() { + // Given everyone in the room is pinned + let user1 = user_id!("@u1:s.co"); + let user2 = user_id!("@u2:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user1, IdentityState::Pinned).await); + room.member(other_user_identities(user2, IdentityState::Pinned).await); + let state = RoomIdentityState::new(room).await; + assert!(state.current_state().is_empty()); + } + + #[async_test] + async fn test_current_state_contains_all_unpinned_users() { + // Given some people are unpinned + let user1 = user_id!("@u1:s.co"); + let user2 = user_id!("@u2:s.co"); + let user3 = user_id!("@u3:s.co"); + let user4 = user_id!("@u4:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identities(user1, IdentityState::Pinned).await); + room.member(other_user_identities(user2, IdentityState::PinViolation).await); + room.member(other_user_identities(user3, IdentityState::Pinned).await); + room.member(other_user_identities(user4, IdentityState::PinViolation).await); + let mut state = RoomIdentityState::new(room).await.current_state(); + state.sort_by_key(|change| change.user_id.to_owned()); + assert_eq!( + state, + vec![ + IdentityStatusChange { + user_id: owned_user_id!("@u2:s.co"), + changed_to: IdentityState::PinViolation + }, + IdentityStatusChange { + user_id: owned_user_id!("@u4:s.co"), + changed_to: IdentityState::PinViolation + } + ] + ); + } + + #[derive(Debug)] + struct FakeRoom { + members: Vec, + non_members: Vec, + } + + impl FakeRoom { + fn new() -> Self { + Self { members: Default::default(), non_members: Default::default() } + } + + fn member(&mut self, user_identity: UserIdentity) { + self.members.push(user_identity); + } + + fn non_member(&mut self, user_identity: UserIdentity) { + self.non_members.push(user_identity); + } + } + + #[async_trait] + impl RoomIdentityProvider for FakeRoom { + async fn is_member(&self, user_id: &UserId) -> bool { + self.members.iter().any(|u| u.user_id() == user_id) + } + + async fn member_identities(&self) -> Vec { + self.members.clone() + } + + async fn user_identity(&self, user_id: &UserId) -> Option { + self.non_members + .iter() + .chain(self.members.iter()) + .find(|u| u.user_id() == user_id) + .cloned() + } + } + + fn room_change(user_id: &UserId, new_state: MembershipState) -> RoomIdentityChange { + let event = SyncRoomMemberEvent::Original(OriginalSyncStateEvent { + content: RoomMemberEventContent::new(new_state), + event_id: owned_event_id!("$1"), + sender: owned_user_id!("@admin:b.c"), + origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), + unsigned: RoomMemberUnsigned::new(), + state_key: user_id.to_owned(), + }); + RoomIdentityChange::SyncRoomMemberEvent(event) + } + + async fn identity_change( + user_id: &UserId, + changed_to: IdentityState, + new: bool, + own: bool, + ) -> RoomIdentityChange { + identity_changes(&[IdentityChangeSpec { + user_id: user_id.to_owned(), + changed_to, + new, + own, + }]) + .await + } + + struct IdentityChangeSpec { + user_id: OwnedUserId, + changed_to: IdentityState, + new: bool, + own: bool, + } + + async fn identity_changes(changes: &[IdentityChangeSpec]) -> RoomIdentityChange { + let mut updates = IdentityUpdates::default(); + + for change in changes { + let user_identities = if change.own { + let user_identity = + own_user_identity(&change.user_id, change.changed_to.clone()).await; + UserIdentity::Own(user_identity) + } else { + let user_identity = + other_user_identity(&change.user_id, change.changed_to.clone()).await; + UserIdentity::Other(user_identity) + }; + + if change.new { + updates.new.insert(user_identities.user_id().to_owned(), user_identities); + } else { + updates.changed.insert(user_identities.user_id().to_owned(), user_identities); + } + } + RoomIdentityChange::IdentityUpdates(updates) + } + + /// Create an other `UserIdentity` + async fn other_user_identities( + user_id: &UserId, + identity_state: IdentityState, + ) -> UserIdentity { + UserIdentity::Other(other_user_identity(user_id, identity_state).await) + } + + /// Create an other `UserIdentity` for use in tests + async fn other_user_identity( + user_id: &UserId, + identity_state: IdentityState, + ) -> OtherUserIdentity { + use std::sync::Arc; + + use ruma::owned_device_id; + use tokio::sync::Mutex; + + use crate::{ + olm::PrivateCrossSigningIdentity, + store::{CryptoStoreWrapper, MemoryStore}, + verification::VerificationMachine, + Account, + }; + + let device_id = owned_device_id!("DEV123"); + let account = Account::with_device_id(user_id, &device_id); + + let private_identity = + Arc::new(Mutex::new(PrivateCrossSigningIdentity::with_account(&account).await.0)); + + let other_user_identity_data = + OtherUserIdentityData::from_private(&*private_identity.lock().await).await; + + let mut user_identity = OtherUserIdentity { + inner: other_user_identity_data, + own_identity: None, + verification_machine: VerificationMachine::new( + account.clone(), + Arc::new(Mutex::new(PrivateCrossSigningIdentity::new( + account.user_id().to_owned(), + ))), + Arc::new(CryptoStoreWrapper::new( + account.user_id(), + account.device_id(), + MemoryStore::new(), + )), + ), + }; + + match identity_state { + IdentityState::Verified => { + // TODO + assert!(user_identity.is_verified()); + assert!(!user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + assert!(!user_identity.identity_needs_user_approval()); + } + IdentityState::Pinned => { + // Pinned is the default state + assert!(!user_identity.is_verified()); + assert!(!user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + assert!(!user_identity.identity_needs_user_approval()); + } + IdentityState::PinViolation => { + change_master_key(&mut user_identity, &account).await; + assert!(!user_identity.is_verified()); + assert!(!user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + assert!(user_identity.identity_needs_user_approval()); + } + IdentityState::VerificationViolation => { + // TODO + assert!(!user_identity.is_verified()); + assert!(user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + assert!(user_identity.identity_needs_user_approval()); + } + } + + user_identity + } + + /// Create an own `UserIdentity` + async fn own_user_identities(user_id: &UserId, identity_state: IdentityState) -> UserIdentity { + UserIdentity::Own(own_user_identity(user_id, identity_state).await) + } + + /// Create an own `UserIdentity` for use in tests + async fn own_user_identity(user_id: &UserId, identity_state: IdentityState) -> OwnUserIdentity { + use std::sync::Arc; + + use ruma::owned_device_id; + use tokio::sync::Mutex; + + use crate::{ + olm::PrivateCrossSigningIdentity, + store::{CryptoStoreWrapper, MemoryStore}, + verification::VerificationMachine, + Account, + }; + + let device_id = owned_device_id!("DEV123"); + let account = Account::with_device_id(user_id, &device_id); + + let private_identity = + Arc::new(Mutex::new(PrivateCrossSigningIdentity::with_account(&account).await.0)); + + let own_user_identity_data = + OwnUserIdentityData::from_private(&*private_identity.lock().await).await; + + let cross_signing_identity = PrivateCrossSigningIdentity::new(account.user_id().to_owned()); + let verification_machine = VerificationMachine::new( + account.clone(), + Arc::new(Mutex::new(cross_signing_identity.clone())), + Arc::new(CryptoStoreWrapper::new( + account.user_id(), + account.device_id(), + MemoryStore::new(), + )), + ); + + let mut user_identity = own_identity_wrapped( + own_user_identity_data, + verification_machine.clone(), + Store::new( + account.static_data().clone(), + Arc::new(Mutex::new(cross_signing_identity)), + Arc::new(CryptoStoreWrapper::new( + user_id!("@u:s.co"), + device_id!("DEV7"), + MemoryStore::new(), + )), + verification_machine, + ), + ); + + match identity_state { + IdentityState::Verified => { + // TODO + assert!(user_identity.is_verified()); + assert!(!user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + } + IdentityState::Pinned => { + // Pinned is the default state + assert!(!user_identity.is_verified()); + assert!(!user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + } + IdentityState::PinViolation => { + change_own_master_key(&mut user_identity, &account).await; + assert!(!user_identity.is_verified()); + assert!(!user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + } + IdentityState::VerificationViolation => { + // TODO + assert!(!user_identity.is_verified()); + assert!(user_identity.was_previously_verified()); + assert!(!user_identity.has_verification_violation()); + } + } + + user_identity + } + + async fn change_master_key(user_identity: &mut OtherUserIdentity, account: &Account) { + // Create a new master key and self signing key + let private_identity = + Arc::new(Mutex::new(PrivateCrossSigningIdentity::with_account(account).await.0)); + let data = OtherUserIdentityData::from_private(&*private_identity.lock().await).await; + + // And set them on the existing identity + user_identity + .update(data.master_key().clone(), data.self_signing_key().clone(), None) + .unwrap(); + } + + async fn change_own_master_key(user_identity: &mut OwnUserIdentity, account: &Account) { + // Create a new master key and self signing key + let private_identity = + Arc::new(Mutex::new(PrivateCrossSigningIdentity::with_account(account).await.0)); + let data = OwnUserIdentityData::from_private(&*private_identity.lock().await).await; + + // And set them on the existing identity + user_identity + .update( + data.master_key().clone(), + data.self_signing_key().clone(), + data.user_signing_key().clone(), + ) + .unwrap(); + } +} diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 44d68c3fb67..27fa450e811 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -1152,7 +1152,7 @@ where pub(crate) mod testing { use ruma::{api::client::keys::get_keys::v3::Response as KeyQueryResponse, user_id}; - use super::{OtherUserIdentityData, OwnUserIdentityData}; + use super::{OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData}; #[cfg(test)] use crate::{identities::manager::testing::other_user_id, olm::PrivateCrossSigningIdentity}; use crate::{ @@ -1160,7 +1160,9 @@ pub(crate) mod testing { manager::testing::{other_key_query, own_key_query}, DeviceData, }, + store::Store, types::CrossSigningKey, + verification::VerificationMachine, }; /// Generate test devices from KeyQueryResponse @@ -1197,6 +1199,14 @@ pub(crate) mod testing { own_identity(&own_key_query()) } + pub fn own_identity_wrapped( + inner: OwnUserIdentityData, + verification_machine: VerificationMachine, + store: Store, + ) -> OwnUserIdentity { + OwnUserIdentity { inner, verification_machine, store } + } + /// Generate default other "own" identity for tests #[cfg(test)] pub async fn get_other_own_identity() -> OwnUserIdentityData { diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index afe19c12e35..d136a2f6bdf 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -44,6 +44,10 @@ pub mod testing { use std::collections::{BTreeMap, BTreeSet}; +pub use identities::room_identity_state::{ + IdentityState, IdentityStatusChange, RoomIdentityChange, RoomIdentityProvider, + RoomIdentityState, +}; use ruma::OwnedRoomId; /// Return type for the room key importing. diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs new file mode 100644 index 00000000000..8185648f09c --- /dev/null +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -0,0 +1,652 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Facility to track changes to the identity of members of rooms. + +use std::collections::BTreeMap; + +use async_stream::stream; +use futures_core::Stream; +use futures_util::{stream_select, StreamExt}; +use matrix_sdk_base::crypto::{IdentityStatusChange, RoomIdentityChange, RoomIdentityState}; +use ruma::{events::room::member::SyncRoomMemberEvent, OwnedUserId, UserId}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +use super::Room; +use crate::{ + encryption::identities::{IdentityUpdates, UserIdentity}, + event_handler::EventHandlerDropGuard, + Client, Error, Result, +}; + +/// Support for creating a stream of batches of [`IdentityStatusChange`]. +/// +/// Internally, this subscribes to all identity changes, and to room events that +/// change the membership, and provides a stream of all changes to the identity +/// status of all room members. +/// +/// This struct does not represent the actual stream, but the state that is used +/// to produce the values of the stream. +/// +/// It does provide a method to create the stream: +/// [`IdentityStatusChanges::create_stream`]. +#[derive(Debug)] +pub struct IdentityStatusChanges { + /// Who is in the room and who is in identity violation at this moment + room_identity_state: RoomIdentityState, + + /// Dropped when we are dropped, and unregisters the event handler we + /// registered to listen for room events + _drop_guard: EventHandlerDropGuard, +} + +impl IdentityStatusChanges { + /// Create a new stream of changes to the identity status of members of a + /// room. + /// + /// The "status" of an identity changes when our level of trust in it + /// changes. + /// + /// For example, if an identity is "pinned" i.e. not manually verified, but + /// known, and it becomes a "unpinned" i.e. unknown, because the + /// encryption keys are different and the user has not acknowledged + /// this, then this constitues a status change. Also, if an identity is + /// "unpinned" and becomes "pinned", this is also a status change. + /// + /// The supplied stream is intended to provide enough information for a + /// client to display a list of room members whose identities have + /// changed, and allow the user to acknowledge this or act upon it. + /// + /// Note: when an unpinned user leaves a room, an update is generated + /// stating that they have become pinned, even though they may not + /// necessarily have become pinned, but we don't care any more because they + /// left the room. + pub async fn create_stream( + room: Room, + ) -> Result>> { + let identity_updates = wrap_identity_updates(&room.client).await?; + let (drop_guard, room_member_events) = wrap_room_member_events(&room); + let mut unprocessed_stream = combine_streams(identity_updates, room_member_events); + let own_user_id = room.client.user_id().ok_or(Error::InsufficientData)?.to_owned(); + + let mut state = IdentityStatusChanges { + room_identity_state: RoomIdentityState::new(room).await, + _drop_guard: drop_guard, + }; + + Ok(stream!({ + let current_state = + filter_non_self(state.room_identity_state.current_state(), &own_user_id); + if !current_state.is_empty() { + yield current_state; + } + while let Some(item) = unprocessed_stream.next().await { + let update = filter_non_self( + state.room_identity_state.process_change(item).await, + &own_user_id, + ); + if !update.is_empty() { + yield update; + } + } + })) + } +} + +fn filter_non_self( + mut input: Vec, + own_user_id: &UserId, +) -> Vec { + input.retain(|change| change.user_id != own_user_id); + input +} + +fn combine_streams( + identity_updates: impl Stream + Unpin, + room_member_events: impl Stream + Unpin, +) -> impl Stream { + stream_select!(identity_updates, room_member_events) +} + +async fn wrap_identity_updates(client: &Client) -> Result> { + Ok(client + .encryption() + .user_identities_stream() + .await? + .map(|item| RoomIdentityChange::IdentityUpdates(to_base_updates(item)))) +} + +fn to_base_updates(input: IdentityUpdates) -> matrix_sdk_base::crypto::store::IdentityUpdates { + matrix_sdk_base::crypto::store::IdentityUpdates { + new: to_base_identities(input.new), + changed: to_base_identities(input.changed), + unchanged: Default::default(), + } +} + +fn to_base_identities( + input: BTreeMap, +) -> BTreeMap { + input.into_iter().map(|(k, v)| (k, v.underlying_identity())).collect() +} + +fn wrap_room_member_events( + room: &Room, +) -> (EventHandlerDropGuard, impl Stream) { + let own_user_id = room.own_user_id().to_owned(); + let room_id = room.room_id(); + let (sender, receiver) = mpsc::channel(16); + let handle = + room.client.add_room_event_handler(room_id, move |event: SyncRoomMemberEvent| async move { + if *event.state_key() == own_user_id { + return; + } + let _: Result<_, _> = sender.send(RoomIdentityChange::SyncRoomMemberEvent(event)).await; + }); + let drop_guard = room.client.event_handler_drop_guard(handle); + (drop_guard, ReceiverStream::new(receiver)) +} + +#[cfg(test)] +mod tests { + use std::{ + pin::{pin, Pin}, + time::Duration, + }; + + use futures_core::Stream; + use futures_util::FutureExt; + use matrix_sdk_base::crypto::{IdentityState, IdentityStatusChange}; + use matrix_sdk_test::async_test; + use test_setup::TestSetup; + use tokio_stream::{StreamExt, Timeout}; + + #[async_test] + async fn test_when_user_becomes_unpinned_we_report_it() { + // Given a room containing us and Bob + let t = TestSetup::new_room_with_other_member().await; + + // And Bob's identity is pinned + t.pin().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + + // When Bob becomes unpinned + t.unpin().await; + + // Then we were notified about it + let change = next_change(&mut pin!(changes)).await; + assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].changed_to, IdentityState::PinViolation); + assert_eq!(change.len(), 1); + } + + #[async_test] + async fn test_when_user_becomes_pinned_we_report_it() { + // Given a room containing us and Bob + let t = TestSetup::new_room_with_other_member().await; + + // And Bob's identity is unpinned + t.unpin().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + let mut changes = pin!(changes); + + // When Bob becomes pinned + t.pin().await; + + // Then we were notified about the initial state of the room + let change1 = next_change(&mut changes).await; + assert_eq!(change1[0].user_id, t.user_id()); + assert_eq!(change1[0].changed_to, IdentityState::PinViolation); + assert_eq!(change1.len(), 1); + + // And the change when Bob became pinned + let change2 = next_change(&mut changes).await; + assert_eq!(change2[0].user_id, t.user_id()); + assert_eq!(change2[0].changed_to, IdentityState::Pinned); + assert_eq!(change2.len(), 1); + } + + #[async_test] + async fn test_when_an_unpinned_user_joins_we_report_it() { + // Given a room containing just us + let mut t = TestSetup::new_just_me_room().await; + + // And Bob's identity is unpinned + t.unpin().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + + // When Bob joins the room + t.join().await; + + // Then we were notified about it + let change = next_change(&mut pin!(changes)).await; + assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].changed_to, IdentityState::PinViolation); + assert_eq!(change.len(), 1); + } + + #[async_test] + async fn test_when_a_pinned_user_joins_we_do_not_report() { + // Given a room containing just us + let mut t = TestSetup::new_just_me_room().await; + + // And Bob's identity is unpinned + t.pin().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + let mut changes = pin!(changes); + + // When Bob joins the room + t.join().await; + + // Then there is no notification + tokio::time::sleep(Duration::from_millis(200)).await; + let change = changes.next().now_or_never(); + assert!(change.is_none()); + } + + #[async_test] + async fn test_when_an_unpinned_user_leaves_we_report_it() { + // Given a room containing us and Bob + let mut t = TestSetup::new_room_with_other_member().await; + + // And Bob's identity is unpinned + t.unpin().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + let mut changes = pin!(changes); + + // When Bob leaves the room + t.leave().await; + + // Then we were notified about the initial state of the room + let change1 = next_change(&mut changes).await; + assert_eq!(change1[0].user_id, t.user_id()); + assert_eq!(change1[0].changed_to, IdentityState::PinViolation); + assert_eq!(change1.len(), 1); + + // And we were notified about the change when the user left + let change2 = next_change(&mut changes).await; + // Note: the user left the room, but we see that as them "becoming pinned" i.e. + // "you no longer need to notify about this user". + assert_eq!(change2[0].user_id, t.user_id()); + assert_eq!(change2[0].changed_to, IdentityState::Pinned); + assert_eq!(change2.len(), 1); + } + + #[async_test] + async fn test_multiple_identity_changes_are_reported() { + // Given a room containing just us + let mut t = TestSetup::new_just_me_room().await; + + // And Bob's identity is unpinned + t.unpin().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + let mut changes = pin!(changes); + + // NOTE: below we pull the changes out of the subscription after each action. + // This makes sure that the identity changes and membership changes are + // properly ordered. If we pull them out later, the identity changes get + // shifted forward because they rely on less-complex async stuff under + // the hood. Calling next_change ends up winding the async + // machinery sufficiently that the membership change and any subsequent events + // have fully completed. + + // When Bob joins the room ... + t.join().await; + let change1 = next_change(&mut changes).await; + + // ... becomes pinned ... + t.pin().await; + let change2 = next_change(&mut changes).await; + + // ... leaves and joins again (ignored since they stay pinned) ... + t.leave().await; + t.join().await; + + // ... becomes unpinned ... + t.unpin().await; + let change3 = next_change(&mut changes).await; + + // ... and leaves. + t.leave().await; + let change4 = next_change(&mut changes).await; + + assert_eq!(change1[0].user_id, t.user_id()); + assert_eq!(change2[0].user_id, t.user_id()); + assert_eq!(change3[0].user_id, t.user_id()); + assert_eq!(change4[0].user_id, t.user_id()); + + assert_eq!(change1[0].changed_to, IdentityState::PinViolation); + assert_eq!(change2[0].changed_to, IdentityState::Pinned); + assert_eq!(change3[0].changed_to, IdentityState::PinViolation); + assert_eq!(change4[0].changed_to, IdentityState::Pinned); + + assert_eq!(change1.len(), 1); + assert_eq!(change2.len(), 1); + assert_eq!(change3.len(), 1); + assert_eq!(change4.len(), 1); + } + + // TODO: I (andyb) haven't figured out how to test room membership changes that + // affect our own user (they should not be shown). Specifically, I haven't + // figure out how to get out own user into a non-pinned state. + + async fn next_change( + changes: &mut Pin<&mut Timeout>>>, + ) -> Vec { + changes + .next() + .await + .expect("There should be an identity update") + .expect("Should not time out") + } + + mod test_setup { + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + use futures_core::Stream; + use matrix_sdk_base::{ + crypto::{IdentityStatusChange, OtherUserIdentity}, + RoomState, + }; + use matrix_sdk_test::{ + test_json::{self, keys_query_sets::IdentityChangeDataSet}, + JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, + }; + use ruma::{ + api::client::keys::get_keys, events::room::member::MembershipState, owned_user_id, + OwnedUserId, TransactionId, UserId, + }; + use serde_json::json; + use tokio_stream::{StreamExt as _, Timeout}; + use wiremock::{ + matchers::{header, method, path_regex}, + Mock, MockServer, ResponseTemplate, + }; + + use crate::{ + encryption::identities::UserIdentity, test_utils::logged_in_client, Client, Room, + }; + + /// Sets up a client and a room and allows changing user identities and + /// room memberships. Note: most methods e.g. [`TestSetup::user_id`] are + /// talking about the OTHER user, not our own user. Only methods + /// starting with `self_` are talking about this user. + pub(super) struct TestSetup { + client: Client, + user_id: OwnedUserId, + sync_response_builder: SyncResponseBuilder, + room: Room, + } + + impl TestSetup { + pub(super) async fn new_just_me_room() -> Self { + let (client, user_id, mut sync_response_builder) = Self::init().await; + let room = create_just_me_room(&client, &mut sync_response_builder).await; + Self { client, user_id, sync_response_builder, room } + } + + pub(super) async fn new_room_with_other_member() -> Self { + let (client, user_id, mut sync_response_builder) = Self::init().await; + let room = + create_room_with_other_member(&mut sync_response_builder, &client, &user_id) + .await; + Self { client, user_id, sync_response_builder, room } + } + + pub(super) fn user_id(&self) -> &UserId { + &self.user_id + } + + pub(super) async fn pin(&self) { + if self.user_identity().await.is_some() { + assert!( + !self.is_pinned().await, + "pin() called when the identity is already pinned!" + ); + + // Pin it + self.crypto_other_identity() + .await + .pin_current_master_key() + .await + .expect("Should not fail to pin"); + } else { + // There was no existing identity. Set one. It will be pinned by default. + self.change_identity(IdentityChangeDataSet::key_query_with_identity_a()).await; + } + + // Sanity check: we are pinned + assert!(self.is_pinned().await); + } + + pub(super) async fn unpin(&self) { + // Change/set their identity - this will unpin if they already had one. + // If this was the first time we'd done this, they are now pinned. + self.change_identity(IdentityChangeDataSet::key_query_with_identity_a()).await; + + if self.is_pinned().await { + // Change their identity. Now they are definitely unpinned + self.change_identity(IdentityChangeDataSet::key_query_with_identity_b()).await; + } + + // Sanity: we are unpinned + assert!(!self.is_pinned().await); + } + + pub(super) async fn join(&mut self) { + self.membership_change(MembershipState::Join).await; + } + + pub(super) async fn leave(&mut self) { + self.membership_change(MembershipState::Leave).await; + } + + pub(super) async fn subscribe_to_identity_status_changes( + &self, + ) -> Timeout>> { + self.room + .subscribe_to_identity_status_changes() + .await + .expect("Should be able to subscribe") + .timeout(Duration::from_secs(2)) + } + + async fn init() -> (Client, OwnedUserId, SyncResponseBuilder) { + let (client, _server) = create_client_and_server().await; + + // Note: if you change the user_id, you will need to change lots of hard-coded + // stuff inside IdentityChangeDataSet + let user_id = owned_user_id!("@bob:localhost"); + + let sync_response_builder = SyncResponseBuilder::default(); + + (client, user_id, sync_response_builder) + } + + async fn change_identity( + &self, + key_query_response: get_keys::v3::Response, + ) -> OtherUserIdentity { + self.client + .mark_request_as_sent(&TransactionId::new(), &key_query_response) + .await + .expect("Should not fail to send identity changes"); + + self.crypto_other_identity().await + } + + async fn membership_change(&mut self, new_state: MembershipState) { + let sync_response = self + .sync_response_builder + .add_joined_room(JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID).add_state_event( + StateTestEvent::Custom(sync_response_member( + &self.user_id, + new_state.clone(), + )), + )) + .build_sync_response(); + self.room.client.process_sync(sync_response).await.unwrap(); + + // Make sure the membership stuck as expected + let m = self + .room + .get_member_no_sync(&self.user_id) + .await + .expect("Should not fail to get member"); + + match (&new_state, m) { + (MembershipState::Leave, None) => {} + (_, None) => { + panic!("Member should exist") + } + (_, Some(m)) => { + assert_eq!(*m.membership(), new_state); + } + }; + } + + async fn is_pinned(&self) -> bool { + !self.crypto_other_identity().await.identity_needs_user_approval() + } + + async fn crypto_other_identity(&self) -> OtherUserIdentity { + self.user_identity() + .await + .expect("User identity should exist") + .underlying_identity() + .other() + .expect("Identity should be Other, not Own") + } + + async fn user_identity(&self) -> Option { + self.client + .encryption() + .get_user_identity(&self.user_id) + .await + .expect("Should not fail to get user identity") + } + } + + async fn create_just_me_room( + client: &Client, + sync_response_builder: &mut SyncResponseBuilder, + ) -> Room { + let create_room_sync_response = sync_response_builder + .add_joined_room( + JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID) + .add_state_event(StateTestEvent::Member), + ) + .build_sync_response(); + client.process_sync(create_room_sync_response).await.unwrap(); + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).expect("Room should exist"); + assert_eq!(room.state(), RoomState::Joined); + room + } + + async fn create_room_with_other_member( + builder: &mut SyncResponseBuilder, + client: &Client, + other_user_id: &UserId, + ) -> Room { + let create_room_sync_response = builder + .add_joined_room( + JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID) + .add_state_event(StateTestEvent::Member) + .add_state_event(StateTestEvent::Custom(sync_response_member( + other_user_id, + MembershipState::Join, + ))), + ) + .build_sync_response(); + client.process_sync(create_room_sync_response).await.unwrap(); + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).expect("Room should exist"); + assert_eq!(room.state(), RoomState::Joined); + assert_eq!( + *room + .get_member_no_sync(other_user_id) + .await + .expect("Should not fail to get member") + .expect("Member should exist") + .membership(), + MembershipState::Join + ); + room + } + + async fn create_client_and_server() -> (Client, MockServer) { + let server = MockServer::start().await; + mock_members_request(&server).await; + mock_secret_storage_default_key(&server).await; + let client = logged_in_client(Some(server.uri())).await; + (client, server) + } + + async fn mock_members_request(server: &MockServer) { + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/members")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200).set_body_json(&*test_json::members::MEMBERS), + ) + .mount(&server) + .await; + } + + async fn mock_secret_storage_default_key(server: &MockServer) { + Mock::given(method("GET")) + .and(path_regex( + r"^/_matrix/client/r0/user/.*/account_data/m.secret_storage.default_key", + )) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .mount(&server) + .await; + } + + fn sync_response_member( + user_id: &UserId, + membership: MembershipState, + ) -> serde_json::Value { + json!({ + "content": { + "membership": membership.to_string(), + }, + "event_id": format!( + "$aa{}bb:localhost", + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() % 100_000 + ), + "origin_server_ts": 1472735824, + "sender": "@example:localhost", + "state_key": user_id, + "type": "m.room.member", + "unsigned": { + "age": 1234 + } + }) + } + } +} From cd072e6dff8dda4be0bc3a942fa1b9ea074acfcf Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 20 Sep 2024 16:09:52 +0100 Subject: [PATCH 192/979] crypto: Provide a way to subscribe to identity status changes --- .../src/encryption/identities/users.rs | 1 + .../src/room/identity_status_changes.rs | 7 +- crates/matrix-sdk/src/room/mod.rs | 68 +++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index 8663b78e2ce..b2f989e46a8 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -108,6 +108,7 @@ impl UserIdentity { Self { inner: identity, client } } + #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] pub(crate) fn underlying_identity(&self) -> CryptoUserIdentities { self.inner.clone() } diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index 8185648f09c..ce93f326a46 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -13,6 +13,7 @@ // limitations under the License. //! Facility to track changes to the identity of members of rooms. +#![cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] use std::collections::BTreeMap; @@ -62,7 +63,7 @@ impl IdentityStatusChanges { /// For example, if an identity is "pinned" i.e. not manually verified, but /// known, and it becomes a "unpinned" i.e. unknown, because the /// encryption keys are different and the user has not acknowledged - /// this, then this constitues a status change. Also, if an identity is + /// this, then this constitutes a status change. Also, if an identity is /// "unpinned" and becomes "pinned", this is also a status change. /// /// The supplied stream is intended to provide enough information for a @@ -612,7 +613,7 @@ mod tests { .respond_with( ResponseTemplate::new(200).set_body_json(&*test_json::members::MEMBERS), ) - .mount(&server) + .mount(server) .await; } @@ -623,7 +624,7 @@ mod tests { )) .and(header("authorization", "Bearer 1234")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) - .mount(&server) + .mount(server) .await; } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 20ad04d6adb..4c5a83ced86 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -8,14 +8,20 @@ use std::{ time::Duration, }; +#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] +use async_trait::async_trait; use eyeball::SharedObservable; use futures_core::Stream; use futures_util::{ future::{try_join, try_join_all}, stream::FuturesUnordered, }; +#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] +pub use identity_status_changes::IdentityStatusChanges; #[cfg(feature = "e2e-encryption")] use matrix_sdk_base::crypto::DecryptionSettings; +#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] +use matrix_sdk_base::crypto::{IdentityStatusChange, RoomIdentityProvider, UserIdentity}; use matrix_sdk_base::{ deserialized_responses::{ RawAnySyncOrStrippedState, RawSyncOrStrippedState, SyncOrStrippedState, TimelineEvent, @@ -114,6 +120,7 @@ use crate::{ pub mod edit; pub mod futures; +pub mod identity_status_changes; mod member; mod messages; pub mod power_levels; @@ -367,6 +374,35 @@ impl Room { (drop_guard, receiver) } + /// Subscribe to updates about users who are in "pin violation" i.e. their + /// identity has changed and the user has not yet acknowledged this. + /// + /// The returned receiver will receive a new vector of + /// [`IdentityStatusChange`] each time a /keys/query response shows a + /// changed identity for a member of this room, or a sync shows a change + /// to the membership of an affected user. (Changes to the current user are + /// not directly included, but some changes to the current user's identity + /// can trigger changes to how we see other users' identities, which + /// will be included.) + /// + /// The first item in the stream provides the current state of the room: + /// each member of the room who is not in "pinned" state will be + /// included (except the current user). + /// + /// If the `changed_to` property of an [`IdentityStatusChange`] is set to + /// `PinViolation` then a warning should be displayed to the user. If it is + /// set to `Pinned` then no warning should be displayed. + /// + /// Note that if a user who is in pin violation leaves the room, a `Pinned` + /// update is sent, to indicate that the warning should be removed, even + /// though the user's identity is not necessarily pinned. + #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] + pub async fn subscribe_to_identity_status_changes( + &self, + ) -> Result>> { + IdentityStatusChanges::create_stream(self.clone()).await + } + /// Returns a wrapping `TimelineEvent` for the input `AnyTimelineEvent`, /// decrypted if needs be. /// @@ -2925,6 +2961,38 @@ impl Room { } } +#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] +#[async_trait] +impl RoomIdentityProvider for Room { + async fn is_member(&self, user_id: &UserId) -> bool { + self.get_member(user_id).await.unwrap_or(None).is_some() + } + + async fn member_identities(&self) -> Vec { + let members = self + .members(RoomMemberships::JOIN | RoomMemberships::INVITE) + .await + .unwrap_or_else(|_| Default::default()); + + let mut ret: Vec = Vec::new(); + for member in members { + if let Some(i) = self.user_identity(member.user_id()).await { + ret.push(i); + } + } + ret + } + + async fn user_identity(&self, user_id: &UserId) -> Option { + self.client + .encryption() + .get_user_identity(user_id) + .await + .unwrap_or(None) + .map(|u| u.underlying_identity()) + } +} + /// A wrapper for a weak client and a room id that allows to lazily retrieve a /// room, only when needed. #[derive(Clone)] From c5f5bc8496441f7b3c5661154f65b077df89acec Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 20 Sep 2024 17:37:43 +0100 Subject: [PATCH 193/979] crypto: FFI bindings for subscribe_to_identity_status_changes --- .../src/identity_status_change.rs | 24 +++++++++++++ bindings/matrix-sdk-ffi/src/lib.rs | 1 + bindings/matrix-sdk-ffi/src/room.rs | 34 ++++++++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 bindings/matrix-sdk-ffi/src/identity_status_change.rs diff --git a/bindings/matrix-sdk-ffi/src/identity_status_change.rs b/bindings/matrix-sdk-ffi/src/identity_status_change.rs new file mode 100644 index 00000000000..3fcb5a69142 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/identity_status_change.rs @@ -0,0 +1,24 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use matrix_sdk::crypto::IdentityState; + +#[derive(uniffi::Record)] +pub struct IdentityStatusChange { + /// The user ID of the user whose identity status changed + pub user_id: String, + + /// The new state of the identity of the user. + pub changed_to: IdentityState, +} diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 47ef7240d3d..801887b6fee 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -11,6 +11,7 @@ mod encryption; mod error; mod event; mod helpers; +mod identity_status_change; mod notification; mod notification_settings; mod platform; diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index c736007e381..8c21f7ca55f 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -1,6 +1,7 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, pin::pin, sync::Arc}; use anyhow::{Context, Result}; +use futures_util::StreamExt; use matrix_sdk::{ crypto::LocalTrust, event_cache::paginator::PaginatorError, @@ -35,6 +36,7 @@ use crate::{ chunk_iterator::ChunkIterator, error::{ClientError, MediaInfoError, RoomError}, event::{MessageLikeEventType, StateEventType}, + identity_status_change::IdentityStatusChange, room_info::RoomInfo, room_member::RoomMember, ruma::{ImageInfo, Mentions, NotifyType}, @@ -582,6 +584,31 @@ impl Room { }))) } + pub fn subscribe_to_identity_status_changes( + &self, + listener: Box, + ) -> Arc { + let room = self.inner.clone(); + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { + let status_changes = room.subscribe_to_identity_status_changes().await; + if let Ok(status_changes) = status_changes { + // TODO: what to do with failures? + let mut status_changes = pin!(status_changes); + while let Some(identity_status_changes) = status_changes.next().await { + listener.call( + identity_status_changes + .into_iter() + .map(|change| { + let user_id = change.user_id.to_string(); + IdentityStatusChange { user_id, changed_to: change.changed_to } + }) + .collect(), + ); + } + } + }))) + } + /// Set (or unset) a flag on the room to indicate that the user has /// explicitly marked it as unread. pub async fn set_unread_flag(&self, new_value: bool) -> Result<(), ClientError> { @@ -898,6 +925,11 @@ pub trait TypingNotificationsListener: Sync + Send { fn call(&self, typing_user_ids: Vec); } +#[uniffi::export(callback_interface)] +pub trait IdentityStatusChangeListener: Sync + Send { + fn call(&self, identity_status_change: Vec); +} + #[derive(uniffi::Object)] pub struct RoomMembersIterator { chunk_iterator: ChunkIterator, From a695e291fad12670f9c3fbcb4709118e1d40e2f6 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 3 Oct 2024 13:52:12 +0100 Subject: [PATCH 194/979] crypto: Rename PreviouslyVerified to VerificationViolation For consistency with other places, we have now settled on `VerificationViolation` as the best way to express this situation. --- .../src/deserialized_responses.rs | 17 ++++---- crates/matrix-sdk-crypto/CHANGELOG.md | 3 ++ .../src/identities/manager.rs | 12 +++--- .../matrix-sdk-crypto/src/identities/user.rs | 17 ++++---- crates/matrix-sdk-crypto/src/machine/mod.rs | 16 ++++---- .../src/olm/group_sessions/sender_data.rs | 39 +++++++++---------- .../olm/group_sessions/sender_data_finder.rs | 2 +- .../group_sessions/share_strategy.rs | 24 ++++++------ .../src/store/integration_tests.rs | 3 +- crates/matrix-sdk/CHANGELOG.md | 1 + .../src/test_json/keys_query_sets.rs | 10 ++--- 11 files changed, 73 insertions(+), 71 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 928a8eb561f..b9a58514307 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -29,7 +29,8 @@ use crate::debug::{DebugRawEvent, DebugStructExt}; const AUTHENTICITY_NOT_GUARANTEED: &str = "The authenticity of this encrypted message can't be guaranteed on this device."; const UNVERIFIED_IDENTITY: &str = "Encrypted by an unverified user."; -const PREVIOUSLY_VERIFIED: &str = "Encrypted by a previously-verified user."; +const VERIFICATION_VIOLATION: &str = + "Encrypted by a previously-verified user who is no longer verified."; const UNSIGNED_DEVICE: &str = "Encrypted by a device not verified by its owner."; const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device."; pub const SENT_IN_CLEAR: &str = "Not encrypted."; @@ -92,7 +93,7 @@ impl VerificationState { VerificationState::Verified => ShieldState::None, VerificationState::Unverified(level) => match level { VerificationLevel::UnverifiedIdentity - | VerificationLevel::PreviouslyVerified + | VerificationLevel::VerificationViolation | VerificationLevel::UnsignedDevice => ShieldState::Red { code: ShieldStateCode::UnverifiedIdentity, message: UNVERIFIED_IDENTITY, @@ -127,12 +128,12 @@ impl VerificationState { // nag you with an error message. ShieldState::None } - VerificationLevel::PreviouslyVerified => { + VerificationLevel::VerificationViolation => { // This is a high warning. The sender was previously // verified, but changed their identity. ShieldState::Red { - code: ShieldStateCode::PreviouslyVerified, - message: PREVIOUSLY_VERIFIED, + code: ShieldStateCode::VerificationViolation, + message: VERIFICATION_VIOLATION, } } VerificationLevel::UnsignedDevice => { @@ -175,7 +176,7 @@ pub enum VerificationLevel { /// The message was sent by a user identity we have not verified, but the /// user was previously verified. - PreviouslyVerified, + VerificationViolation, /// The message was sent by a device not linked to (signed by) any user /// identity. @@ -193,7 +194,7 @@ impl fmt::Display for VerificationLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { let display = match self { VerificationLevel::UnverifiedIdentity => "The sender's identity was not verified", - VerificationLevel::PreviouslyVerified => { + VerificationLevel::VerificationViolation => { "The sender's identity was previously verified but has changed" } VerificationLevel::UnsignedDevice => { @@ -258,7 +259,7 @@ pub enum ShieldStateCode { /// An unencrypted event in an encrypted room. SentInClear, /// The sender was previously verified but changed their identity. - PreviouslyVerified, + VerificationViolation, } /// The algorithm specific information of a decrypted event. diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index c45f2fc52da..b4c3732b8f4 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -57,6 +57,9 @@ Breaking changes: the CryptoStore, meaning that, once upgraded, it will not be possible to roll back applications to earlier versions without breaking user sessions. +- Renamed `VerificationLevel::PreviouslyVerified` to + `VerificationLevel::VerificationViolation`. + - `OlmMachine::decrypt_room_event` now takes a `DecryptionSettings` argument, which includes a `TrustRequirement` indicating the required trust level for the sending device. When it is called with `TrustRequirement` other than diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index 512669731c2..bd43f005a27 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -2200,7 +2200,7 @@ pub(crate) mod tests { // Set up a machine do initial own key query and import cross-signing secret to // make the current session verified. async fn common_verified_identity_changes_machine_setup() -> OlmMachine { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; @@ -2220,7 +2220,7 @@ pub(crate) mod tests { } #[async_test] async fn test_manager_verified_latch_setup_on_new_identities() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = common_verified_identity_changes_machine_setup().await; @@ -2276,7 +2276,7 @@ pub(crate) mod tests { #[async_test] async fn test_manager_verified_identity_changes_setup_on_updated_identities() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = common_verified_identity_changes_machine_setup().await; @@ -2318,7 +2318,7 @@ pub(crate) mod tests { // The cross signing secrets are not yet uploaded. // Then query keys for carol and bob (both signed by own identity) async fn common_verified_identity_changes_own_trust_change_machine_setup() -> OlmMachine { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; // Start on a non-verified session let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; @@ -2352,7 +2352,7 @@ pub(crate) mod tests { #[async_test] async fn test_manager_verified_identity_changes_setup_on_own_identity_trust_change() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = common_verified_identity_changes_own_trust_change_machine_setup().await; let own_identity = @@ -2389,7 +2389,7 @@ pub(crate) mod tests { #[async_test] async fn test_manager_verified_identity_change_setup_on_import_secrets() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = common_verified_identity_changes_own_trust_change_machine_setup().await; let own_identity = diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 27fa450e811..6948516d9b7 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -871,7 +871,7 @@ enum OwnUserIdentityVerifiedState { NeverVerified, /// We previously verified this identity, but it has changed. - PreviouslyVerifiedButNoLonger, + VerificationViolation, /// We have verified the current identity. Verified, @@ -1022,7 +1022,7 @@ impl OwnUserIdentityData { pub(crate) fn mark_as_unverified(&self) { let mut guard = self.verified.write().unwrap(); if *guard == OwnUserIdentityVerifiedState::Verified { - *guard = OwnUserIdentityVerifiedState::PreviouslyVerifiedButNoLonger; + *guard = OwnUserIdentityVerifiedState::VerificationViolation; } } @@ -1039,7 +1039,7 @@ impl OwnUserIdentityData { matches!( *self.verified.read().unwrap(), OwnUserIdentityVerifiedState::Verified - | OwnUserIdentityVerifiedState::PreviouslyVerifiedButNoLonger + | OwnUserIdentityVerifiedState::VerificationViolation ) } @@ -1050,7 +1050,7 @@ impl OwnUserIdentityData { /// verify again or to withdraw the verification requirement. pub fn withdraw_verification(&self) { let mut guard = self.verified.write().unwrap(); - if *guard == OwnUserIdentityVerifiedState::PreviouslyVerifiedButNoLonger { + if *guard == OwnUserIdentityVerifiedState::VerificationViolation { *guard = OwnUserIdentityVerifiedState::NeverVerified; } } @@ -1065,8 +1065,7 @@ impl OwnUserIdentityData { /// - Or by withdrawing the verification requirement /// [`OwnUserIdentity::withdraw_verification`]. pub fn has_verification_violation(&self) -> bool { - *self.verified.read().unwrap() - == OwnUserIdentityVerifiedState::PreviouslyVerifiedButNoLonger + *self.verified.read().unwrap() == OwnUserIdentityVerifiedState::VerificationViolation } /// Update the identity with a new master key and self signing key. @@ -1632,7 +1631,7 @@ pub(crate) mod tests { #[async_test] async fn test_resolve_identity_verification_violation_with_withdraw() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; @@ -1672,7 +1671,7 @@ pub(crate) mod tests { #[async_test] async fn test_reset_own_keys_creates_verification_violation() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; @@ -1713,7 +1712,7 @@ pub(crate) mod tests { /// verification violation on our own identity. #[async_test] async fn test_own_keys_update_creates_own_identity_verification_violation() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index f0b9c6c7cbe..b219ba6578d 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -1477,7 +1477,7 @@ impl OlmMachine { sender_data, SenderData::UnknownDevice { .. } | SenderData::DeviceInfo { .. } - | SenderData::SenderUnverifiedButPreviouslyVerified { .. } + | SenderData::VerificationViolation { .. } ) } @@ -1689,8 +1689,8 @@ impl OlmMachine { TrustRequirement::CrossSignedOrLegacy => match &session.sender_data { // Reject if the sender was previously verified, but changed // their identity and is not verified any more. - SenderData::SenderUnverifiedButPreviouslyVerified(..) => Err( - MegolmError::SenderIdentityNotTrusted(VerificationLevel::PreviouslyVerified), + SenderData::VerificationViolation(..) => Err( + MegolmError::SenderIdentityNotTrusted(VerificationLevel::VerificationViolation), ), SenderData::SenderUnverified(..) => Ok(()), SenderData::SenderVerified(..) => Ok(()), @@ -1702,8 +1702,8 @@ impl OlmMachine { TrustRequirement::CrossSigned => match &session.sender_data { // Reject if the sender was previously verified, but changed // their identity and is not verified any more. - SenderData::SenderUnverifiedButPreviouslyVerified(..) => Err( - MegolmError::SenderIdentityNotTrusted(VerificationLevel::PreviouslyVerified), + SenderData::VerificationViolation(..) => Err( + MegolmError::SenderIdentityNotTrusted(VerificationLevel::VerificationViolation), ), SenderData::SenderUnverified(..) => Ok(()), SenderData::SenderVerified(..) => Ok(()), @@ -2493,9 +2493,9 @@ fn sender_data_to_verification_state( VerificationState::Unverified(VerificationLevel::UnsignedDevice), Some(device_keys.device_id), ), - SenderData::SenderUnverifiedButPreviouslyVerified(KnownSenderData { - device_id, .. - }) => (VerificationState::Unverified(VerificationLevel::PreviouslyVerified), device_id), + SenderData::VerificationViolation(KnownSenderData { device_id, .. }) => { + (VerificationState::Unverified(VerificationLevel::VerificationViolation), device_id) + } SenderData::SenderUnverified(KnownSenderData { device_id, .. }) => { (VerificationState::Unverified(VerificationLevel::UnverifiedIdentity), device_id) } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index a9cdbb1235b..df54390e580 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -41,7 +41,7 @@ pub struct KnownSenderData { /// Sessions start off in `UnknownDevice` state, and progress into `DeviceInfo` /// state when we get the device info. Finally, if we can look up the sender /// using the device info, the session can be moved into -/// `SenderUnverifiedButPreviouslyVerified`, `SenderUnverified`, or +/// `VerificationViolation`, `SenderUnverified`, or /// `SenderVerified` state, depending on the verification status of the user. /// If the user's verification state changes, the state may change accordingly. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -79,7 +79,7 @@ pub enum SenderData { /// the to-device message that established this session, but we have not yet /// verified the cross-signing key, and we had verified a previous /// cross-signing key for this user. - SenderUnverifiedButPreviouslyVerified(KnownSenderData), + VerificationViolation(KnownSenderData), /// We have found proof that this user, with this cross-signing key, sent /// the to-device message that established this session, but we have not yet @@ -105,12 +105,12 @@ impl SenderData { /// Create a [`SenderData`] with a known but unverified sender, where the /// sender was previously verified. - pub fn sender_previously_verified( + pub fn sender_verification_violation( user_id: &UserId, device_id: &DeviceId, master_key: Ed25519PublicKey, ) -> Self { - Self::SenderUnverifiedButPreviouslyVerified(KnownSenderData { + Self::VerificationViolation(KnownSenderData { user_id: user_id.to_owned(), device_id: Some(device_id.to_owned()), master_key: Box::new(master_key), @@ -172,7 +172,7 @@ impl SenderData { match self { SenderData::UnknownDevice { .. } => 0, SenderData::DeviceInfo { .. } => 1, - SenderData::SenderUnverifiedButPreviouslyVerified(..) => 2, + SenderData::VerificationViolation(..) => 2, SenderData::SenderUnverified(..) => 3, SenderData::SenderVerified(..) => 4, } @@ -183,9 +183,7 @@ impl SenderData { match self { Self::UnknownDevice { .. } => SenderDataType::UnknownDevice, Self::DeviceInfo { .. } => SenderDataType::DeviceInfo, - Self::SenderUnverifiedButPreviouslyVerified { .. } => { - SenderDataType::SenderUnverifiedButPreviouslyVerified - } + Self::VerificationViolation { .. } => SenderDataType::VerificationViolation, Self::SenderUnverified { .. } => SenderDataType::SenderUnverified, Self::SenderVerified { .. } => SenderDataType::SenderVerified, } @@ -217,7 +215,7 @@ enum SenderDataReader { legacy_session: bool, }, - SenderUnverifiedButPreviouslyVerified(KnownSenderData), + VerificationViolation(KnownSenderData), SenderUnverified(KnownSenderData), @@ -242,9 +240,7 @@ impl From for SenderData { SenderDataReader::DeviceInfo { device_keys, legacy_session } => { Self::DeviceInfo { device_keys, legacy_session } } - SenderDataReader::SenderUnverifiedButPreviouslyVerified(data) => { - Self::SenderUnverifiedButPreviouslyVerified(data) - } + SenderDataReader::VerificationViolation(data) => Self::VerificationViolation(data), SenderDataReader::SenderUnverified(data) => Self::SenderUnverified(data), SenderDataReader::SenderVerified(data) => Self::SenderVerified(data), SenderDataReader::SenderKnown { @@ -273,8 +269,8 @@ pub enum SenderDataType { UnknownDevice = 1, /// The [`SenderData`] is of type `DeviceInfo`. DeviceInfo = 2, - /// The [`SenderData`] is of type `SenderUnverifiedButPreviouslyVerified`. - SenderUnverifiedButPreviouslyVerified = 3, + /// The [`SenderData`] is of type `VerificationViolation`. + VerificationViolation = 3, /// The [`SenderData`] is of type `SenderUnverified`. SenderUnverified = 4, /// The [`SenderData`] is of type `SenderVerified`. @@ -399,7 +395,7 @@ mod tests { )); let master_key = Ed25519PublicKey::from_base64("2/5LWJMow5zhJqakV88SIc7q/1pa8fmkfgAzx72w9G4").unwrap(); - let sender_previously_verified = SenderData::sender_previously_verified( + let sender_verification_violation = SenderData::sender_verification_violation( user_id!("@u:s.co"), device_id!("DEV"), master_key, @@ -410,26 +406,29 @@ mod tests { SenderData::sender_verified(user_id!("@u:s.co"), device_id!("DEV"), master_key); assert_eq!(unknown.compare_trust_level(&device_keys), Ordering::Less); - assert_eq!(unknown.compare_trust_level(&sender_previously_verified), Ordering::Less); + assert_eq!(unknown.compare_trust_level(&sender_verification_violation), Ordering::Less); assert_eq!(unknown.compare_trust_level(&sender_unverified), Ordering::Less); assert_eq!(unknown.compare_trust_level(&sender_verified), Ordering::Less); assert_eq!(device_keys.compare_trust_level(&unknown), Ordering::Greater); - assert_eq!(sender_previously_verified.compare_trust_level(&unknown), Ordering::Greater); + assert_eq!(sender_verification_violation.compare_trust_level(&unknown), Ordering::Greater); assert_eq!(sender_unverified.compare_trust_level(&unknown), Ordering::Greater); assert_eq!(sender_verified.compare_trust_level(&unknown), Ordering::Greater); assert_eq!(device_keys.compare_trust_level(&sender_unverified), Ordering::Less); assert_eq!(device_keys.compare_trust_level(&sender_verified), Ordering::Less); - assert_eq!(sender_previously_verified.compare_trust_level(&device_keys), Ordering::Greater); + assert_eq!( + sender_verification_violation.compare_trust_level(&device_keys), + Ordering::Greater + ); assert_eq!(sender_unverified.compare_trust_level(&device_keys), Ordering::Greater); assert_eq!(sender_verified.compare_trust_level(&device_keys), Ordering::Greater); assert_eq!( - sender_previously_verified.compare_trust_level(&sender_verified), + sender_verification_violation.compare_trust_level(&sender_verified), Ordering::Less ); assert_eq!( - sender_previously_verified.compare_trust_level(&sender_unverified), + sender_verification_violation.compare_trust_level(&sender_unverified), Ordering::Less ); assert_eq!(sender_unverified.compare_trust_level(&sender_verified), Ordering::Less); diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs index 8ef51d6d9be..fcf0394a81b 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs @@ -267,7 +267,7 @@ impl<'a> SenderDataFinder<'a> { .expect("User with master key must have identity") .was_previously_verified() { - SenderData::SenderUnverifiedButPreviouslyVerified(known_sender_data) + SenderData::VerificationViolation(known_sender_data) } else { SenderData::SenderUnverified(known_sender_data) } diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 71af660c0b2..1f0c0e3647c 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -521,7 +521,7 @@ mod tests { async_test, test_json, test_json::keys_query_sets::{ IdentityChangeDataSet, KeyDistributionTestData, MaloIdentityChangeDataSet, - PreviouslyVerifiedTestData, + VerificationViolationTestData, }, }; use ruma::{ @@ -710,7 +710,7 @@ mod tests { /// `error_on_verified_user_problem` is set. #[async_test] async fn test_error_on_unsigned_of_verified_users() { - use PreviouslyVerifiedTestData as DataSet; + use VerificationViolationTestData as DataSet; // We start with Bob, who is verified and has one unsigned device. let machine = unsigned_of_verified_setup().await; @@ -766,7 +766,7 @@ mod tests { /// device. #[async_test] async fn test_error_on_unsigned_of_verified_resolve_by_whitelisting() { - use PreviouslyVerifiedTestData as DataSet; + use VerificationViolationTestData as DataSet; let machine = unsigned_of_verified_setup().await; @@ -802,7 +802,7 @@ mod tests { /// device. #[async_test] async fn test_error_on_unsigned_of_verified_resolve_by_blacklisting() { - use PreviouslyVerifiedTestData as DataSet; + use VerificationViolationTestData as DataSet; let machine = unsigned_of_verified_setup().await; @@ -846,7 +846,7 @@ mod tests { /// is verified and we have unsigned devices. #[async_test] async fn test_error_on_unsigned_of_verified_owner_is_us() { - use PreviouslyVerifiedTestData as DataSet; + use VerificationViolationTestData as DataSet; let machine = unsigned_of_verified_setup().await; @@ -891,7 +891,7 @@ mod tests { /// error. #[async_test] async fn test_should_not_error_on_unsigned_of_unverified() { - use PreviouslyVerifiedTestData as DataSet; + use VerificationViolationTestData as DataSet; let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; @@ -941,7 +941,7 @@ mod tests { /// error, when we have not verified our own identity. #[async_test] async fn test_should_not_error_on_unsigned_of_signed_but_unverified() { - use PreviouslyVerifiedTestData as DataSet; + use VerificationViolationTestData as DataSet; let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; @@ -989,7 +989,7 @@ mod tests { /// withdrawing verification #[async_test] async fn test_verified_user_changed_identity() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; // We start with Bob, who is verified and has one unsigned device. We have also // verified our own identity. @@ -1039,7 +1039,7 @@ mod tests { /// withdrawing verification #[async_test] async fn test_own_verified_identity_changed() { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; // We start with a verified identity. let machine = unsigned_of_verified_setup().await; @@ -1497,10 +1497,10 @@ mod tests { /// /// Returns an `OlmMachine` which is properly configured with trusted /// cross-signing keys. Also imports a set of keys for - /// Bob ([`PreviouslyVerifiedTestData::bob_id`]), where Bob is verified and - /// has 2 devices, one signed and the other not. + /// Bob ([`VerificationViolationTestData::bob_id`]), where Bob is verified + /// and has 2 devices, one signed and the other not. async fn unsigned_of_verified_setup() -> OlmMachine { - use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet; + use test_json::keys_query_sets::VerificationViolationTestData as DataSet; let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await; diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 5cbb61bfb17..0f8af2d0733 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -1247,8 +1247,7 @@ macro_rules! cryptostore_integration_tests { device_keys: account.device_keys().clone(), legacy_session: false, }, - SenderDataType::SenderUnverifiedButPreviouslyVerified => - panic!("SenderUnverifiedButPreviouslyVerified not supported"), + SenderDataType::VerificationViolation => panic!("VerificationViolation not supported"), SenderDataType::SenderUnverified=> panic!("SenderUnverified not supported"), SenderDataType::SenderVerified => panic!("SenderVerified not supported"), }; diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 95c6e781d30..8610707e991 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -2,6 +2,7 @@ Breaking changes: +- Renamed `VerificationLevel::PreviouslyVerified` to `VerificationLevel::VerificationViolation`. - Add a `PreviouslyVerified` variant to `VerificationLevel` indicating that the identity is unverified and previously it was verified. - Replace the `Notification` type from Ruma in `SyncResponse` and `Client::register_notification_handler` by a custom one diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 4fa38e100a6..ae1d8864f77 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -653,10 +653,10 @@ impl IdentityChangeDataSet { /// A set of `/keys/query` responses that were initially created to simulate /// when a user that was verified reset his keys and became unverified. /// -/// The local user (as returned by [`PreviouslyVerifiedTestData::own_id`]) is +/// The local user (as returned by [`VerificationViolationTestData::own_id`]) is /// `@alice:localhost`. There are 2 other users: `@bob:localhost` (returned by -/// [`PreviouslyVerifiedTestData::bob_id`]), and `@carol:localhost` (returned by -/// [`PreviouslyVerifiedTestData::carol_id`]). +/// [`VerificationViolationTestData::bob_id`]), and `@carol:localhost` (returned +/// by [`VerificationViolationTestData::carol_id`]). /// /// We provide two `/keys/query` responses for each of Bob and Carol: one signed /// by Alice, and one not signed. @@ -665,10 +665,10 @@ impl IdentityChangeDataSet { /// another one not cross-signed. /// /// The `/keys/query` responses were generated using a local synapse. -pub struct PreviouslyVerifiedTestData {} +pub struct VerificationViolationTestData {} #[allow(dead_code)] -impl PreviouslyVerifiedTestData { +impl VerificationViolationTestData { /// Secret part of Alice's master cross-signing key. /// /// Exported from Element-Web with the following console snippet: From 1d1863d323842274b8efaf9aff5542a91a81d68d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 2 Oct 2024 10:58:26 +0100 Subject: [PATCH 195/979] crypto: Give `decrypt_room_event` a new return type I want to do a bit of a refactoring on `TimelineEvent`, so let's start by giving `decrypt_room_event` its own return type. --- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 7 ++- .../src/deserialized_responses.rs | 52 ++++++++++++++++++- crates/matrix-sdk-crypto/CHANGELOG.md | 5 +- crates/matrix-sdk-crypto/src/machine/mod.rs | 19 +++---- .../tests/decryption_verification_state.rs | 3 +- .../src/machine/tests/mod.rs | 38 ++++---------- crates/matrix-sdk-ui/src/timeline/traits.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 3 +- 8 files changed, 78 insertions(+), 51 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 52bdee8f548..8bb1fedd718 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -38,7 +38,7 @@ use ruma::{ }, events::{ key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent, - AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent, + AnySyncMessageLikeEvent, MessageLikeEvent, }, serde::Raw, to_device::DeviceIdOrAllDevices, @@ -891,7 +891,7 @@ impl OlmMachine { ))?; if handle_verification_events { - if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() { + if let Ok(e) = decrypted.event.deserialize() { match &e { AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original( original_event, @@ -909,8 +909,7 @@ impl OlmMachine { } } - let encryption_info = - decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info"); + let encryption_info = decrypted.encryption_info; let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?; diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index b9a58514307..e01ea298323 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -15,7 +15,7 @@ use std::{collections::BTreeMap, fmt}; use ruma::{ - events::{AnySyncTimelineEvent, AnyTimelineEvent}, + events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent}, push::Action, serde::{JsonObject, Raw}, DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedUserId, @@ -381,6 +381,13 @@ impl From for SyncTimelineEvent { } } +impl From for SyncTimelineEvent { + fn from(decrypted: DecryptedRoomEvent) -> Self { + let timeline_event: TimelineEvent = decrypted.into(); + timeline_event.into() + } +} + #[derive(Clone)] pub struct TimelineEvent { /// The actual event. @@ -407,6 +414,20 @@ impl TimelineEvent { } } +impl From for TimelineEvent { + fn from(decrypted: DecryptedRoomEvent) -> Self { + Self { + // Casting from the more specific `AnyMessageLikeEvent` (i.e. an event without a + // `state_key`) to a more generic `AnyTimelineEvent` (i.e. one that may contain + // a `state_key`) is safe. + event: decrypted.event.cast(), + encryption_info: Some(decrypted.encryption_info), + push_actions: None, + unsigned_encryption_info: decrypted.unsigned_encryption_info, + } + } +} + #[cfg(not(tarpaulin_include))] impl fmt::Debug for TimelineEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -424,6 +445,35 @@ impl fmt::Debug for TimelineEvent { } } +#[derive(Clone, Serialize, Deserialize)] +/// A successfully-decrypted encrypted event. +pub struct DecryptedRoomEvent { + /// The decrypted event. + pub event: Raw, + + /// The encryption info about the event. + pub encryption_info: EncryptionInfo, + + /// The encryption info about the events bundled in the `unsigned` + /// object. + /// + /// Will be `None` if no bundled event was encrypted. + pub unsigned_encryption_info: Option>, +} + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for DecryptedRoomEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info } = self; + + f.debug_struct("DecryptedRoomEvent") + .field("event", &DebugRawEvent(event)) + .field("encryption_info", encryption_info) + .maybe_field("unsigned_encryption_info", unsigned_encryption_info) + .finish() + } +} + /// The location of an event bundled in an `unsigned` object. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum UnsignedEventLocation { diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index b4c3732b8f4..bbcf222dbdb 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -53,7 +53,10 @@ Changes: Breaking changes: - **NOTE**: this version causes changes to the format of the serialised data in +- `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type, + instead of the more generic `TimelineEvent` type. + +- **NOTE**: this version causes changes to the format of the serialised data in the CryptoStore, meaning that, once upgraded, it will not be possible to roll back applications to earlier versions without breaking user sessions. diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index b219ba6578d..763f9ebb464 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -21,7 +21,7 @@ use std::{ use itertools::Itertools; use matrix_sdk_common::{ deserialized_responses::{ - AlgorithmInfo, DeviceLinkProblem, EncryptionInfo, TimelineEvent, UnableToDecryptInfo, + AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, UnableToDecryptInfo, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, VerificationState, }, BoxFuture, @@ -40,7 +40,7 @@ use ruma::{ assign, events::{ secret::request::SecretName, AnyMessageLikeEvent, AnyMessageLikeEventContent, - AnyTimelineEvent, AnyToDeviceEvent, MessageLikeEventContent, + AnyToDeviceEvent, MessageLikeEventContent, }, serde::{JsonObject, Raw}, DeviceId, DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedDeviceKeyId, @@ -1747,7 +1747,7 @@ impl OlmMachine { event: &Raw, room_id: &RoomId, decryption_settings: &DecryptionSettings, - ) -> MegolmResult { + ) -> MegolmResult { self.decrypt_room_event_inner(event, room_id, true, decryption_settings).await } @@ -1758,7 +1758,7 @@ impl OlmMachine { room_id: &RoomId, decrypt_unsigned: bool, decryption_settings: &DecryptionSettings, - ) -> MegolmResult { + ) -> MegolmResult { let event = event.deserialize()?; Span::current() @@ -1818,14 +1818,9 @@ impl OlmMachine { .await; } - let event = serde_json::from_value::>(decrypted_event.into())?; + let event = serde_json::from_value::>(decrypted_event.into())?; - Ok(TimelineEvent { - event, - encryption_info: Some(encryption_info), - push_actions: None, - unsigned_encryption_info, - }) + Ok(DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info }) } /// Try to decrypt the events bundled in the `unsigned` object of the given @@ -1906,7 +1901,7 @@ impl OlmMachine { Ok(decrypted_event) => { // Replace the encrypted event. *event = serde_json::to_value(decrypted_event.event).ok()?; - Some(UnsignedDecryptionResult::Decrypted(decrypted_event.encryption_info?)) + Some(UnsignedDecryptionResult::Decrypted(decrypted_event.encryption_info)) } Err(_) => { let session_id = diff --git a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs index 2187b615215..027d38b63a7 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs @@ -117,8 +117,7 @@ async fn test_decryption_verification_state() { .decrypt_room_event(&event, room_id, &decryption_settings) .await .unwrap() - .encryption_info - .unwrap(); + .encryption_info; assert_eq!( VerificationState::Unverified(VerificationLevel::UnsignedDevice), diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 8d7bea24dad..4a0dd8e60f8 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -31,8 +31,8 @@ use ruma::{ room::message::{ AddMentions, MessageType, Relation, ReplyWithinThread, RoomMessageEventContent, }, - AnyMessageLikeEvent, AnyMessageLikeEventContent, AnyTimelineEvent, AnyToDeviceEvent, - MessageLikeEvent, OriginalMessageLikeEvent, + AnyMessageLikeEvent, AnyMessageLikeEventContent, AnyToDeviceEvent, MessageLikeEvent, + OriginalMessageLikeEvent, }, room_id, serde::Raw, @@ -563,8 +563,8 @@ async fn test_megolm_encryption() { .deserialize() .unwrap(); - if let AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage( - MessageLikeEvent::Original(OriginalMessageLikeEvent { sender, content, .. }), + if let AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original( + OriginalMessageLikeEvent { sender, content, .. }, )) = decrypted_event { assert_eq!(&sender, alice.user_id()); @@ -1291,16 +1291,12 @@ async fn test_unsigned_decryption() { bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); - assert_matches!( - decrypted_event, - AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(first_message)) - ); + assert_matches!(decrypted_event, AnyMessageLikeEvent::RoomMessage(first_message)); let first_message = first_message.as_original().unwrap(); assert_eq!(first_message.content.body(), first_message_text); assert!(first_message.unsigned.relations.is_empty()); - assert!(raw_decrypted_event.encryption_info.is_some()); assert!(raw_decrypted_event.unsigned_encryption_info.is_none()); // Get a new room key, but don't give it to Bob yet. @@ -1344,10 +1340,7 @@ async fn test_unsigned_decryption() { bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); - assert_matches!( - decrypted_event, - AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(first_message)) - ); + assert_matches!(decrypted_event, AnyMessageLikeEvent::RoomMessage(first_message)); let first_message = first_message.as_original().unwrap(); assert_eq!(first_message.content.body(), first_message_text); @@ -1355,7 +1348,6 @@ async fn test_unsigned_decryption() { assert!(first_message.unsigned.relations.replace.is_none()); assert!(first_message.unsigned.relations.has_replacement()); - assert!(raw_decrypted_event.encryption_info.is_some()); let unsigned_encryption_info = raw_decrypted_event.unsigned_encryption_info.unwrap(); assert_eq!(unsigned_encryption_info.len(), 1); let replace_encryption_result = @@ -1388,10 +1380,7 @@ async fn test_unsigned_decryption() { bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); - assert_matches!( - decrypted_event, - AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(first_message)) - ); + assert_matches!(decrypted_event, AnyMessageLikeEvent::RoomMessage(first_message)); let first_message = first_message.as_original().unwrap(); assert_eq!(first_message.content.body(), first_message_text); @@ -1399,7 +1388,6 @@ async fn test_unsigned_decryption() { assert_matches!(&replace.content.relates_to, Some(Relation::Replacement(replace_content))); assert_eq!(replace_content.new_content.msgtype.body(), second_message_text); - assert!(raw_decrypted_event.encryption_info.is_some()); let unsigned_encryption_info = raw_decrypted_event.unsigned_encryption_info.unwrap(); assert_eq!(unsigned_encryption_info.len(), 1); let replace_encryption_result = @@ -1453,10 +1441,7 @@ async fn test_unsigned_decryption() { bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); - assert_matches!( - decrypted_event, - AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(first_message)) - ); + assert_matches!(decrypted_event, AnyMessageLikeEvent::RoomMessage(first_message)); let first_message = first_message.as_original().unwrap(); assert_eq!(first_message.content.body(), first_message_text); @@ -1465,7 +1450,6 @@ async fn test_unsigned_decryption() { let thread = first_message.unsigned.relations.thread.as_ref().unwrap(); assert_matches!(thread.latest_event.deserialize(), Ok(AnyMessageLikeEvent::RoomEncrypted(_))); - assert!(raw_decrypted_event.encryption_info.is_some()); let unsigned_encryption_info = raw_decrypted_event.unsigned_encryption_info.unwrap(); assert_eq!(unsigned_encryption_info.len(), 2); let replace_encryption_result = @@ -1501,10 +1485,7 @@ async fn test_unsigned_decryption() { bob.decrypt_room_event(&raw_encrypted_event, room_id, &decryption_settings).await.unwrap(); let decrypted_event = raw_decrypted_event.event.deserialize().unwrap(); - assert_matches!( - decrypted_event, - AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(first_message)) - ); + assert_matches!(decrypted_event, AnyMessageLikeEvent::RoomMessage(first_message)); let first_message = first_message.as_original().unwrap(); assert_eq!(first_message.content.body(), first_message_text); @@ -1517,7 +1498,6 @@ async fn test_unsigned_decryption() { let third_message = third_message.as_original().unwrap(); assert_eq!(third_message.content.body(), third_message_text); - assert!(raw_decrypted_event.encryption_info.is_some()); let unsigned_encryption_info = raw_decrypted_event.unsigned_encryption_info.unwrap(); assert_eq!(unsigned_encryption_info.len(), 2); let replace_encryption_result = diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index f1aefb2008b..158488eb06f 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -304,6 +304,6 @@ impl Decryptor for (matrix_sdk_base::crypto::OlmMachine, ruma::OwnedRoomId) { DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; let event = olm_machine.decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings).await?; - Ok(event) + Ok(event.into()) } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 4c5a83ced86..09479921ad6 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1222,7 +1222,7 @@ impl Room { let decryption_settings = DecryptionSettings { sender_device_trust_requirement: self.client.base_client().decryption_trust_requirement, }; - let mut event = match machine + let decrypted = match machine .decrypt_room_event(event.cast_ref(), self.inner.room_id(), &decryption_settings) .await { @@ -1237,6 +1237,7 @@ impl Room { } }; + let mut event: TimelineEvent = decrypted.into(); event.push_actions = self.event_push_actions(&event.event).await?; Ok(event) From 7bac0340d60ff8d6ce46c990646771853b66aef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 30 Sep 2024 17:27:31 +0200 Subject: [PATCH 196/979] base: Apply RoomInfo migrations for notable_tags and pinned_events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk-base/src/rooms/normal.rs | 186 +++++++++++++++++- .../src/store/migration_helpers.rs | 1 + crates/matrix-sdk-base/src/store/mod.rs | 33 +++- crates/matrix-sdk/tests/integration/client.rs | 87 +++++++- 4 files changed, 300 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 2424907a466..38c83972cbf 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -40,10 +40,11 @@ use ruma::{ history_visibility::HistoryVisibility, join_rules::JoinRule, member::{MembershipState, RoomMemberEventContent}, + pinned_events::RoomPinnedEventsEventContent, redaction::SyncRoomRedactionEvent, tombstone::RoomTombstoneEventContent, }, - tag::Tags, + tag::{TagEventContent, Tags}, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, RoomAccountDataEventType, }, @@ -63,7 +64,7 @@ use super::{ #[cfg(feature = "experimental-sliding-sync")] use crate::latest_event::LatestEvent; use crate::{ - deserialized_responses::MemberEvent, + deserialized_responses::{MemberEvent, RawSyncOrStrippedState}, notification_settings::RoomNotificationMode, read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, @@ -1021,6 +1022,10 @@ fn test_send_sync_for_room() { /// Holds all the info needed to persist a room into the state store. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RoomInfo { + /// The version of the room info. + #[serde(default)] + pub(crate) version: u8, + /// The unique room id of the room. pub(crate) room_id: OwnedRoomId, @@ -1109,6 +1114,7 @@ impl RoomInfo { #[doc(hidden)] // used by store tests, otherwise it would be pub(crate) pub fn new(room_id: &RoomId, room_state: RoomState) -> Self { Self { + version: 1, room_id: room_id.into(), room_state, notification_counts: Default::default(), @@ -1567,6 +1573,68 @@ impl RoomInfo { .map(|p| p.pinned.contains(&event_id.to_owned())) .unwrap_or_default() } + + /// Apply migrations to this `RoomInfo` if needed. + /// + /// This should be used to populate new fields with data from the state + /// store. + /// + /// Returns `true` if migrations were applied and this `RoomInfo` needs to + /// be persisted to the state store. + #[instrument(skip_all, fields(room_id = ?self.room_id))] + pub(crate) async fn apply_migrations(&mut self, store: Arc) -> bool { + let mut migrated = false; + + if self.version < 1 { + info!("Migrating room info to version 1"); + + // notable_tags + match store.get_room_account_data_event_static::(&self.room_id).await { + // Pinned events are never in stripped state. + Ok(Some(raw_event)) => match raw_event.deserialize() { + Ok(event) => { + self.base_info.handle_notable_tags(&event.content.tags); + } + Err(error) => { + warn!("Failed to deserialize room tags: {error}"); + } + }, + Ok(_) => { + // Nothing to do. + } + Err(error) => { + warn!("Failed to load room tags: {error}"); + } + } + + // pinned_events + match store.get_state_event_static::(&self.room_id).await + { + // Pinned events are never in stripped state. + Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => { + match raw_event.deserialize() { + Ok(event) => { + self.handle_state_event(&event.into()); + } + Err(error) => { + warn!("Failed to deserialize room pinned events: {error}"); + } + } + } + Ok(_) => { + // Nothing to do. + } + Err(error) => { + warn!("Failed to load room pinned events: {error}"); + } + } + + self.version = 1; + migrated = true; + } + + migrated + } } #[cfg(feature = "experimental-sliding-sync")] @@ -1700,7 +1768,11 @@ mod tests { use assign::assign; #[cfg(feature = "experimental-sliding-sync")] use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; - use matrix_sdk_test::{async_test, ALICE, BOB, CAROL}; + use matrix_sdk_test::{ + async_test, + test_json::{sync_events::PINNED_EVENTS, TAG}, + ALICE, BOB, CAROL, + }; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, device_id, event_id, @@ -1722,7 +1794,7 @@ mod tests { }, AnySyncStateEvent, EmptyStateKey, StateEventType, StateUnsigned, SyncStateEvent, }, - owned_event_id, room_alias_id, room_id, + owned_event_id, owned_user_id, room_alias_id, room_id, serde::Raw, time::SystemTime, user_id, DeviceId, EventEncryptionAlgorithm, EventId, MilliSecondsSinceUnixEpoch, @@ -1735,7 +1807,8 @@ mod tests { #[cfg(any(feature = "experimental-sliding-sync", feature = "e2e-encryption"))] use crate::latest_event::LatestEvent; use crate::{ - store::{MemoryStore, StateChanges, StateStore}, + rooms::RoomNotableTags, + store::{IntoStateStore, MemoryStore, StateChanges, StateStore}, BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, SessionMeta, }; @@ -1751,6 +1824,7 @@ mod tests { use crate::{rooms::BaseRoomInfo, sync::UnreadNotificationsCount}; let info = RoomInfo { + version: 1, room_id: room_id!("!gda78o:server.tld").into(), room_state: RoomState::Invited, notification_counts: UnreadNotificationsCount { @@ -1784,6 +1858,7 @@ mod tests { }; let info_json = json!({ + "version": 1, "room_id": "!gda78o:server.tld", "room_state": "Invited", "notification_counts": { @@ -2990,4 +3065,105 @@ mod tests { assert!(room.is_encryption_state_synced()); assert!(room.is_encrypted()); } + + #[async_test] + async fn test_room_info_migration_v1() { + let store = MemoryStore::new().into_state_store(); + + let room_info_json = json!({ + "room_id": "!gda78o:server.tld", + "room_state": "Joined", + "notification_counts": { + "highlight_count": 1, + "notification_count": 2, + }, + "summary": { + "room_heroes": [{ + "user_id": "@somebody:example.org", + "display_name": null, + "avatar_url": null + }], + "joined_member_count": 5, + "invited_member_count": 0, + }, + "members_synced": true, + "last_prev_batch": "pb", + "sync_info": "FullySynced", + "encryption_state_synced": true, + "latest_event": { + "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, + }, + "read_receipts": { + "num_unread": 0, + "num_mentions": 0, + "num_notifications": 0, + "latest_active": null, + "pending": [] + }, + "recency_stamp": 42, + }); + let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap(); + + assert_eq!(room_info.version, 0); + assert!(room_info.base_info.notable_tags.is_empty()); + assert!(room_info.base_info.pinned_events.is_none()); + + // Apply migrations with an empty store. + assert!(room_info.apply_migrations(store.clone()).await); + + assert_eq!(room_info.version, 1); + assert!(room_info.base_info.notable_tags.is_empty()); + assert!(room_info.base_info.pinned_events.is_none()); + + // Applying migrations again has no effect. + assert!(!room_info.apply_migrations(store.clone()).await); + + assert_eq!(room_info.version, 1); + assert!(room_info.base_info.notable_tags.is_empty()); + assert!(room_info.base_info.pinned_events.is_none()); + + // Add events to the store. + let mut changes = StateChanges::default(); + + let raw_tag_event = Raw::new(&*TAG).unwrap().cast(); + let tag_event = raw_tag_event.deserialize().unwrap(); + changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event); + + let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast(); + let pinned_events_event = raw_pinned_events_event.deserialize().unwrap(); + changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event); + + store.save_changes(&changes).await.unwrap(); + + // Reset to version 0 and reapply migrations. + room_info.version = 0; + assert!(room_info.apply_migrations(store.clone()).await); + + assert_eq!(room_info.version, 1); + assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)); + assert!(room_info.base_info.pinned_events.is_some()); + + // Creating a new room info initializes it to version 1. + let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined); + assert_eq!(new_room_info.version, 1); + } } diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index 40076f19458..454f3afc977 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -111,6 +111,7 @@ impl RoomInfoV1 { } = self; RoomInfo { + version: 0, room_id, room_state: room_type, notification_counts, diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 9e496aa11a1..9c774a769a0 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -57,6 +57,7 @@ use ruma::{ EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; use tokio::sync::{broadcast, Mutex, RwLock}; +use tracing::warn; use crate::{ event_cache_store::{DynEventCacheStore, IntoEventCacheStore}, @@ -171,6 +172,36 @@ impl Store { &self.sync_lock } + /// Load the room infos from the inner `StateStore`. + /// + /// Applies migrations to the room infos if needed. + async fn load_room_infos(&self) -> Result> { + let mut room_infos = self.inner.get_room_infos().await?; + let mut migrated_room_infos = Vec::with_capacity(room_infos.len()); + + for room_info in room_infos.iter_mut() { + if room_info.apply_migrations(self.inner.clone()).await { + migrated_room_infos.push(room_info.clone()); + } + } + + if !migrated_room_infos.is_empty() { + let changes = StateChanges { + room_infos: migrated_room_infos + .into_iter() + .map(|room_info| (room_info.room_id.clone(), room_info)) + .collect(), + ..Default::default() + }; + + if let Err(error) = self.inner.save_changes(&changes).await { + warn!("Failed to save migrated room infos: {error}"); + } + } + + Ok(room_infos) + } + /// Set the meta of the session. /// /// Restores the state of this `Store` from the given `SessionMeta` and the @@ -183,7 +214,7 @@ impl Store { room_info_notable_update_sender: &broadcast::Sender, ) -> Result<()> { { - let room_infos = self.inner.get_room_infos().await?; + let room_infos = self.load_room_infos().await?; let mut rooms = self.rooms.write().unwrap(); diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index d3af616a76f..ac8158c1db0 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -4,7 +4,11 @@ use assert_matches2::{assert_let, assert_matches}; use eyeball_im::VectorDiff; use futures_util::FutureExt; use matrix_sdk::{ - config::SyncSettings, sync::RoomUpdate, test_utils::no_retry_test_client_with_server, + config::{RequestConfig, StoreConfig, SyncSettings}, + matrix_auth::{MatrixSession, MatrixSessionTokens}, + sync::RoomUpdate, + test_utils::no_retry_test_client_with_server, + Client, MemoryStore, SessionMeta, StateChanges, StateStore, }; use matrix_sdk_base::{sync::RoomUpdates, RoomState}; use matrix_sdk_test::{ @@ -12,6 +16,8 @@ use matrix_sdk_test::{ test_json::{ self, sync::{MIXED_INVITED_ROOM_ID, MIXED_JOINED_ROOM_ID, MIXED_LEFT_ROOM_ID, MIXED_SYNC}, + sync_events::PINNED_EVENTS, + TAG, }, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, }; @@ -1292,3 +1298,82 @@ async fn test_dms_are_processed_in_any_sync_response() { let room_2 = client.get_room(room_id_2).unwrap(); assert!(room_2.is_direct().await.unwrap()); } + +#[async_test] +async fn test_restore_room() { + let room_id = room_id!("!stored_room:localhost"); + + // Create memory store with some room data. + let store = MemoryStore::new(); + + let mut changes = StateChanges::default(); + + let raw_tag_event = Raw::new(&*TAG).unwrap().cast(); + let tag_event = raw_tag_event.deserialize().unwrap(); + changes.add_room_account_data(room_id, tag_event, raw_tag_event); + + let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast(); + let pinned_events_event = raw_pinned_events_event.deserialize().unwrap(); + changes.add_state_event(room_id, pinned_events_event, raw_pinned_events_event); + + let room_info = serde_json::from_value(json!({ + "room_id": room_id, + "room_state": "Joined", + "notification_counts": { + "highlight_count": 0, + "notification_count": 0, + }, + "summary": { + "room_heroes": [], + "joined_member_count": 1, + "invited_member_count": 0, + }, + "members_synced": true, + "last_prev_batch": "pb", + "sync_info": "FullySynced", + "encryption_state_synced": true, + "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, + }, + })) + .unwrap(); + changes.add_room(room_info); + + store.save_changes(&changes).await.unwrap(); + + // Build a client with that store. + let store_config = StoreConfig::new().state_store(store); + let client = Client::builder() + .homeserver_url("http://localhost:1234") + .request_config(RequestConfig::new().disable_retry()) + .store_config(store_config) + .build() + .await + .unwrap(); + client + .matrix_auth() + .restore_session(MatrixSession { + meta: SessionMeta { + user_id: user_id!("@example:localhost").to_owned(), + device_id: device_id!("DEVICEID").to_owned(), + }, + tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None }, + }) + .await + .unwrap(); + + let room = client.get_room(room_id).unwrap(); + assert!(room.is_favourite()); + assert!(!room.pinned_event_ids().is_empty()); +} From 65b422312c27a1b7f5a8e25f1156d1a1d6691cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 4 Oct 2024 12:22:17 +0200 Subject: [PATCH 197/979] chore: Enable the proper feature of tower MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We only use `service_fn` which is behind the `util` feature. Signed-off-by: Kévin Commaille --- crates/matrix-sdk/Cargo.toml | 2 +- examples/oidc_cli/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index d478e76b38a..096ce0250e9 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -106,7 +106,7 @@ sha2 = { workspace = true, optional = true } tempfile = "3.3.0" thiserror = { workspace = true } tokio-stream = { workspace = true, features = ["sync"] } -tower = { version = "0.4.13", features = ["make"], optional = true } +tower = { version = "0.4.13", features = ["util"], optional = true } tracing = { workspace = true, features = ["attributes"] } uniffi = { workspace = true, optional = true } url = { workspace = true, features = ["serde"] } diff --git a/examples/oidc_cli/Cargo.toml b/examples/oidc_cli/Cargo.toml index 802a0e6612d..5d4af394921 100644 --- a/examples/oidc_cli/Cargo.toml +++ b/examples/oidc_cli/Cargo.toml @@ -18,7 +18,7 @@ rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tower = { version = "0.4.13", features = ["make"] } +tower = { version = "0.4.13", features = ["util"] } tracing-subscriber = { workspace = true } url = { workspace = true } From 5d46b35d95a4e4255d6c1f2f8684ca7f0bd7bfa6 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 4 Oct 2024 13:49:41 +0100 Subject: [PATCH 198/979] crypto: Rename some straggling 'Identities' to 'Identity' The main enum was renamed to `UserIdentity` and some aliases and comments had not kept up. --- bindings/matrix-sdk-crypto-ffi/src/users.rs | 2 +- .../matrix-sdk-crypto/src/identities/user.rs | 2 +- crates/matrix-sdk-crypto/src/machine/mod.rs | 2 +- .../src/encryption/identities/users.rs | 24 +++++++++---------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/users.rs b/bindings/matrix-sdk-crypto-ffi/src/users.rs index 6120f4c8319..dfffd9fe546 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/users.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/users.rs @@ -2,7 +2,7 @@ use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity} use crate::CryptoStoreError; -/// Enum representing cross signing identities of our own user or some other +/// Enum representing cross signing identity of our own user or some other /// user. #[derive(uniffi::Enum)] pub enum UserIdentity { diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 6948516d9b7..e70c1201383 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -446,7 +446,7 @@ impl OtherUserIdentity { pub enum UserIdentityData { /// Our own user identity. Own(OwnUserIdentityData), - /// Identities of other users. + /// The identity of another user. Other(OtherUserIdentityData), } diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 763f9ebb464..949ca1e56f5 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -2090,7 +2090,7 @@ impl OlmMachine { /// the requests from [`OlmMachine::outgoing_requests`] are being /// processed and sent out. /// - /// Returns a `UserIdentities` enum if one is found and the crypto store + /// Returns a [`UserIdentity`] enum if one is found and the crypto store /// didn't throw an error. #[instrument(skip(self))] pub async fn get_identity( diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index b2f989e46a8..d71ca1e7e23 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -15,7 +15,7 @@ use std::collections::BTreeMap; use matrix_sdk_base::{ - crypto::{types::MasterPubkey, CryptoStoreError, UserIdentity as CryptoUserIdentities}, + crypto::{types::MasterPubkey, CryptoStoreError, UserIdentity as CryptoUserIdentity}, RoomMemberships, }; use ruma::{ @@ -100,16 +100,16 @@ impl IdentityUpdates { #[derive(Debug, Clone)] pub struct UserIdentity { client: Client, - inner: CryptoUserIdentities, + inner: CryptoUserIdentity, } impl UserIdentity { - pub(crate) fn new(client: Client, identity: CryptoUserIdentities) -> Self { + pub(crate) fn new(client: Client, identity: CryptoUserIdentity) -> Self { Self { inner: identity, client } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] - pub(crate) fn underlying_identity(&self) -> CryptoUserIdentities { + pub(crate) fn underlying_identity(&self) -> CryptoUserIdentity { self.inner.clone() } @@ -134,8 +134,8 @@ impl UserIdentity { /// ``` pub fn user_id(&self) -> &UserId { match &self.inner { - CryptoUserIdentities::Own(identity) => identity.user_id(), - CryptoUserIdentities::Other(identity) => identity.user_id(), + CryptoUserIdentity::Own(identity) => identity.user_id(), + CryptoUserIdentity::Other(identity) => identity.user_id(), } } @@ -257,7 +257,7 @@ impl UserIdentity { methods: Option>, ) -> Result { match &self.inner { - CryptoUserIdentities::Own(identity) => { + CryptoUserIdentity::Own(identity) => { let (verification, request) = if let Some(methods) = methods { identity .request_verification_with_methods(methods) @@ -271,7 +271,7 @@ impl UserIdentity { Ok(VerificationRequest { inner: verification, client: self.client.clone() }) } - CryptoUserIdentities::Other(i) => { + CryptoUserIdentity::Other(i) => { let content = i.verification_request_content(methods.clone()); let room = if let Some(room) = self.client.get_dm_room(i.user_id()) { @@ -362,8 +362,8 @@ impl UserIdentity { /// [`Encryption::cross_signing_status()`]: crate::encryption::Encryption::cross_signing_status pub async fn verify(&self) -> Result<(), ManualVerifyError> { let request = match &self.inner { - CryptoUserIdentities::Own(identity) => identity.verify().await?, - CryptoUserIdentities::Other(identity) => identity.verify().await?, + CryptoUserIdentity::Own(identity) => identity.verify().await?, + CryptoUserIdentity::Other(identity) => identity.verify().await?, }; self.client.send(request, None).await?; @@ -465,8 +465,8 @@ impl UserIdentity { /// ``` pub fn master_key(&self) -> &MasterPubkey { match &self.inner { - CryptoUserIdentities::Own(identity) => identity.master_key(), - CryptoUserIdentities::Other(identity) => identity.master_key(), + CryptoUserIdentity::Own(identity) => identity.master_key(), + CryptoUserIdentity::Other(identity) => identity.master_key(), } } } From a4415c9fa59d2cedfde1829f7ba6d56659f2e79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 4 Oct 2024 13:04:11 +0200 Subject: [PATCH 199/979] chore: Use a released version of vodozemac --- Cargo.lock | 238 +++++++++++++++++++++++++++-------------------------- Cargo.toml | 2 +- 2 files changed, 124 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b983c7002ec..de3dde76178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -177,7 +177,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -194,7 +194,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -205,9 +205,9 @@ checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" dependencies = [ "serde", ] @@ -241,7 +241,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -359,7 +359,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -370,7 +370,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -897,7 +897,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1202,7 +1202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1239,7 +1239,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1263,7 +1263,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1274,7 +1274,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1359,7 +1359,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1747,7 +1747,7 @@ checksum = "dd65f1b59dd22d680c7a626cc4a000c1e03d241c51c3e034d2bc9f1e90734f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1819,7 +1819,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -1995,7 +1995,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -2154,7 +2154,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.2.6", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -2187,6 +2187,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hashlink" version = "0.9.1" @@ -2267,7 +2273,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -2553,12 +2559,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.0", "serde", ] @@ -2575,7 +2581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c77a3ae7d4761b9c64d2c030f70746ceb8cfba32dce0325a56792e0a4816c31" dependencies = [ "ahash", - "indexmap 2.2.6", + "indexmap 2.6.0", "is-terminal", "itoa", "log", @@ -2613,7 +2619,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -2909,7 +2915,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -2920,7 +2926,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -2933,7 +2939,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -3072,9 +3078,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "matrix-pickle" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb521190328c57a2051f70250beb874dc0fac6bcd22b615f7f9700b7b4fb826" +checksum = "4e2551de3bba2cc65b52dc6b268df6114011fe118ac24870fbcf1b35537bd721" dependencies = [ "matrix-pickle-derive", "thiserror", @@ -3082,15 +3088,15 @@ dependencies = [ [[package]] name = "matrix-pickle-derive" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fb3c7231cbb7fbbc50871615edebf65183b382cdaa1fe21c5e88a12617de8e" +checksum = "f75de44c3120d78e978adbcf6d453b20ba011f3c46363e52d1dbbc72f545e9fb" dependencies = [ "proc-macro-crate", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -3124,7 +3130,7 @@ dependencies = [ "http", "image", "imbl", - "indexmap 2.2.6", + "indexmap 2.6.0", "js_int", "language-tags", "mas-oidc-client", @@ -3488,7 +3494,7 @@ name = "matrix-sdk-test-macros" version = "0.7.0" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -3513,7 +3519,7 @@ dependencies = [ "fuzzy-matcher", "growable-bloom-filter", "imbl", - "indexmap 2.2.6", + "indexmap 2.6.0", "itertools 0.12.1", "matrix-sdk", "matrix-sdk-base", @@ -3752,7 +3758,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -3946,7 +3952,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -4178,7 +4184,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -4216,7 +4222,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -4367,11 +4373,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.21.1", + "toml_edit", ] [[package]] @@ -4398,6 +4404,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -4423,7 +4450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -4448,9 +4475,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", "prost-derive", @@ -4458,15 +4485,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -4582,9 +4609,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -4994,7 +5021,7 @@ dependencies = [ "form_urlencoded", "getrandom", "http", - "indexmap 2.2.6", + "indexmap 2.6.0", "js-sys", "js_int", "konst", @@ -5021,7 +5048,7 @@ version = "0.28.1" source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" dependencies = [ "as_variant", - "indexmap 2.2.6", + "indexmap 2.6.0", "js_int", "js_option", "percent-encoding", @@ -5087,7 +5114,7 @@ dependencies = [ "quote", "ruma-identifiers-validation", "serde", - "syn 2.0.72", + "syn 2.0.79", "toml 0.8.15", ] @@ -5273,7 +5300,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -5305,7 +5332,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -5356,9 +5383,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -5395,13 +5422,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -5412,7 +5439,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -5422,7 +5449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" dependencies = [ "form_urlencoded", - "indexmap 2.2.6", + "indexmap 2.6.0", "itoa", "ryu", "serde", @@ -5430,11 +5457,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -5462,9 +5489,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -5491,7 +5518,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.6.0", "serde", "serde_derive", "serde_json", @@ -5508,7 +5535,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -5680,7 +5707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -5761,7 +5788,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -5805,9 +5832,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -5894,7 +5921,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -6000,7 +6027,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -6079,40 +6106,29 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.16", + "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.2.6", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" -dependencies = [ - "indexmap 2.2.6", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.14", + "winnow", ] [[package]] @@ -6197,7 +6213,7 @@ source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -6429,7 +6445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcfa22f55829d3aaa7acfb1c5150224188fe0f27c59a8a3eddcaa24d1ffbe58" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -6461,7 +6477,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.72", + "syn 2.0.79", "toml 0.5.11", "uniffi_meta", ] @@ -6610,8 +6626,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vodozemac" -version = "0.7.0" -source = "git+https://github.com/matrix-org/vodozemac?rev=57cbf7e939d7b54d20207e8361b7135bd65c9cc2#57cbf7e939d7b54d20207e8361b7135bd65c9cc2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7761890811f1dfe2ebef5e630add0a98597682ebf4e4477d98fb8d2e9172ac" dependencies = [ "aes", "arrayvec", @@ -6692,7 +6709,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", "wasm-bindgen-shared", ] @@ -6726,7 +6743,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6759,7 +6776,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -7006,18 +7023,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.6.14" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374ec40a2d767a3c1b4972d9475ecd557356637be906f2cb3f7fe17a6eb5e22f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -7119,7 +7127,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] @@ -7139,7 +7147,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.79", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3bced02cd96..b83c6155104 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ tracing-subscriber = "0.3.18" uniffi = { version = "0.28.0" } uniffi_bindgen = { version = "0.28.0" } url = "2.5.0" -vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "57cbf7e939d7b54d20207e8361b7135bd65c9cc2", features = ["insecure-pk-encryption"] } +vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] } wiremock = "0.6.0" zeroize = "1.6.0" From de752eb089ff527af091879cc90d2379a452c3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 4 Oct 2024 13:05:17 +0200 Subject: [PATCH 200/979] chore: Use a released version of the qrcode crate for the qr-login example --- Cargo.lock | 12 ++---------- examples/qr-login/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de3dde76178..d5b99265047 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1691,7 +1691,7 @@ dependencies = [ "clap", "futures-util", "matrix-sdk", - "qrcode 0.14.1 (git+https://github.com/kennytm/qrcode-rust/)", + "qrcode", "tokio", "tracing-subscriber", "url", @@ -3418,7 +3418,7 @@ version = "0.7.1" dependencies = [ "byteorder", "image", - "qrcode 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", + "qrcode", "ruma-common", "thiserror", "vodozemac", @@ -4532,14 +4532,6 @@ dependencies = [ "image", ] -[[package]] -name = "qrcode" -version = "0.14.1" -source = "git+https://github.com/kennytm/qrcode-rust/#e9373dea6a90fd4f39292b464f718649bbdb36a9" -dependencies = [ - "image", -] - [[package]] name = "quick-error" version = "1.2.3" diff --git a/examples/qr-login/Cargo.toml b/examples/qr-login/Cargo.toml index 2c1898d7318..44f2d6be861 100644 --- a/examples/qr-login/Cargo.toml +++ b/examples/qr-login/Cargo.toml @@ -12,7 +12,7 @@ test = false anyhow = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } clap = { version = "4.0.15", features = ["derive"] } -qrcode = { git = "https://github.com/kennytm/qrcode-rust/" } +qrcode = { version = "0.14.1" } futures-util = { workspace = true } tracing-subscriber = { workspace = true } url = "2.3.1" From 657c72904a53ca5154adecd2c23df8f832cbbf80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 4 Oct 2024 13:08:10 +0200 Subject: [PATCH 201/979] chore: Define our license in every crate we have --- examples/autojoin/Cargo.toml | 1 + examples/backups/Cargo.toml | 1 + examples/command_bot/Cargo.toml | 1 + examples/cross_signing_bootstrap/Cargo.toml | 1 + examples/custom_events/Cargo.toml | 1 + examples/emoji_verification/Cargo.toml | 1 + examples/get_profiles/Cargo.toml | 1 + examples/getting_started/Cargo.toml | 1 + examples/image_bot/Cargo.toml | 1 + examples/login/Cargo.toml | 1 + examples/oidc_cli/Cargo.toml | 1 + examples/persist_session/Cargo.toml | 1 + examples/qr-login/Cargo.toml | 1 + examples/secret_storage/Cargo.toml | 1 + examples/timeline/Cargo.toml | 1 + labs/multiverse/Cargo.toml | 1 + testing/matrix-sdk-integration-testing/Cargo.toml | 1 + uniffi-bindgen/Cargo.toml | 1 + xtask/Cargo.toml | 1 + 19 files changed, 19 insertions(+) diff --git a/examples/autojoin/Cargo.toml b/examples/autojoin/Cargo.toml index 011bb517d1e..491914db83e 100644 --- a/examples/autojoin/Cargo.toml +++ b/examples/autojoin/Cargo.toml @@ -3,6 +3,7 @@ name = "example-autojoin" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-autojoin" diff --git a/examples/backups/Cargo.toml b/examples/backups/Cargo.toml index 26ff6ff3349..c6660a70004 100644 --- a/examples/backups/Cargo.toml +++ b/examples/backups/Cargo.toml @@ -3,6 +3,7 @@ name = "example-backups" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-backups" diff --git a/examples/command_bot/Cargo.toml b/examples/command_bot/Cargo.toml index 271767ee81b..5b15be635f3 100644 --- a/examples/command_bot/Cargo.toml +++ b/examples/command_bot/Cargo.toml @@ -3,6 +3,7 @@ name = "example-command-bot" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-command-bot" diff --git a/examples/cross_signing_bootstrap/Cargo.toml b/examples/cross_signing_bootstrap/Cargo.toml index 96aa9747db5..1592ec9415b 100644 --- a/examples/cross_signing_bootstrap/Cargo.toml +++ b/examples/cross_signing_bootstrap/Cargo.toml @@ -3,6 +3,7 @@ name = "example-cross-signing-bootstrap" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-cross-signing-bootstrap" diff --git a/examples/custom_events/Cargo.toml b/examples/custom_events/Cargo.toml index 4d78fc5f4ef..27ed2f7ebec 100644 --- a/examples/custom_events/Cargo.toml +++ b/examples/custom_events/Cargo.toml @@ -3,6 +3,7 @@ name = "example-custom-events" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-custom-events" diff --git a/examples/emoji_verification/Cargo.toml b/examples/emoji_verification/Cargo.toml index facea1d0b30..d38c62b74c0 100644 --- a/examples/emoji_verification/Cargo.toml +++ b/examples/emoji_verification/Cargo.toml @@ -3,6 +3,7 @@ name = "example-emoji-verification" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-emoji-verification" diff --git a/examples/get_profiles/Cargo.toml b/examples/get_profiles/Cargo.toml index a0da86992fb..c06cda8361f 100644 --- a/examples/get_profiles/Cargo.toml +++ b/examples/get_profiles/Cargo.toml @@ -3,6 +3,7 @@ name = "example-get-profiles" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-get-profiles" diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml index ffd0b09ee3a..1b913633080 100644 --- a/examples/getting_started/Cargo.toml +++ b/examples/getting_started/Cargo.toml @@ -3,6 +3,7 @@ name = "example-getting-started" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-getting-started" diff --git a/examples/image_bot/Cargo.toml b/examples/image_bot/Cargo.toml index db84d70155e..1ae3bf43227 100644 --- a/examples/image_bot/Cargo.toml +++ b/examples/image_bot/Cargo.toml @@ -3,6 +3,7 @@ name = "example-image-bot" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-image-bot" diff --git a/examples/login/Cargo.toml b/examples/login/Cargo.toml index 789b5efa071..a5cd1623dfe 100644 --- a/examples/login/Cargo.toml +++ b/examples/login/Cargo.toml @@ -3,6 +3,7 @@ name = "example-login" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-login" diff --git a/examples/oidc_cli/Cargo.toml b/examples/oidc_cli/Cargo.toml index 5d4af394921..30e475ca62e 100644 --- a/examples/oidc_cli/Cargo.toml +++ b/examples/oidc_cli/Cargo.toml @@ -3,6 +3,7 @@ name = "example-oidc-cli" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-oidc-cli" diff --git a/examples/persist_session/Cargo.toml b/examples/persist_session/Cargo.toml index 63273840e42..8478f4e7420 100644 --- a/examples/persist_session/Cargo.toml +++ b/examples/persist_session/Cargo.toml @@ -3,6 +3,7 @@ name = "example-persist-session" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-persist-session" diff --git a/examples/qr-login/Cargo.toml b/examples/qr-login/Cargo.toml index 44f2d6be861..445e5384418 100644 --- a/examples/qr-login/Cargo.toml +++ b/examples/qr-login/Cargo.toml @@ -3,6 +3,7 @@ name = "example-qr-login" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-qr-login" diff --git a/examples/secret_storage/Cargo.toml b/examples/secret_storage/Cargo.toml index 5a57c7dc459..0b0466721a1 100644 --- a/examples/secret_storage/Cargo.toml +++ b/examples/secret_storage/Cargo.toml @@ -3,6 +3,7 @@ name = "example-secret-storage" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-secret-storage" diff --git a/examples/timeline/Cargo.toml b/examples/timeline/Cargo.toml index 1da831c3c94..0150f4c7718 100644 --- a/examples/timeline/Cargo.toml +++ b/examples/timeline/Cargo.toml @@ -3,6 +3,7 @@ name = "example-timeline" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "example-timeline" diff --git a/labs/multiverse/Cargo.toml b/labs/multiverse/Cargo.toml index 710ddc97236..29098ea9193 100644 --- a/labs/multiverse/Cargo.toml +++ b/labs/multiverse/Cargo.toml @@ -3,6 +3,7 @@ name = "multiverse" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "multiverse" diff --git a/testing/matrix-sdk-integration-testing/Cargo.toml b/testing/matrix-sdk-integration-testing/Cargo.toml index 83ec826ecd1..12d7fa3ce95 100644 --- a/testing/matrix-sdk-integration-testing/Cargo.toml +++ b/testing/matrix-sdk-integration-testing/Cargo.toml @@ -4,6 +4,7 @@ description = "Internal integration testing for matrix-sdk crate" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [dev-dependencies] anyhow = { workspace = true } diff --git a/uniffi-bindgen/Cargo.toml b/uniffi-bindgen/Cargo.toml index 7b318d6462d..a7a51a7e19d 100644 --- a/uniffi-bindgen/Cargo.toml +++ b/uniffi-bindgen/Cargo.toml @@ -3,6 +3,7 @@ name = "uniffi-bindgen" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [dependencies] uniffi = { workspace = true, features = ["cli"] } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 47d71d2ac4f..94f1bae3055 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -3,6 +3,7 @@ name = "xtask" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" [[bin]] name = "xtask" From a3a0125421ad05fcaab862f944469ccdf265ebe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 4 Oct 2024 13:16:49 +0200 Subject: [PATCH 202/979] chore: Set up cargo-deny --- .deny.toml | 63 +++++++++++++++++++++++++++++++++++++ .github/workflows/audit.yml | 13 -------- .github/workflows/deny.yml | 14 +++++++++ 3 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 .deny.toml delete mode 100644 .github/workflows/audit.yml create mode 100644 .github/workflows/deny.yml diff --git a/.deny.toml b/.deny.toml new file mode 100644 index 00000000000..88b53b451a2 --- /dev/null +++ b/.deny.toml @@ -0,0 +1,63 @@ +# https://embarkstudios.github.io/cargo-deny/checks/cfg.html +[graph] +all-features = true +exclude = [ + # dev only dependency + "criterion" +] + +[advisories] +version = 2 +ignore = [ + { id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" }, + { id = "RUSTSEC-2024-0370", reason = "Waiting for a Aquamarine release" }, +] + +[licenses] +version = 2 +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "ISC", + "MIT", + "MPL-2.0", + "Zlib", +] +exceptions = [ + { allow = ["Unicode-DFS-2016"], crate = "unicode-ident" }, + { allow = ["CDDL-1.0"], crate = "inferno" }, + { allow = ["LicenseRef-ring"], crate = "ring" }, +] + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, +] + +[bans] +# We should disallow this, but it's currently a PITA. +multiple-versions = "allow" +wildcards = "allow" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" + +allow-git = [ + # A patch override for the bindings fixing a bug for Android before upstream + # releases a new version. + "https://github.com/element-hq/tracing.git", + # Sam as for the tracing dependency. + "https://github.com/element-hq/paranoid-android.git", + # Well, it's Ruma. + "https://github.com/ruma/ruma", + # A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10 + "https://github.com/jplatte/const_panic", + # A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22 + "https://github.com/jplatte/async-compat", +] diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml deleted file mode 100644 index a508a769765..00000000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Security audit -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * *' -jobs: - audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.0 - - uses: actions-rust-lang/audit@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deny.yml b/.github/workflows/deny.yml new file mode 100644 index 00000000000..527d89c5115 --- /dev/null +++ b/.github/workflows/deny.yml @@ -0,0 +1,14 @@ +name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities) +on: + pull_request: + paths: + - '**/Cargo.toml' + workflow_dispatch: + schedule: + - cron: '0 0 * * *' +jobs: + cargo-deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 From abf3c6e7b7f932e66b0717be293bab437affd504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 4 Oct 2024 16:41:39 +0200 Subject: [PATCH 203/979] ui: Do not warn when no reaction to redact was found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `handle_reaction_redaction` method is called by `handle_redaction` for every single redaction event that we receive as a first step to check if the redaction matches a reaction. It means that not finding a reaction to redact is perfectly fine and is not worthy of a warning. Signed-off-by: Kévin Commaille --- crates/matrix-sdk-ui/src/timeline/event_handler.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 7bd1ea253fc..b8ac93ad573 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -906,7 +906,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } - warn!("Reaction to redact was missing from the reaction or user map"); false } From 3ce2f16d556ec519116a86fe746967d8627985f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 4 Oct 2024 16:28:40 +0200 Subject: [PATCH 204/979] base: Do not warn when room in m.direct account data is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It can occur that the data contains rooms that were forgotten. Knowing that we now update that data after every sync, that creates a lot of noise in the logs. Signed-off-by: Kévin Commaille --- crates/matrix-sdk-base/src/response_processors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/response_processors.rs b/crates/matrix-sdk-base/src/response_processors.rs index 4fee7ffe87f..a6a35a5446c 100644 --- a/crates/matrix-sdk-base/src/response_processors.rs +++ b/crates/matrix-sdk-base/src/response_processors.rs @@ -41,7 +41,7 @@ fn map_info( f(&mut info); changes.add_room(info); } else { - warn!(room = %room_id, "couldn't find room in state changes or store"); + debug!(room = %room_id, "couldn't find room in state changes or store"); } } From 46856f54af099162d0f17a22a640f3b5bfe4c2cf Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 18:08:47 +0200 Subject: [PATCH 205/979] timeline: get rid of one indent level thanks to a `let else` --- .../src/timeline/event_handler.rs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index b8ac93ad573..3b69c49db59 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -525,28 +525,28 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &mut self, item: &EventTimelineItem, ) -> Option { + let Flow::Remote { event_id, .. } = &self.ctx.flow else { + return None; + }; + let mut find_and_remove_pending = |event_id| { let edits = &mut self.meta.pending_edits; let pos = edits.iter().position(|(prev_event_id, _)| prev_event_id == event_id)?; Some(edits.remove(pos).unwrap().1) }; - if let Flow::Remote { event_id, .. } = &self.ctx.flow { - match item.content() { - TimelineItemContent::Message(..) => { - let pending = find_and_remove_pending(event_id)?; - let edit = as_variant!(pending, PendingEdit::RoomMessage)?; - self.apply_msg_edit(item, edit) - } - TimelineItemContent::Poll(..) => { - let pending = find_and_remove_pending(event_id)?; - let edit = as_variant!(pending, PendingEdit::Poll)?; - self.apply_poll_edit(item, edit) - } - _ => None, + match item.content() { + TimelineItemContent::Message(..) => { + let pending = find_and_remove_pending(event_id)?; + let edit = as_variant!(pending, PendingEdit::RoomMessage)?; + self.apply_msg_edit(item, edit) } - } else { - None + TimelineItemContent::Poll(..) => { + let pending = find_and_remove_pending(event_id)?; + let edit = as_variant!(pending, PendingEdit::Poll)?; + self.apply_poll_edit(item, edit) + } + _ => None, } } From 1434285a1b620f71698b04b035579d919dff603a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 18:43:47 +0200 Subject: [PATCH 206/979] timeline: extract edits from bundled relations and pass an optional edited content to `Message::from_event` No changes in functionality. --- .../timeline/event_item/content/message.rs | 58 +++++++++++-------- .../src/timeline/event_item/content/mod.rs | 7 ++- .../src/timeline/event_item/mod.rs | 9 +-- crates/matrix-sdk-ui/src/timeline/mod.rs | 4 +- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 4948e121497..f57ba34fe5a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -59,27 +59,10 @@ impl Message { /// Construct a `Message` from a `m.room.message` event. pub(in crate::timeline) fn from_event( c: RoomMessageEventContent, - relations: BundledMessageLikeRelations, + edit: Option, timeline_items: &Vector>, ) -> Self { - let edited = relations.has_replacement(); - let edit = relations.replace.and_then(|r| match *r { - AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev - .content - .relates_to - { - Some(Relation::Replacement(re)) => Some(re), - _ => { - error!("got m.room.message event with an edit without a valid m.replace relation"); - None - } - }, - AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None, - _ => { - error!("got m.room.message event with an edit of a different event type"); - None - } - }); + let edited = edit.is_some(); let mut thread_root = None; let in_reply_to = c.relates_to.and_then(|relation| match relation { @@ -98,8 +81,8 @@ impl Message { let (msgtype, mentions) = match edit { Some(mut e) => { // Edit's content is never supposed to contain the reply fallback. - e.new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); - (e.new_content.msgtype, e.new_content.mentions) + e.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); + (e.msgtype, e.mentions) } None => { let remove_reply_fallback = if in_reply_to.is_some() { @@ -178,6 +161,32 @@ impl From for RoomMessageEventContent { } } +/// Extracts a replacement for a room message, if present in the bundled +/// relations. +pub(crate) fn extract_edit_content( + relations: BundledMessageLikeRelations, +) -> Option { + match *relations.replace? { + AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev + .content + .relates_to + { + Some(Relation::Replacement(re)) => Some(re.new_content), + _ => { + error!("got m.room.message event with an edit without a valid m.replace relation"); + None + } + }, + + AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None, + + _ => { + error!("got m.room.message event with an edit of a different event type"); + None + } + } +} + /// Turn a pair of thread root ID and in-reply-to ID as stored in [`Message`] /// back into a [`Relation`]. /// @@ -308,8 +317,11 @@ impl RepliedToEvent { return Err(TimelineError::UnsupportedEvent); }; - let content = - TimelineItemContent::Message(Message::from_event(c, event.relations(), &vector![])); + let content = TimelineItemContent::Message(Message::from_event( + c, + extract_edit_content(event.relations()), + &vector![], + )); let sender = event.sender().to_owned(); let sender_profile = TimelineDetails::from_initial_value( room_data_provider.profile_from_user_id(&sender).await, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index e533fc61d53..e0febdf5560 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -62,6 +62,7 @@ pub(crate) mod pinned_events; pub use pinned_events::RoomPinnedEventsChange; +pub(crate) use self::message::extract_edit_content; pub use self::message::{InReplyToDetails, Message, RepliedToEvent}; /// The content of an [`EventTimelineItem`][super::EventTimelineItem]. @@ -176,7 +177,7 @@ impl TimelineItemContent { // 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(); + let edit = None; // 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, @@ -186,7 +187,7 @@ impl TimelineItemContent { let timeline_items = Vector::new(); TimelineItemContent::Message(Message::from_event( event_content, - relations, + edit, &timeline_items, )) } @@ -264,7 +265,7 @@ impl TimelineItemContent { relations: BundledMessageLikeRelations, timeline_items: &Vector>, ) -> Self { - Self::Message(Message::from_event(c, relations, timeline_items)) + Self::Message(Message::from_event(c, extract_edit_content(relations), timeline_items)) } #[cfg(not(tarpaulin_include))] // debug-logging functionality 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 79a23ee5024..dd55e5b77c7 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -41,6 +41,11 @@ mod content; mod local; mod remote; +pub(super) use self::{ + content::extract_edit_content, + local::LocalEventTimelineItem, + remote::{RemoteEventOrigin, RemoteEventTimelineItem}, +}; pub use self::{ content::{ AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange, @@ -49,10 +54,6 @@ pub use self::{ }, local::EventSendState, }; -pub(super) use self::{ - local::LocalEventTimelineItem, - remote::{RemoteEventOrigin, RemoteEventTimelineItem}, -}; use super::{RepliedToInfo, ReplyContent, UnsupportedReplyItem}; /// An item in the timeline that represents at least one event. diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index d45b90acb4d..48ef57110e6 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -18,7 +18,7 @@ use std::{path::PathBuf, pin::Pin, sync::Arc, task::Poll}; -use event_item::{EventTimelineItemKind, TimelineItemHandle}; +use event_item::{extract_edit_content, EventTimelineItemKind, TimelineItemHandle}; use eyeball_im::VectorDiff; use futures_core::Stream; use imbl::Vector; @@ -433,7 +433,7 @@ impl Timeline { { ReplyContent::Message(Message::from_event( original_message.content.clone(), - message_like_event.relations(), + extract_edit_content(message_like_event.relations()), &self.items().await, )) } else { From efbf9472f2004a14cb92624f0d92808046435970 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 1 Oct 2024 18:55:23 +0200 Subject: [PATCH 207/979] latest event: consider bundled edits when constructing an event item from a latest event --- crates/matrix-sdk-base/src/latest_event.rs | 3 +- .../timeline/event_item/content/message.rs | 1 + .../src/timeline/event_item/content/mod.rs | 20 ++++-- .../src/timeline/event_item/mod.rs | 71 +++++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 6904fe811f4..0799a296bc8 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -67,9 +67,8 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat if is_replacement { return PossibleLatestEvent::NoUnsupportedMessageLikeType; - } else { - return PossibleLatestEvent::YesRoomMessage(message); } + return PossibleLatestEvent::YesRoomMessage(message); } return PossibleLatestEvent::YesRoomMessage(message); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index f57ba34fe5a..3d11bb9daae 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -84,6 +84,7 @@ impl Message { e.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); (e.msgtype, e.mentions) } + None => { let remove_reply_fallback = if in_reply_to.is_some() { RemoveReplyFallback::Yes diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index e0febdf5560..458fe9d8f04 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -37,7 +37,7 @@ use ruma::{ history_visibility::RoomHistoryVisibilityEventContent, join_rules::RoomJoinRulesEventContent, member::{Change, RoomMemberEventContent}, - message::{RoomMessageEventContent, SyncRoomMessageEvent}, + message::{Relation, RoomMessageEventContent, SyncRoomMessageEvent}, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, power_levels::RoomPowerLevelsEventContent, @@ -174,10 +174,19 @@ impl TimelineItemContent { // Grab the content of this event let event_content = event.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 edit = None; + // Feed the bundled edit, if present, or we might miss showing edited content. + let edit = event + .unsigned + .relations + .replace + .as_ref() + .and_then(|boxed| match &boxed.content.relates_to { + Some(Relation::Replacement(re)) => Some(re.new_content.clone()), + _ => { + warn!("got m.room.message event with an edit without a valid m.replace relation"); + None + } + }); // 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, @@ -191,6 +200,7 @@ impl TimelineItemContent { &timeline_items, )) } + SyncRoomMessageEvent::Redacted(_) => TimelineItemContent::RedactedMessage, } } 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 dd55e5b77c7..475f5e23f09 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -767,6 +767,77 @@ mod tests { } } + #[async_test] + async fn test_latest_message_includes_bundled_edit() { + // Given a sync event that is suitable to be used as a latest_event, and + // contains a bundled edit, + let room_id = room_id!("!q:x.uk"); + let user_id = user_id!("@t:o.uk"); + let event = sync_timeline_event!({ + "event_id": "$eventid6", + "sender": user_id, + "origin_server_ts": 42, + "type": "m.room.message", + "room_id": room_id, + "content": { + "body": "**My M**", + "format": "org.matrix.custom.html", + "formatted_body": "My M" , + "msgtype": "m.text" + }, + "unsigned": { + "m.relations": { + "m.replace" : { + "sender" : user_id, + "content" : { + "format" : "org.matrix.custom.html", + "formatted_body" : " * Updated!", + "m.relates_to" : { + "event_id" : "$eventid6", + "rel_type" : "m.replace" + }, + "m.new_content": { + "body" : "Updated!", + "formatted_body" : "Updated!", + "msgtype" : "m.text", + "format" : "org.matrix.custom.html" + }, + "msgtype" : "m.text", + "body" : " * Updated!" + }, + "origin_server_ts" : 43, + "room_id" : room_id, + "event_id" : "$edit-event-id", + "user_id" : user_id, + "type" : "m.room.message", + } + } + } + }) + .into(); + + let client = logged_in_client(None).await; + + // When we construct a timeline event from it, + let timeline_item = + EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) + .await + .unwrap(); + + // 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(42).unwrap()); + if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() { + assert_eq!(txt.body, "Updated!"); + let formatted = txt.formatted.as_ref().unwrap(); + assert_eq!(formatted.format, MessageFormat::Html); + assert_eq!(formatted.body, "Updated!"); + } else { + panic!("Unexpected message type"); + } + } + #[async_test] async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage( ) { From 45968b2a2bcb94f804bd00cf758bb4aab00bc5c8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 11:17:40 +0200 Subject: [PATCH 208/979] timeline: prefer a bundled edit to a pending edit when adding a new message --- .../src/timeline/controller/mod.rs | 7 +- .../src/timeline/event_handler.rs | 30 +++++++- .../src/timeline/event_item/content/mod.rs | 13 ++-- .../matrix-sdk-ui/src/timeline/tests/edit.rs | 71 ++++++++++++++++++- 4 files changed, 109 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 383d18e8114..b884d8af1ef 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -947,10 +947,9 @@ impl TimelineController

{ // Replace the local-related state (kind) and the content state. let new_item = TimelineItem::new( - prev_item.with_kind(ti_kind).with_content( - TimelineItemContent::message(content, Default::default(), &txn.items), - None, - ), + prev_item + .with_kind(ti_kind) + .with_content(TimelineItemContent::message(content, None, &txn.items), None), prev_item.internal_id.to_owned(), ); diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 3b69c49db59..fd776939343 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -51,7 +51,7 @@ use super::{ controller::{TimelineMetadata, TimelineStateTransaction}, day_dividers::DayDividerAdjuster, event_item::{ - AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, + extract_edit_content, AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, LocalEventTimelineItem, Profile, ReactionsByKeyBySender, RemoteEventOrigin, RemoteEventTimelineItem, TimelineEventItemId, }, @@ -328,7 +328,33 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::RoomMessage(c) => { if should_add { - self.add_item(TimelineItemContent::message(c, relations, self.items)); + // Always remove the pending edit, if there's any. The reason is that if + // there's an edit in the relations mapping, we want to prefer it over any + // other pending edit, since it's more likely to be up to date, and we + // don't want to apply another pending edit on top of it. + let pending_edit = if let Flow::Remote { event_id, .. } = &self.ctx.flow { + let edits = &mut self.meta.pending_edits; + edits + .iter() + .position(|(prev_event_id, _)| prev_event_id == event_id) + .into_iter() + .filter_map(|pos| { + Some( + as_variant!( + edits.remove(pos).unwrap().1, + PendingEdit::RoomMessage + )? + .new_content, + ) + }) + .next() + } else { + None + }; + + let edit = extract_edit_content(relations).or(pending_edit); + + self.add_item(TimelineItemContent::message(c, edit, self.items)); } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 458fe9d8f04..6260057b721 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -37,7 +37,10 @@ use ruma::{ history_visibility::RoomHistoryVisibilityEventContent, join_rules::RoomJoinRulesEventContent, member::{Change, RoomMemberEventContent}, - message::{Relation, RoomMessageEventContent, SyncRoomMessageEvent}, + message::{ + Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, + SyncRoomMessageEvent, + }, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, power_levels::RoomPowerLevelsEventContent, @@ -48,8 +51,8 @@ use ruma::{ }, space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, sticker::{StickerEventContent, SyncStickerEvent}, - AnyFullStateEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, - BundledMessageLikeRelations, FullStateEventContent, MessageLikeEventType, StateEventType, + AnyFullStateEventContent, AnySyncTimelineEvent, FullStateEventContent, + MessageLikeEventType, StateEventType, }, OwnedDeviceId, OwnedMxcUri, OwnedUserId, RoomVersionId, UserId, }; @@ -272,10 +275,10 @@ impl TimelineItemContent { // allow users to call them directly, which should not be supported pub(crate) fn message( c: RoomMessageEventContent, - relations: BundledMessageLikeRelations, + edit: Option, timeline_items: &Vector>, ) -> Self { - Self::Message(Message::from_event(c, extract_edit_content(relations), timeline_items)) + Self::Message(Message::from_event(c, edit, timeline_items)) } #[cfg(not(tarpaulin_include))] // debug-logging functionality diff --git a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs index f711bedd5e5..33fa44f024a 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs @@ -25,7 +25,7 @@ use ruma::{ events::room::message::{MessageType, RedactedRoomMessageEventContent}, server_name, EventId, }; -use stream_assert::assert_next_matches; +use stream_assert::{assert_next_matches, assert_pending}; use super::TestTimeline; use crate::timeline::TimelineItemContent; @@ -222,3 +222,72 @@ async fn test_edit_updates_encryption_info() { assert_let!(MessageType::Text(text) = message.msgtype()); assert_eq!(text.body, "!!edited!! **better** message"); } + +#[async_test] +async fn test_relations_edit_overrides_pending_edit() { + let timeline = TestTimeline::new(); + let mut stream = timeline.subscribe().await; + + let f = &timeline.factory; + + let original_event_id = event_id!("$original"); + let edit1_event_id = event_id!("$edit1"); + let edit2_event_id = event_id!("$edit2"); + + // Pending edit is stashed, nothing comes from the stream. + timeline + .handle_live_event( + f.text_msg("*edit 1") + .sender(*ALICE) + .edit(original_event_id, MessageType::text_plain("edit 1").into()) + .event_id(edit1_event_id), + ) + .await; + assert_pending!(stream); + + // Now we receive the original event, with a bundled relations group. + let ev = sync_timeline_event!({ + "content": { + "body": "original", + "msgtype": "m.text" + }, + "event_id": &original_event_id, + "origin_server_ts": timeline.event_builder.next_server_ts(), + "sender": *ALICE, + "type": "m.room.message", + "unsigned": { + "m.relations": { + "m.replace": { + "content": { + "body": "* edit 2", + "m.new_content": { + "body": "edit 2", + "msgtype": "m.text" + }, + "m.relates_to": { + "event_id": original_event_id, + "rel_type": "m.replace" + }, + "msgtype": "m.text" + }, + "event_id": edit2_event_id, + "origin_server_ts": timeline.event_builder.next_server_ts(), + "sender": *ALICE, + "type": "m.room.message", + } + } + } + }); + timeline.handle_live_event(ev).await; + + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + + // We receive the latest edit, not the pending one. + let text = item.as_event().unwrap().content().as_message().unwrap(); + assert_eq!(text.body(), "edit 2"); + + let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(day_divider.is_day_divider()); + + assert_pending!(stream); +} From 8a71ac622dba97c26bf2fbbb1d3c276977695b19 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 16:00:35 +0200 Subject: [PATCH 209/979] tests: allow bundled relations in EventBuidler --- .../src/timeline/event_item/mod.rs | 73 ++++------- .../matrix-sdk-ui/src/timeline/tests/edit.rs | 124 +++++++----------- crates/matrix-sdk/src/test_utils/events.rs | 40 +++++- 3 files changed, 113 insertions(+), 124 deletions(-) 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 475f5e23f09..aa0ecbc54a2 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -716,23 +716,24 @@ impl ReactionsByKeyBySender { mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; - use matrix_sdk::test_utils::logged_in_client; + use matrix_sdk::test_utils::{events::EventFactory, logged_in_client}; use matrix_sdk_base::{ deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http, MinimalStateEvent, OriginalMinimalStateEvent, }; use matrix_sdk_test::{async_test, sync_timeline_event}; use ruma::{ + event_id, events::{ room::{ member::RoomMemberEventContent, message::{MessageFormat, MessageType}, }, - AnySyncTimelineEvent, + AnySyncTimelineEvent, BundledMessageLikeRelations, }, room_id, serde::Raw, - user_id, RoomId, UInt, UserId, + uint, user_id, MilliSecondsSinceUnixEpoch, RoomId, UInt, UserId, }; use super::{EventTimelineItem, Profile}; @@ -773,48 +774,30 @@ mod tests { // contains a bundled edit, let room_id = room_id!("!q:x.uk"); let user_id = user_id!("@t:o.uk"); - let event = sync_timeline_event!({ - "event_id": "$eventid6", - "sender": user_id, - "origin_server_ts": 42, - "type": "m.room.message", - "room_id": room_id, - "content": { - "body": "**My M**", - "format": "org.matrix.custom.html", - "formatted_body": "My M" , - "msgtype": "m.text" - }, - "unsigned": { - "m.relations": { - "m.replace" : { - "sender" : user_id, - "content" : { - "format" : "org.matrix.custom.html", - "formatted_body" : " * Updated!", - "m.relates_to" : { - "event_id" : "$eventid6", - "rel_type" : "m.replace" - }, - "m.new_content": { - "body" : "Updated!", - "formatted_body" : "Updated!", - "msgtype" : "m.text", - "format" : "org.matrix.custom.html" - }, - "msgtype" : "m.text", - "body" : " * Updated!" - }, - "origin_server_ts" : 43, - "room_id" : room_id, - "event_id" : "$edit-event-id", - "user_id" : user_id, - "type" : "m.room.message", - } - } - } - }) - .into(); + + let f = EventFactory::new(); + + let original_event_id = event_id!("$original"); + + let mut relations = BundledMessageLikeRelations::new(); + relations.replace = Some(Box::new( + f.text_html(" * Updated!", " * Updated!") + .edit( + original_event_id, + MessageType::text_html("Updated!", "Updated!").into(), + ) + .event_id(event_id!("$edit")) + .sender(user_id) + .into_raw_sync(), + )); + + let event = f + .text_html("**My M**", "My M") + .sender(user_id) + .event_id(original_event_id) + .bundled_relations(relations) + .server_ts(MilliSecondsSinceUnixEpoch(uint!(42))) + .into_sync(); let client = logged_in_client(None).await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs index 33fa44f024a..ddfcdead893 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs @@ -19,11 +19,13 @@ use eyeball_im::VectorDiff; use matrix_sdk::deserialized_responses::{ AlgorithmInfo, EncryptionInfo, VerificationLevel, VerificationState, }; -use matrix_sdk_test::{async_test, sync_timeline_event, ALICE}; +use matrix_sdk_test::{async_test, ALICE}; use ruma::{ event_id, - events::room::message::{MessageType, RedactedRoomMessageEventContent}, - server_name, EventId, + events::{ + room::message::{MessageType, RedactedRoomMessageEventContent}, + BundledMessageLikeRelations, + }, }; use stream_assert::{assert_next_matches, assert_pending}; @@ -108,45 +110,36 @@ async fn test_aggregated_sanitized() { let timeline = TestTimeline::new(); let mut stream = timeline.subscribe().await; - let original_event_id = EventId::new(server_name!("dummy.server")); - let ev = sync_timeline_event!({ - "content": { - "formatted_body": "original message", - "format": "org.matrix.custom.html", - "body": "**original** message", - "msgtype": "m.text" - }, - "event_id": &original_event_id, - "origin_server_ts": timeline.event_builder.next_server_ts(), - "sender": *ALICE, - "type": "m.room.message", - "unsigned": { - "m.relations": { - "m.replace": { - "content": { - "formatted_body": "* better message", - "format": "org.matrix.custom.html", - "body": "* !!edited!! **better** message", - "m.new_content": { - "formatted_body": " better message", - "format": "org.matrix.custom.html", - "body": "!!edited!! **better** message", - "msgtype": "m.text" - }, - "m.relates_to": { - "event_id": original_event_id, - "rel_type": "m.replace" - }, - "msgtype": "m.text" - }, - "event_id": EventId::new(server_name!("dummy.server")), - "origin_server_ts": timeline.event_builder.next_server_ts(), - "sender": *ALICE, - "type": "m.room.message", - } - } - } - }); + let original_event_id = event_id!("$original"); + let edit_event_id = event_id!("$edit"); + + let f = &timeline.factory; + + let mut relations = BundledMessageLikeRelations::new(); + relations.replace = Some(Box::new( + f.text_html( + "* !!edited!! **better** message", + "* better message", + ) + .edit( + original_event_id, + MessageType::text_html( + "!!edited!! **better** message", + " better message", + ) + .into(), + ) + .event_id(edit_event_id) + .sender(*ALICE) + .into_raw_sync(), + )); + + let ev = f + .text_html("**original** message", "original message") + .sender(*ALICE) + .event_id(original_event_id) + .bundled_relations(relations); + timeline.handle_live_event(ev).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -246,38 +239,21 @@ async fn test_relations_edit_overrides_pending_edit() { assert_pending!(stream); // Now we receive the original event, with a bundled relations group. - let ev = sync_timeline_event!({ - "content": { - "body": "original", - "msgtype": "m.text" - }, - "event_id": &original_event_id, - "origin_server_ts": timeline.event_builder.next_server_ts(), - "sender": *ALICE, - "type": "m.room.message", - "unsigned": { - "m.relations": { - "m.replace": { - "content": { - "body": "* edit 2", - "m.new_content": { - "body": "edit 2", - "msgtype": "m.text" - }, - "m.relates_to": { - "event_id": original_event_id, - "rel_type": "m.replace" - }, - "msgtype": "m.text" - }, - "event_id": edit2_event_id, - "origin_server_ts": timeline.event_builder.next_server_ts(), - "sender": *ALICE, - "type": "m.room.message", - } - } - } - }); + let mut relations = BundledMessageLikeRelations::new(); + relations.replace = Some(Box::new( + f.text_msg("* edit 2") + .edit(original_event_id, MessageType::text_plain("edit 2").into()) + .event_id(edit2_event_id) + .sender(*ALICE) + .into_raw_sync(), + )); + + let ev = f + .text_msg("original") + .sender(*ALICE) + .event_id(original_event_id) + .bundled_relations(relations); + timeline.handle_live_event(ev).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index ce7b0bb5f1f..260e210b438 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -34,7 +34,7 @@ use ruma::{ message::{Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, redaction::RoomRedactionEventContent, }, - AnySyncTimelineEvent, AnyTimelineEvent, EventContent, + AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, EventContent, }, serde::Raw, server_name, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, @@ -43,18 +43,19 @@ use ruma::{ use serde::Serialize; use serde_json::json; -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default)] struct Unsigned { transaction_id: Option, + relations: Option>>, } #[derive(Debug)] -pub struct EventBuilder { +pub struct EventBuilder { sender: Option, room: Option, event_id: Option, redacts: Option, - content: E, + content: C, server_ts: MilliSecondsSinceUnixEpoch, unsigned: Option, state_key: Option, @@ -90,6 +91,21 @@ where self } + /// Adds bundled relations to this event. + /// + /// Ideally, we'd type-check that an event passed as a relation is the same + /// type as this one, but it's not trivial to do so because this builder + /// is only generic on the event's *content*, not the event type itself; + /// doing so would require many changes, and this is testing code after + /// all. + pub fn bundled_relations( + mut self, + relations: BundledMessageLikeRelations>, + ) -> Self { + self.unsigned.get_or_insert_with(Default::default).relations = Some(relations); + self + } + pub fn state_key(mut self, state_key: impl Into) -> Self { self.state_key = Some(state_key.into()); self @@ -122,9 +138,23 @@ where if let Some(redacts) = self.redacts { map.insert("redacts".to_owned(), json!(redacts)); } + if let Some(unsigned) = self.unsigned { - map.insert("unsigned".to_owned(), json!(unsigned)); + let mut unsigned_json = json!({}); + + // We can't plain serialize `Unsigned`, otherwise this would result in some + // `null` fields when options are set to none, which Ruma rejects. + let unsigned_obj = unsigned_json.as_object_mut().unwrap(); + if let Some(transaction_id) = unsigned.transaction_id { + unsigned_obj.insert("transaction_id".to_owned(), json!(transaction_id)); + } + if let Some(relations) = unsigned.relations { + unsigned_obj.insert("m.relations".to_owned(), json!(relations)); + } + + map.insert("unsigned".to_owned(), unsigned_json); } + if let Some(state_key) = self.state_key { map.insert("state_key".to_owned(), json!(state_key)); } From 157499955a3a9684b892aae41e25ead025e80ef7 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 16:13:44 +0200 Subject: [PATCH 210/979] tests: allow passing an u64 to `EventBuilder::server_ts` --- .../src/timeline/event_item/mod.rs | 4 ++-- .../matrix-sdk-ui/src/timeline/tests/echo.rs | 16 ++++---------- .../src/timeline/tests/reactions.rs | 2 +- .../tests/integration/timeline/echo.rs | 4 ++-- crates/matrix-sdk/src/test_utils/events.rs | 22 ++++++++++++++++--- 5 files changed, 28 insertions(+), 20 deletions(-) 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 aa0ecbc54a2..7436f96e49e 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -733,7 +733,7 @@ mod tests { }, room_id, serde::Raw, - uint, user_id, MilliSecondsSinceUnixEpoch, RoomId, UInt, UserId, + user_id, RoomId, UInt, UserId, }; use super::{EventTimelineItem, Profile}; @@ -796,7 +796,7 @@ mod tests { .sender(user_id) .event_id(original_event_id) .bundled_relations(relations) - .server_ts(MilliSecondsSinceUnixEpoch(uint!(42))) + .server_ts(42) .into_sync(); let client = logged_in_client(None).await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index ea3a3f77399..d8d8b56f6d8 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -24,7 +24,7 @@ use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::{ event_id, events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, - uint, user_id, MilliSecondsSinceUnixEpoch, + user_id, MilliSecondsSinceUnixEpoch, }; use stream_assert::assert_next_matches; @@ -164,7 +164,7 @@ async fn test_remote_echo_new_position() { f.text_msg("echo") .sender(*ALICE) .event_id(event_id!("$eeG0HA0FAZ37wP8kXlNkxx3I")) - .server_ts(MilliSecondsSinceUnixEpoch(uint!(6))) + .server_ts(6) .unsigned_transaction_id(&txn_id), ) .await; @@ -202,11 +202,7 @@ async fn test_day_divider_duplication() { // … when the second remote event is re-received (day still the same) let event_id = items[2].as_event().unwrap().event_id().unwrap(); - timeline - .handle_live_event( - f.text_msg("B").event_id(event_id).server_ts(MilliSecondsSinceUnixEpoch(uint!(1))), - ) - .await; + timeline.handle_live_event(f.text_msg("B").event_id(event_id).server_ts(1)).await; // … it should not impact the day dividers. let items = timeline.controller.items().await; @@ -225,11 +221,7 @@ async fn test_day_divider_removed_after_local_echo_disappeared() { let f = &timeline.factory; timeline - .handle_live_event( - f.text_msg("remote echo") - .sender(user_id!("@a:b.c")) - .server_ts(MilliSecondsSinceUnixEpoch(0.try_into().unwrap())), - ) + .handle_live_event(f.text_msg("remote echo").sender(user_id!("@a:b.c")).server_ts(0)) .await; let items = timeline.controller.items().await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs index cd31fbf1bf9..2321563c107 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs @@ -209,7 +209,7 @@ async fn test_initial_reaction_timestamp_is_stored() { let reactions = items.last().unwrap().as_event().unwrap().reactions(); let entry = reactions.get(&REACTION_KEY.to_owned()).unwrap(); - assert_eq!(reaction_timestamp, entry.values().next().unwrap().timestamp); + assert_eq!(entry.values().next().unwrap().timestamp, reaction_timestamp); } /// Returns the unique item id, the event id, and position of the message. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs index e0f3203630d..03047fa6a45 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs @@ -107,7 +107,7 @@ async fn test_echo() { f.text_msg("Hello, World!") .sender(user_id!("@example:localhost")) .event_id(event_id!("$7at8sd:localhost")) - .server_ts(MilliSecondsSinceUnixEpoch(uint!(152038280))) + .server_ts(152038280) .unsigned_transaction_id(txn_id), ), ); @@ -262,7 +262,7 @@ async fn test_dedup_by_event_id_late() { f.text_msg("Hello, World!") .sender(user_id!("@example:localhost")) .event_id(event_id) - .server_ts(MilliSecondsSinceUnixEpoch(uint!(123456))), + .server_ts(123456), ), ); diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index 260e210b438..03bc123670c 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -38,11 +38,27 @@ use ruma::{ }, serde::Raw, server_name, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, - OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, + OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId, }; use serde::Serialize; use serde_json::json; +pub trait TimestampArg { + fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch; +} + +impl TimestampArg for MilliSecondsSinceUnixEpoch { + fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch { + self + } +} + +impl TimestampArg for u64 { + fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch { + MilliSecondsSinceUnixEpoch(UInt::try_from(self).unwrap()) + } +} + #[derive(Debug, Default)] struct Unsigned { transaction_id: Option, @@ -80,8 +96,8 @@ where self } - pub fn server_ts(mut self, ts: MilliSecondsSinceUnixEpoch) -> Self { - self.server_ts = ts; + pub fn server_ts(mut self, ts: impl TimestampArg) -> Self { + self.server_ts = ts.to_milliseconds_since_unix_epoch(); self } From 56ccda4dedb55988ddc4482d6bfb0a3c7f7f3d72 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 16:20:12 +0200 Subject: [PATCH 211/979] timeline: apply Message edits in a single place --- .../src/timeline/event_handler.rs | 24 +++-------- .../timeline/event_item/content/message.rs | 40 +++++++++---------- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index fd776939343..83c45c71f22 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -41,7 +41,6 @@ use ruma::{ AnySyncTimelineEvent, BundledMessageLikeRelations, EventContent, FullStateEventContent, MessageLikeEventType, StateEventType, SyncStateEvent, }, - html::RemoveReplyFallback, serde::Raw, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomVersionId, }; @@ -58,8 +57,8 @@ use super::{ polls::PollState, reactions::FullReactionKey, util::{rfind_event_by_id, rfind_event_item}, - EventTimelineItem, InReplyToDetails, Message, OtherState, Sticker, TimelineDetails, - TimelineItem, TimelineItemContent, + EventTimelineItem, InReplyToDetails, OtherState, Sticker, TimelineDetails, TimelineItem, + TimelineItemContent, }; use crate::{ events::SyncTimelineEventWithoutContent, @@ -68,7 +67,6 @@ use crate::{ event_item::{ReactionInfo, ReactionStatus}, reactions::PendingReaction, }, - DEFAULT_SANITIZER_MODE, }; /// When adding an event, useful information related to the source of the event. @@ -601,25 +599,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return None; }; - let mut msgtype = replacement.new_content.msgtype; - - // Edit's content is never supposed to contain the reply fallback. - msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); - - let new_content = TimelineItemContent::Message(Message { - msgtype, - in_reply_to: msg.in_reply_to.clone(), - thread_root: msg.thread_root.clone(), - edited: true, - mentions: replacement.new_content.mentions, - }); - let edit_json = match &self.ctx.flow { Flow::Local { .. } => None, Flow::Remote { raw_event, .. } => Some(raw_event.clone()), }; - let mut new_item = item.with_content(new_content, edit_json); + let mut new_msg = msg.clone(); + new_msg.apply_edit(replacement.new_content); + + let mut new_item = item.with_content(TimelineItemContent::Message(new_msg), edit_json); if let EventTimelineItemKind::Remote(remote_event) = &item.kind { if let Flow::Remote { encryption_info, .. } = &self.ctx.flow { diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 3d11bb9daae..6d04d72cf00 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -62,8 +62,6 @@ impl Message { edit: Option, timeline_items: &Vector>, ) -> Self { - let edited = edit.is_some(); - let mut thread_root = None; let in_reply_to = c.relates_to.and_then(|relation| match relation { Relation::Reply { in_reply_to } => { @@ -78,27 +76,29 @@ impl Message { _ => None, }); - let (msgtype, mentions) = match edit { - Some(mut e) => { - // Edit's content is never supposed to contain the reply fallback. - e.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); - (e.msgtype, e.mentions) - } + let remove_reply_fallback = + if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No }; - None => { - let remove_reply_fallback = if in_reply_to.is_some() { - RemoveReplyFallback::Yes - } else { - RemoveReplyFallback::No - }; + let mut msgtype = c.msgtype; + msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback); - let mut msgtype = c.msgtype; - msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback); - (msgtype, c.mentions) - } - }; + let mut ret = + Self { msgtype, in_reply_to, thread_root, edited: false, mentions: c.mentions }; + + if let Some(edit) = edit { + ret.apply_edit(edit); + } + + ret + } - Self { msgtype, in_reply_to, thread_root, edited, mentions } + /// Apply an edit to the current message. + pub(crate) fn apply_edit(&mut self, mut new_content: RoomMessageEventContentWithoutRelation) { + // Edit's content is never supposed to contain the reply fallback. + new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); + self.msgtype = new_content.msgtype; + self.mentions = new_content.mentions; + self.edited = true; } /// Get the `msgtype`-specific data of this message. From d403bf343138c0c9f1ea0cf0c26f699e0168ed0b Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 16:36:25 +0200 Subject: [PATCH 212/979] timeline(code motion): move the poll code to other files The content of a poll timeline item goes to the content directory. The data structure handling pending poll events goes into state. No functional changes, only code motion. --- .../src/timeline/controller/state.rs | 65 ++++++++++++- .../src/timeline/event_handler.rs | 3 +- .../src/timeline/event_item/content/mod.rs | 10 +- .../{ => event_item/content}/polls.rs | 92 ++++++------------- .../src/timeline/event_item/mod.rs | 6 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 6 +- .../matrix-sdk-ui/src/timeline/tests/polls.rs | 2 +- 7 files changed, 101 insertions(+), 83 deletions(-) rename crates/matrix-sdk-ui/src/timeline/{ => event_item/content}/polls.rs (65%) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 7acef444b2f..371c0b356cb 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::{ - collections::VecDeque, + collections::{HashMap, VecDeque}, future::Future, num::NonZeroUsize, sync::{Arc, RwLock}, @@ -29,8 +29,12 @@ use matrix_sdk_base::deserialized_responses::TimelineEvent; use ruma::events::receipt::ReceiptEventContent; use ruma::{ events::{ - poll::unstable_start::NewUnstablePollStartEventContentWithoutRelation, - relation::Replacement, room::message::RoomMessageEventContentWithoutRelation, + poll::{ + unstable_response::UnstablePollResponseEventContent, + unstable_start::NewUnstablePollStartEventContentWithoutRelation, + }, + relation::Replacement, + room::message::RoomMessageEventContentWithoutRelation, AnySyncEphemeralRoomEvent, }, push::Action, @@ -49,8 +53,7 @@ use crate::{ Flow, HandleEventResult, TimelineEventContext, TimelineEventHandler, TimelineEventKind, TimelineItemPosition, }, - event_item::RemoteEventOrigin, - polls::PendingPollEvents, + event_item::{PollState, RemoteEventOrigin, ResponseData}, reactions::Reactions, read_receipts::ReadReceipts, traits::RoomDataProvider, @@ -755,6 +758,58 @@ impl TimelineStateTransaction<'_> { } } +/// Cache holding poll response and end events handled before their poll start +/// event has been handled. +#[derive(Clone, Debug, Default)] +pub(in crate::timeline) struct PendingPollEvents { + /// Responses to a poll (identified by the poll's start event id). + responses: HashMap>, + + /// Mapping of a poll (identified by its start event's id) to its end date. + end_dates: HashMap, +} + +impl PendingPollEvents { + pub(crate) fn add_response( + &mut self, + start_event_id: &EventId, + sender: &UserId, + timestamp: MilliSecondsSinceUnixEpoch, + content: &UnstablePollResponseEventContent, + ) { + self.responses.entry(start_event_id.to_owned()).or_default().push(ResponseData { + sender: sender.to_owned(), + timestamp, + answers: content.poll_response.answers.clone(), + }); + } + + pub(crate) fn clear(&mut self) { + self.end_dates.clear(); + self.responses.clear(); + } + + /// Mark a poll as finished by inserting its poll date. + pub(crate) fn mark_as_ended( + &mut self, + start_event_id: &EventId, + timestamp: MilliSecondsSinceUnixEpoch, + ) { + self.end_dates.insert(start_event_id.to_owned(), timestamp); + } + + /// Dumps all response and end events present in the cache that belong to + /// the given start_event_id into the given poll_state. + pub(crate) fn apply_pending(&mut self, start_event_id: &EventId, poll_state: &mut PollState) { + if let Some(pending_responses) = self.responses.remove(start_event_id) { + poll_state.response_data.extend(pending_responses); + } + if let Some(pending_end) = self.end_dates.remove(start_event_id) { + poll_state.end_event_timestamp = Some(pending_end); + } + } +} + #[derive(Clone)] pub(in crate::timeline) enum PendingEdit { RoomMessage(Replacement), diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 83c45c71f22..06a9cfc58d2 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -51,10 +51,9 @@ use super::{ day_dividers::DayDividerAdjuster, event_item::{ extract_edit_content, AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, - LocalEventTimelineItem, Profile, ReactionsByKeyBySender, RemoteEventOrigin, + LocalEventTimelineItem, PollState, Profile, ReactionsByKeyBySender, RemoteEventOrigin, RemoteEventTimelineItem, TimelineEventItemId, }, - polls::PollState, reactions::FullReactionKey, util::{rfind_event_by_id, rfind_event_item}, EventTimelineItem, InReplyToDetails, OtherState, Sticker, TimelineDetails, TimelineItem, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 6260057b721..f76a7238f67 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -58,15 +58,19 @@ use ruma::{ }; use tracing::warn; -use crate::timeline::{polls::PollState, TimelineItem}; +use crate::timeline::TimelineItem; mod message; pub(crate) mod pinned_events; +mod polls; pub use pinned_events::RoomPinnedEventsChange; -pub(crate) use self::message::extract_edit_content; -pub use self::message::{InReplyToDetails, Message, RepliedToEvent}; +pub(in crate::timeline) use self::{message::extract_edit_content, polls::ResponseData}; +pub use self::{ + message::{InReplyToDetails, Message, RepliedToEvent}, + polls::{PollResult, PollState}, +}; /// The content of an [`EventTimelineItem`][super::EventTimelineItem]. #[derive(Clone, Debug)] diff --git a/crates/matrix-sdk-ui/src/timeline/polls.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs similarity index 65% rename from crates/matrix-sdk-ui/src/timeline/polls.rs rename to crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs index 19d8ec45e03..131f579cc38 100644 --- a/crates/matrix-sdk-ui/src/timeline/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs @@ -1,3 +1,17 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + //! This module handles rendering of MSC3381 polls in the timeline. use std::collections::HashMap; @@ -13,7 +27,7 @@ use ruma::{ }, PollResponseData, }, - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId, + MilliSecondsSinceUnixEpoch, OwnedUserId, UserId, }; /// Holds the state of a poll. @@ -23,21 +37,21 @@ use ruma::{ /// to the same poll start event. #[derive(Clone, Debug)] pub struct PollState { - pub(super) start_event_content: NewUnstablePollStartEventContent, - pub(super) response_data: Vec, - pub(super) end_event_timestamp: Option, - pub(super) has_been_edited: bool, + pub(in crate::timeline) start_event_content: NewUnstablePollStartEventContent, + pub(in crate::timeline) response_data: Vec, + pub(in crate::timeline) end_event_timestamp: Option, + pub(in crate::timeline) has_been_edited: bool, } #[derive(Clone, Debug)] -pub(super) struct ResponseData { - pub(super) sender: OwnedUserId, - pub(super) timestamp: MilliSecondsSinceUnixEpoch, - pub(super) answers: Vec, +pub(in crate::timeline) struct ResponseData { + pub sender: OwnedUserId, + pub timestamp: MilliSecondsSinceUnixEpoch, + pub answers: Vec, } impl PollState { - pub(super) fn new(content: NewUnstablePollStartEventContent) -> Self { + pub(crate) fn new(content: NewUnstablePollStartEventContent) -> Self { Self { start_event_content: content, response_data: vec![], @@ -48,7 +62,7 @@ impl PollState { /// Applies an edit to a poll, returns `None` if the poll was already marked /// as finished. - pub(super) fn edit( + pub(crate) fn edit( &self, replacement: &NewUnstablePollStartEventContentWithoutRelation, ) -> Option { @@ -63,7 +77,7 @@ impl PollState { } } - pub(super) fn add_response( + pub(crate) fn add_response( &self, sender: &UserId, timestamp: MilliSecondsSinceUnixEpoch, @@ -81,7 +95,7 @@ impl PollState { /// Marks the poll as ended. /// /// If the poll has already ended, returns `Err(())`. - pub(super) fn end(&self, timestamp: MilliSecondsSinceUnixEpoch) -> Result { + pub(crate) fn end(&self, timestamp: MilliSecondsSinceUnixEpoch) -> Result { if self.end_event_timestamp.is_none() { let mut clone = self.clone(); clone.end_event_timestamp = Some(timestamp); @@ -146,58 +160,6 @@ impl From for NewUnstablePollStartEventContent { } } -/// Cache holding poll response and end events handled before their poll start -/// event has been handled. -#[derive(Clone, Debug, Default)] -pub(super) struct PendingPollEvents { - /// Responses to a poll (identified by the poll's start event id). - responses: HashMap>, - - /// Mapping of a poll (identified by its start event's id) to its end date. - end_dates: HashMap, -} - -impl PendingPollEvents { - pub(super) fn add_response( - &mut self, - start_event_id: &EventId, - sender: &UserId, - timestamp: MilliSecondsSinceUnixEpoch, - content: &UnstablePollResponseEventContent, - ) { - self.responses.entry(start_event_id.to_owned()).or_default().push(ResponseData { - sender: sender.to_owned(), - timestamp, - answers: content.poll_response.answers.clone(), - }); - } - - pub(super) fn clear(&mut self) { - self.end_dates.clear(); - self.responses.clear(); - } - - /// Mark a poll as finished by inserting its poll date. - pub(super) fn mark_as_ended( - &mut self, - start_event_id: &EventId, - timestamp: MilliSecondsSinceUnixEpoch, - ) { - self.end_dates.insert(start_event_id.to_owned(), timestamp); - } - - /// Dumps all response and end events present in the cache that belong to - /// the given start_event_id into the given poll_state. - pub(super) fn apply_pending(&mut self, start_event_id: &EventId, poll_state: &mut PollState) { - if let Some(pending_responses) = self.responses.remove(start_event_id) { - poll_state.response_data.extend(pending_responses); - } - if let Some(pending_end) = self.end_dates.remove(start_event_id) { - poll_state.end_event_timestamp = Some(pending_end); - } - } -} - #[derive(Debug)] pub struct PollResult { pub question: String, 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 7436f96e49e..853f85a0557 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -42,15 +42,15 @@ mod local; mod remote; pub(super) use self::{ - content::extract_edit_content, + content::{extract_edit_content, ResponseData}, local::LocalEventTimelineItem, remote::{RemoteEventOrigin, RemoteEventTimelineItem}, }; pub use self::{ content::{ AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange, - MembershipChange, Message, OtherState, RepliedToEvent, RoomMembershipChange, - RoomPinnedEventsChange, Sticker, TimelineItemContent, + MembershipChange, Message, OtherState, PollResult, PollState, RepliedToEvent, + RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineItemContent, }, local::EventSendState, }; diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 48ef57110e6..17422445073 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -68,7 +68,6 @@ pub mod futures; mod item; mod pagination; mod pinned_events_loader; -mod polls; mod reactions; mod read_receipts; #[cfg(test)] @@ -85,14 +84,13 @@ pub use self::{ event_item::{ AnyOtherFullStateEventContent, EncryptedMessage, EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, Message, - OtherState, Profile, ReactionInfo, ReactionStatus, ReactionsByKeyBySender, RepliedToEvent, - RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineDetails, + OtherState, PollResult, Profile, ReactionInfo, ReactionStatus, ReactionsByKeyBySender, + RepliedToEvent, RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineDetails, TimelineEventItemId, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, item::{TimelineItem, TimelineItemKind}, pagination::LiveBackPaginationStatus, - polls::PollResult, traits::RoomExt, virtual_item::VirtualTimelineItem, }; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs index 14c7438d8e6..10f9b1fcddd 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs @@ -15,7 +15,7 @@ use ruma::{ }; use crate::timeline::{ - polls::PollState, tests::TestTimeline, EventTimelineItem, TimelineItemContent, + event_item::PollState, tests::TestTimeline, EventTimelineItem, TimelineItemContent, }; #[async_test] From 05cbb9e290e4e0b219324aec31fbe6b91c6fb2d5 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 16:58:27 +0200 Subject: [PATCH 213/979] timeline(refactor): get rid of the stored event id in the pending_edits array Since it's implied from the `Replacement` data structure. Also reuse `find_and_remove_pending` in more places. --- .../src/timeline/controller/state.rs | 11 ++- .../src/timeline/event_handler.rs | 69 +++++++++---------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 371c0b356cb..4cc3af19afd 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -816,6 +816,15 @@ pub(in crate::timeline) enum PendingEdit { Poll(Replacement), } +impl PendingEdit { + pub fn edited_event(&self) -> &EventId { + match self { + PendingEdit::RoomMessage(Replacement { event_id, .. }) + | PendingEdit::Poll(Replacement { event_id, .. }) => event_id, + } + } +} + #[cfg(not(tarpaulin_include))] impl std::fmt::Debug for PendingEdit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -869,7 +878,7 @@ pub(in crate::timeline) struct TimelineMetadata { pub pending_poll_events: PendingPollEvents, /// Edit events received before the related event they're editing. - pub pending_edits: RingBuffer<(OwnedEventId, PendingEdit)>, + pub pending_edits: RingBuffer, /// Identifier of the fully-read event, helping knowing where to introduce /// the read marker. diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 06a9cfc58d2..4cc55834de8 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -18,7 +18,8 @@ use as_variant::as_variant; use eyeball_im::{ObservableVectorTransaction, ObservableVectorTransactionEntry}; use indexmap::IndexMap; use matrix_sdk::{ - crypto::types::events::UtdCause, deserialized_responses::EncryptionInfo, send_queue::SendHandle, + crypto::types::events::UtdCause, deserialized_responses::EncryptionInfo, + ring_buffer::RingBuffer, send_queue::SendHandle, }; use ruma::{ events::{ @@ -42,7 +43,8 @@ use ruma::{ MessageLikeEventType, StateEventType, SyncStateEvent, }, serde::Raw, - MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomVersionId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, + RoomVersionId, }; use tracing::{debug, error, field::debug, info, instrument, trace, warn}; @@ -329,25 +331,17 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // there's an edit in the relations mapping, we want to prefer it over any // other pending edit, since it's more likely to be up to date, and we // don't want to apply another pending edit on top of it. - let pending_edit = if let Flow::Remote { event_id, .. } = &self.ctx.flow { - let edits = &mut self.meta.pending_edits; - edits - .iter() - .position(|(prev_event_id, _)| prev_event_id == event_id) - .into_iter() - .filter_map(|pos| { - Some( - as_variant!( - edits.remove(pos).unwrap().1, - PendingEdit::RoomMessage - )? - .new_content, + let pending_edit = + as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) + .and_then(|event_id| { + Self::find_and_remove_pending( + &mut self.meta.pending_edits, + event_id, ) }) - .next() - } else { - None - }; + .and_then(|edit| { + Some(as_variant!(edit, PendingEdit::RoomMessage)?.new_content) + }); let edit = extract_edit_content(relations).or(pending_edit); @@ -517,9 +511,9 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { .meta .pending_edits .iter() - .any(|(event_id, _)| *event_id == replaced_event_id) + .any(|edit| edit.edited_event() == replaced_event_id) { - self.meta.pending_edits.push((replaced_event_id, replacement)); + self.meta.pending_edits.push(replacement); debug!("Timeline item not found, stashing edit"); } else { debug!("Timeline item not found, but there was a previous edit for the event: discarding"); @@ -531,41 +525,40 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // forward-pagination: it's fine to overwrite the previous one, if // available. let edits = &mut self.meta.pending_edits; - if let Some(pos) = - edits.iter().position(|(event_id, _)| *event_id == replaced_event_id) - { - edits.remove(pos); - } - edits.push((replaced_event_id, replacement)); + let _ = Self::find_and_remove_pending(edits, &replaced_event_id); + edits.push(replacement); debug!("Timeline item not found, stashing edit"); } } } + /// TODO rename to maybe_unstash_pending_edit? + fn find_and_remove_pending( + edits: &mut RingBuffer, + event_id: &EventId, + ) -> Option { + let pos = edits.iter().position(|edit| edit.edited_event() == event_id)?; + Some(edits.remove(pos).unwrap()) + } + /// If there's a pending edit for an item, apply it immediately, returning /// an updated [`EventTimelineItem`]. Otherwise, return `None`. fn maybe_unstash_pending_edit( &mut self, item: &EventTimelineItem, ) -> Option { - let Flow::Remote { event_id, .. } = &self.ctx.flow else { - return None; - }; - - let mut find_and_remove_pending = |event_id| { - let edits = &mut self.meta.pending_edits; - let pos = edits.iter().position(|(prev_event_id, _)| prev_event_id == event_id)?; - Some(edits.remove(pos).unwrap().1) - }; + let event_id = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id)?; match item.content() { TimelineItemContent::Message(..) => { - let pending = find_and_remove_pending(event_id)?; + let pending = + Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id)?; let edit = as_variant!(pending, PendingEdit::RoomMessage)?; self.apply_msg_edit(item, edit) } TimelineItemContent::Poll(..) => { - let pending = find_and_remove_pending(event_id)?; + let pending = + Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id)?; let edit = as_variant!(pending, PendingEdit::Poll)?; self.apply_poll_edit(item, edit) } From f8e65f53cdaa18a3a0e7776cdc91c3d2a6003edf Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 18:30:22 +0200 Subject: [PATCH 214/979] timeline: provide the edit JSON for edits either pending or bundled --- .../src/timeline/controller/mod.rs | 4 +- .../src/timeline/controller/state.rs | 24 ++-- .../src/timeline/event_handler.rs | 122 +++++++++++------- .../matrix-sdk-ui/src/timeline/tests/edit.rs | 13 +- .../tests/integration/timeline/edit.rs | 7 +- 5 files changed, 111 insertions(+), 59 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index b884d8af1ef..9095a94937b 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -52,8 +52,8 @@ use tracing::{ }; pub(super) use self::state::{ - EventMeta, FullEventMeta, PendingEdit, TimelineEnd, TimelineMetadata, TimelineState, - TimelineStateTransaction, + EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineEnd, TimelineMetadata, + TimelineState, TimelineStateTransaction, }; use super::{ event_handler::TimelineEventKind, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4cc3af19afd..b4f8b60f720 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -35,7 +35,7 @@ use ruma::{ }, relation::Replacement, room::message::RoomMessageEventContentWithoutRelation, - AnySyncEphemeralRoomEvent, + AnySyncEphemeralRoomEvent, AnySyncTimelineEvent, }, push::Action, serde::Raw, @@ -811,16 +811,22 @@ impl PendingPollEvents { } #[derive(Clone)] -pub(in crate::timeline) enum PendingEdit { +pub(in crate::timeline) enum PendingEditKind { RoomMessage(Replacement), Poll(Replacement), } +#[derive(Clone)] +pub(in crate::timeline) struct PendingEdit { + pub kind: PendingEditKind, + pub event_json: Raw, +} + impl PendingEdit { pub fn edited_event(&self) -> &EventId { - match self { - PendingEdit::RoomMessage(Replacement { event_id, .. }) - | PendingEdit::Poll(Replacement { event_id, .. }) => event_id, + match &self.kind { + PendingEditKind::RoomMessage(Replacement { event_id, .. }) + | PendingEditKind::Poll(Replacement { event_id, .. }) => event_id, } } } @@ -828,9 +834,11 @@ impl PendingEdit { #[cfg(not(tarpaulin_include))] impl std::fmt::Debug for PendingEdit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::RoomMessage(_) => f.debug_struct("RoomMessage").finish_non_exhaustive(), - Self::Poll(_) => f.debug_struct("Poll").finish_non_exhaustive(), + match &self.kind { + PendingEditKind::RoomMessage(_) => { + f.debug_struct("RoomMessage").finish_non_exhaustive() + } + PendingEditKind::Poll(_) => f.debug_struct("Poll").finish_non_exhaustive(), } } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 4cc55834de8..50b7dd90b19 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -49,7 +49,7 @@ use ruma::{ use tracing::{debug, error, field::debug, info, instrument, trace, warn}; use super::{ - controller::{TimelineMetadata, TimelineStateTransaction}, + controller::{PendingEditKind, TimelineMetadata, TimelineStateTransaction}, day_dividers::DayDividerAdjuster, event_item::{ extract_edit_content, AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, @@ -339,20 +339,40 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { event_id, ) }) - .and_then(|edit| { - Some(as_variant!(edit, PendingEdit::RoomMessage)?.new_content) + .and_then(|edit| match edit.kind { + PendingEditKind::RoomMessage(replacement) => { + Some((Some(edit.event_json), replacement.new_content)) + } + _ => None, }); - let edit = extract_edit_content(relations).or(pending_edit); + let (edit_json, edit_content) = extract_edit_content(relations) + .map(|content| { + let edit_json = as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event).and_then(|raw| { + // Kids, don't do this at home. We're extracting the edit event + // from the `unsigned`.m.relations`.`m.replace` path. + let raw_unsigned: Raw = raw.get_field("unsigned").ok()??; + let raw_relations: Raw = raw_unsigned.get_field("m.relations").ok()??; + raw_relations.get_field::>("m.replace").ok()? + }); + + (edit_json, content) + } + ).or(pending_edit).unzip(); + + let edit_json = edit_json.flatten(); - self.add_item(TimelineItemContent::message(c, edit, self.items)); + self.add_item( + TimelineItemContent::message(c, edit_content, self.items), + edit_json, + ); } } AnyMessageLikeEventContent::RoomEncrypted(c) => { // TODO: Handle replacements if the replaced event is also UTD let cause = UtdCause::determine(raw_event); - self.add_item(TimelineItemContent::unable_to_decrypt(c, cause)); + self.add_item(TimelineItemContent::unable_to_decrypt(c, cause), None); // Let the hook know that we ran into an unable-to-decrypt that is added to the // timeline. @@ -365,7 +385,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::Sticker(content) => { if should_add { - self.add_item(TimelineItemContent::Sticker(Sticker { content })); + self.add_item(TimelineItemContent::Sticker(Sticker { content }), None); } } @@ -383,13 +403,13 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::CallInvite(_) => { if should_add { - self.add_item(TimelineItemContent::CallInvite); + self.add_item(TimelineItemContent::CallInvite, None); } } AnyMessageLikeEventContent::CallNotify(_) => { if should_add { - self.add_item(TimelineItemContent::CallNotify) + self.add_item(TimelineItemContent::CallNotify, None) } } @@ -404,7 +424,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { TimelineEventKind::RedactedMessage { event_type } => { if event_type != MessageLikeEventType::Reaction && should_add { - self.add_item(TimelineItemContent::RedactedMessage); + self.add_item(TimelineItemContent::RedactedMessage, None); } } @@ -414,7 +434,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { TimelineEventKind::RoomMember { user_id, content, sender } => { if should_add { - self.add_item(TimelineItemContent::room_member(user_id, content, sender)); + self.add_item(TimelineItemContent::room_member(user_id, content, sender), None); } } @@ -422,29 +442,28 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // Update room encryption if a `m.room.encryption` event is found in the // timeline if should_add { - self.add_item(TimelineItemContent::OtherState(OtherState { - state_key, - content, - })); + self.add_item( + TimelineItemContent::OtherState(OtherState { state_key, content }), + None, + ); } } TimelineEventKind::FailedToParseMessageLike { event_type, error } => { if should_add { - self.add_item(TimelineItemContent::FailedToParseMessageLike { - event_type, - error, - }); + self.add_item( + TimelineItemContent::FailedToParseMessageLike { event_type, error }, + None, + ); } } TimelineEventKind::FailedToParseState { event_type, state_key, error } => { if should_add { - self.add_item(TimelineItemContent::FailedToParseState { - event_type, - state_key, - error, - }); + self.add_item( + TimelineItemContent::FailedToParseState { event_type, state_key, error }, + None, + ); } } } @@ -474,14 +493,19 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: Replacement, ) { if let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) { - if let Some(new_item) = self.apply_msg_edit(&item, replacement) { + let edit_json = + as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => raw_event).cloned(); + if let Some(new_item) = self.apply_msg_edit(&item, replacement, edit_json) { trace!("Applied edit"); self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); self.result.items_updated += 1; } - } else if let Flow::Remote { position, .. } = &self.ctx.flow { + } else if let Flow::Remote { position, raw_event, .. } = &self.ctx.flow { let replaced_event_id = replacement.event_id.clone(); - let replacement = PendingEdit::RoomMessage(replacement); + let replacement = PendingEdit { + kind: PendingEditKind::RoomMessage(replacement), + event_json: raw_event.clone(), + }; self.stash_pending_edit(*position, replaced_event_id, replacement); } else { debug!("Local message edit for a timeline item not found, discarding"); @@ -553,14 +577,16 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { TimelineItemContent::Message(..) => { let pending = Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id)?; - let edit = as_variant!(pending, PendingEdit::RoomMessage)?; - self.apply_msg_edit(item, edit) + let edit = as_variant!(pending.kind, PendingEditKind::RoomMessage)?; + //let json = as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => + // raw_event.clone()); self.apply_msg_edit(item, edit, json) + self.apply_msg_edit(item, edit, Some(pending.event_json)) } TimelineItemContent::Poll(..) => { let pending = Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id)?; - let edit = as_variant!(pending, PendingEdit::Poll)?; - self.apply_poll_edit(item, edit) + let edit = as_variant!(pending.kind, PendingEditKind::Poll)?; + self.apply_poll_edit(item, edit, Some(pending.event_json)) } _ => None, } @@ -574,6 +600,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &self, item: &EventTimelineItem, replacement: Replacement, + edit_json: Option>, ) -> Option { if self.ctx.sender != item.sender() { info!( @@ -591,11 +618,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return None; }; - let edit_json = match &self.ctx.flow { - Flow::Local { .. } => None, - Flow::Remote { raw_event, .. } => Some(raw_event.clone()), - }; - let mut new_msg = msg.clone(); new_msg.apply_edit(replacement.new_content); @@ -697,9 +719,12 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: Replacement, ) { let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) else { - if let Flow::Remote { position, .. } = &self.ctx.flow { + if let Flow::Remote { position, raw_event, .. } = &self.ctx.flow { let replaced_event_id = replacement.event_id.clone(); - let replacement = PendingEdit::Poll(replacement); + let replacement = PendingEdit { + kind: PendingEditKind::Poll(replacement), + event_json: raw_event.clone(), + }; self.stash_pending_edit(*position, replaced_event_id, replacement); } else { debug!("Local poll edit for a timeline item not found, discarding"); @@ -707,7 +732,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return; }; - let Some(new_item) = self.apply_poll_edit(item.inner, replacement) else { + let edit_json = + as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => raw_event.clone()); + + let Some(new_item) = self.apply_poll_edit(item.inner, replacement, edit_json) else { return; }; @@ -720,6 +748,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &self, item: &EventTimelineItem, replacement: Replacement, + edit_json: Option>, ) -> Option { if self.ctx.sender != item.sender() { info!( @@ -742,11 +771,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } }; - let edit_json = match &self.ctx.flow { - Flow::Local { .. } => None, - Flow::Remote { raw_event, .. } => Some(raw_event.clone()), - }; - Some(item.with_content(new_content, edit_json)) } @@ -761,7 +785,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } if should_add { - self.add_item(TimelineItemContent::Poll(poll_state)); + self.add_item(TimelineItemContent::Poll(poll_state), None); } } @@ -916,7 +940,11 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } /// Add a new event item in the timeline. - fn add_item(&mut self, content: TimelineItemContent) { + fn add_item( + &mut self, + content: TimelineItemContent, + edit_json: Option>, + ) { self.result.item_added = true; let sender = self.ctx.sender.to_owned(); @@ -955,7 +983,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { is_highlighted: self.ctx.is_highlighted, encryption_info: encryption_info.clone(), original_json: Some(raw_event.clone()), - latest_edit_json: None, + latest_edit_json: edit_json, origin, } .into() diff --git a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs index ddfcdead893..2e6329d7246 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs @@ -259,7 +259,18 @@ async fn test_relations_edit_overrides_pending_edit() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); // We receive the latest edit, not the pending one. - let text = item.as_event().unwrap().content().as_message().unwrap(); + let event = item.as_event().unwrap(); + assert_eq!( + event + .latest_edit_json() + .expect("we should have an edit json") + .deserialize() + .unwrap() + .event_id(), + edit2_event_id + ); + + let text = event.content().as_message().unwrap(); assert_eq!(text.body(), "edit 2"); let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 512ea98b8c7..0ad3b7353db 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -851,7 +851,12 @@ async fn test_pending_edit() { // Then I get the edited content immediately. assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); - let msg = value.as_event().unwrap().content().as_message().unwrap(); + + let event = value.as_event().unwrap(); + let latest_edit_json = event.latest_edit_json().expect("we should have an edit json"); + assert_eq!(latest_edit_json.deserialize().unwrap().event_id(), edit_event_id); + + let msg = event.content().as_message().unwrap(); assert!(msg.is_edited()); assert_eq!(msg.body(), "[edit]"); From d789983eff79dd0ac324f8b8fb6d3a0abbcfccb7 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 2 Oct 2024 18:33:09 +0200 Subject: [PATCH 215/979] timeline: rename `extract_edit_content` to `extract_room_msg_edit_content` --- crates/matrix-sdk-ui/src/timeline/event_handler.rs | 8 ++++---- .../src/timeline/event_item/content/message.rs | 4 ++-- .../matrix-sdk-ui/src/timeline/event_item/content/mod.rs | 2 +- crates/matrix-sdk-ui/src/timeline/event_item/mod.rs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 50b7dd90b19..427c77bc68f 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -52,9 +52,9 @@ use super::{ controller::{PendingEditKind, TimelineMetadata, TimelineStateTransaction}, day_dividers::DayDividerAdjuster, event_item::{ - extract_edit_content, AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, - LocalEventTimelineItem, PollState, Profile, ReactionsByKeyBySender, RemoteEventOrigin, - RemoteEventTimelineItem, TimelineEventItemId, + extract_room_msg_edit_content, AnyOtherFullStateEventContent, EventSendState, + EventTimelineItemKind, LocalEventTimelineItem, PollState, Profile, ReactionsByKeyBySender, + RemoteEventOrigin, RemoteEventTimelineItem, TimelineEventItemId, }, reactions::FullReactionKey, util::{rfind_event_by_id, rfind_event_item}, @@ -346,7 +346,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { _ => None, }); - let (edit_json, edit_content) = extract_edit_content(relations) + let (edit_json, edit_content) = extract_room_msg_edit_content(relations) .map(|content| { let edit_json = as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event).and_then(|raw| { // Kids, don't do this at home. We're extracting the edit event diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 6d04d72cf00..4c16523e0cd 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -164,7 +164,7 @@ impl From for RoomMessageEventContent { /// Extracts a replacement for a room message, if present in the bundled /// relations. -pub(crate) fn extract_edit_content( +pub(crate) fn extract_room_msg_edit_content( relations: BundledMessageLikeRelations, ) -> Option { match *relations.replace? { @@ -320,7 +320,7 @@ impl RepliedToEvent { let content = TimelineItemContent::Message(Message::from_event( c, - extract_edit_content(event.relations()), + extract_room_msg_edit_content(event.relations()), &vector![], )); let sender = event.sender().to_owned(); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index f76a7238f67..9e62f927b9a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -66,7 +66,7 @@ mod polls; pub use pinned_events::RoomPinnedEventsChange; -pub(in crate::timeline) use self::{message::extract_edit_content, polls::ResponseData}; +pub(in crate::timeline) use self::{message::extract_room_msg_edit_content, polls::ResponseData}; pub use self::{ message::{InReplyToDetails, Message, RepliedToEvent}, polls::{PollResult, PollState}, 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 853f85a0557..1de842ab20f 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -42,7 +42,7 @@ mod local; mod remote; pub(super) use self::{ - content::{extract_edit_content, ResponseData}, + content::{extract_room_msg_edit_content, ResponseData}, local::LocalEventTimelineItem, remote::{RemoteEventOrigin, RemoteEventTimelineItem}, }; From f21de25da036a5ff054d88b1730450880a08d907 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 10:17:36 +0200 Subject: [PATCH 216/979] timeline: apply bundled edits for polls too --- .../src/timeline/event_handler.rs | 95 +++++++++---------- .../timeline/event_item/content/message.rs | 48 +++++++++- .../src/timeline/event_item/content/mod.rs | 40 ++++++-- .../src/timeline/event_item/content/polls.rs | 21 +++- .../src/timeline/event_item/mod.rs | 5 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 4 +- 6 files changed, 144 insertions(+), 69 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 427c77bc68f..633ce170ed0 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -52,9 +52,10 @@ use super::{ controller::{PendingEditKind, TimelineMetadata, TimelineStateTransaction}, day_dividers::DayDividerAdjuster, event_item::{ - extract_room_msg_edit_content, AnyOtherFullStateEventContent, EventSendState, - EventTimelineItemKind, LocalEventTimelineItem, PollState, Profile, ReactionsByKeyBySender, - RemoteEventOrigin, RemoteEventTimelineItem, TimelineEventItemId, + extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content, + AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, + LocalEventTimelineItem, PollState, Profile, ReactionsByKeyBySender, RemoteEventOrigin, + RemoteEventTimelineItem, TimelineEventItemId, }, reactions::FullReactionKey, util::{rfind_event_by_id, rfind_event_item}, @@ -348,14 +349,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let (edit_json, edit_content) = extract_room_msg_edit_content(relations) .map(|content| { - let edit_json = as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event).and_then(|raw| { - // Kids, don't do this at home. We're extracting the edit event - // from the `unsigned`.m.relations`.`m.replace` path. - let raw_unsigned: Raw = raw.get_field("unsigned").ok()??; - let raw_relations: Raw = raw_unsigned.get_field("m.relations").ok()??; - raw_relations.get_field::>("m.replace").ok()? - }); - + let raw_event = as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event); + let edit_json = raw_event.and_then(extract_bundled_edit_event_json); (edit_json, content) } ).or(pending_edit).unzip(); @@ -395,7 +390,11 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::UnstablePollStart( UnstablePollStartEventContent::New(c), - ) => self.handle_poll_start(c, should_add), + ) => { + if should_add { + self.handle_poll_start(c, relations) + } + } AnyMessageLikeEventContent::UnstablePollResponse(c) => self.handle_poll_response(c), @@ -565,33 +564,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Some(edits.remove(pos).unwrap()) } - /// If there's a pending edit for an item, apply it immediately, returning - /// an updated [`EventTimelineItem`]. Otherwise, return `None`. - fn maybe_unstash_pending_edit( - &mut self, - item: &EventTimelineItem, - ) -> Option { - let event_id = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id)?; - - match item.content() { - TimelineItemContent::Message(..) => { - let pending = - Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id)?; - let edit = as_variant!(pending.kind, PendingEditKind::RoomMessage)?; - //let json = as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => - // raw_event.clone()); self.apply_msg_edit(item, edit, json) - self.apply_msg_edit(item, edit, Some(pending.event_json)) - } - TimelineItemContent::Poll(..) => { - let pending = - Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id)?; - let edit = as_variant!(pending.kind, PendingEditKind::Poll)?; - self.apply_poll_edit(item, edit, Some(pending.event_json)) - } - _ => None, - } - } - /// Try applying an edit to an existing [`EventTimelineItem`]. /// /// Return a new item if applying the edit succeeded, or `None` if there was @@ -763,7 +735,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return None; }; - let new_content = match poll_state.edit(&replacement.new_content) { + let new_content = match poll_state.edit(replacement.new_content) { Some(edited_poll_state) => TimelineItemContent::Poll(edited_poll_state), None => { info!("Not applying edit to a poll that's already ended"); @@ -775,8 +747,37 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } /// Adds a new poll to the timeline. - fn handle_poll_start(&mut self, c: NewUnstablePollStartEventContent, should_add: bool) { - let mut poll_state = PollState::new(c); + fn handle_poll_start( + &mut self, + c: NewUnstablePollStartEventContent, + relations: BundledMessageLikeRelations, + ) { + // Always remove the pending edit, if there's any. The reason is that if + // there's an edit in the relations mapping, we want to prefer it over any + // other pending edit, since it's more likely to be up to date, and we + // don't want to apply another pending edit on top of it. + let pending_edit = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) + .and_then(|event_id| { + Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id) + }) + .and_then(|edit| match edit.kind { + PendingEditKind::Poll(replacement) => { + Some((Some(edit.event_json), replacement.new_content)) + } + _ => None, + }); + + let (edit_json, edit_content) = extract_poll_edit_content(relations) + .map(|content| { + let raw_event = + as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event); + let edit_json = raw_event.and_then(extract_bundled_edit_event_json); + (edit_json, content) + }) + .or(pending_edit) + .unzip(); + + let mut poll_state = PollState::new(c, edit_content); if let Flow::Remote { event_id, .. } = &self.ctx.flow { // Applying the cache to remote events only because local echoes @@ -784,9 +785,9 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { self.meta.pending_poll_events.apply_pending(event_id, &mut poll_state); } - if should_add { - self.add_item(TimelineItemContent::Poll(poll_state), None); - } + let edit_json = edit_json.flatten(); + + self.add_item(TimelineItemContent::Poll(poll_state), edit_json); } fn handle_poll_response(&mut self, c: UnstablePollResponseEventContent) { @@ -1006,10 +1007,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { is_room_encrypted, ); - if let Some(edited_item) = self.maybe_unstash_pending_edit(&item) { - item = edited_item; - } - match &self.ctx.flow { Flow::Local { .. } => { trace!("Adding new local timeline item"); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 4c16523e0cd..df1d3d160dd 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -21,15 +21,20 @@ use matrix_sdk::{deserialized_responses::TimelineEvent, Room}; use ruma::{ assign, events::{ + poll::unstable_start::{ + NewUnstablePollStartEventContentWithoutRelation, SyncUnstablePollStartEvent, + UnstablePollStartEventContent, + }, relation::{InReplyTo, Thread}, room::message::{ MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, SyncRoomMessageEvent, }, - AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnyTimelineEvent, - BundledMessageLikeRelations, Mentions, + AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, + AnyTimelineEvent, BundledMessageLikeRelations, Mentions, }, html::RemoveReplyFallback, + serde::Raw, OwnedEventId, OwnedUserId, RoomVersionId, UserId, }; use tracing::error; @@ -162,6 +167,20 @@ impl From for RoomMessageEventContent { } } +/// Extracts the raw json of the edit event part of bundled relations. +/// +/// Note: while we had access to the deserialized event earlier, events are not +/// serializable, by design of Ruma, so we can't extract a bundled related event +/// and serialize it back to a raw JSON event. +pub(crate) fn extract_bundled_edit_event_json( + raw: &Raw, +) -> Option> { + // Follow the `unsigned`.`m.relations`.`m.replace` path. + let raw_unsigned: Raw = raw.get_field("unsigned").ok()??; + let raw_relations: Raw = raw_unsigned.get_field("m.relations").ok()??; + raw_relations.get_field::>("m.replace").ok()? +} + /// Extracts a replacement for a room message, if present in the bundled /// relations. pub(crate) fn extract_room_msg_edit_content( @@ -188,6 +207,31 @@ pub(crate) fn extract_room_msg_edit_content( } } +/// Extracts a replacement for a room message, if present in the bundled +/// relations. +pub(crate) fn extract_poll_edit_content( + relations: BundledMessageLikeRelations, +) -> Option { + match *relations.replace? { + AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Original(ev)) => { + match ev.content { + UnstablePollStartEventContent::Replacement(re) => Some(re.relates_to.new_content), + _ => { + error!("got new poll start event in a bundled edit"); + None + } + } + } + + AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Redacted(_)) => None, + + _ => { + error!("got poll edit event with an edit of a different event type"); + None + } + } +} + /// Turn a pair of thread root ID and in-reply-to ID as stored in [`Message`] /// back into a [`Relation`]. /// diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 9e62f927b9a..da5f7c5a731 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -25,7 +25,10 @@ use ruma::{ room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, user::PolicyRuleUserEventContent, }, - poll::unstable_start::{NewUnstablePollStartEventContent, SyncUnstablePollStartEvent}, + poll::unstable_start::{ + NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, + UnstablePollStartEventContent, + }, room::{ aliases::RoomAliasesEventContent, avatar::RoomAvatarEventContent, @@ -66,7 +69,12 @@ mod polls; pub use pinned_events::RoomPinnedEventsChange; -pub(in crate::timeline) use self::{message::extract_room_msg_edit_content, polls::ResponseData}; +pub(in crate::timeline) use self::{ + message::{ + extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content, + }, + polls::ResponseData, +}; pub use self::{ message::{InReplyToDetails, Message, RepliedToEvent}, polls::{PollResult, PollState}, @@ -231,14 +239,26 @@ impl TimelineItemContent { fn from_suitable_latest_poll_event_content( event: &SyncUnstablePollStartEvent, ) -> TimelineItemContent { - match event { - SyncUnstablePollStartEvent::Original(event) => { - TimelineItemContent::Poll(PollState::new(NewUnstablePollStartEventContent::new( - event.content.poll_start().clone(), - ))) - } - SyncUnstablePollStartEvent::Redacted(_) => TimelineItemContent::RedactedMessage, - } + let SyncUnstablePollStartEvent::Original(event) = event else { + return TimelineItemContent::RedactedMessage; + }; + + // Feed the bundled edit, if present, or we might miss showing edited content. + let edit = + event.unsigned.relations.replace.as_ref().and_then(|boxed| match &boxed.content { + UnstablePollStartEventContent::Replacement(re) => { + Some(re.relates_to.new_content.clone()) + } + _ => { + warn!("got poll event with an edit without a valid m.replace relation"); + None + } + }); + + TimelineItemContent::Poll(PollState::new( + NewUnstablePollStartEventContent::new(event.content.poll_start().clone()), + edit, + )) } fn from_suitable_latest_call_invite_content( diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs index 131f579cc38..8112bc7b147 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs @@ -51,25 +51,36 @@ pub(in crate::timeline) struct ResponseData { } impl PollState { - pub(crate) fn new(content: NewUnstablePollStartEventContent) -> Self { - Self { + pub(crate) fn new( + content: NewUnstablePollStartEventContent, + edit: Option, + ) -> Self { + let mut ret = Self { start_event_content: content, response_data: vec![], end_event_timestamp: None, has_been_edited: false, + }; + + if let Some(edit) = edit { + // SAFETY: [`Self::edit`] only returns `None` when the poll has ended, not the + // case here. + ret = ret.edit(edit).unwrap(); } + + ret } /// Applies an edit to a poll, returns `None` if the poll was already marked /// as finished. pub(crate) fn edit( &self, - replacement: &NewUnstablePollStartEventContentWithoutRelation, + replacement: NewUnstablePollStartEventContentWithoutRelation, ) -> Option { if self.end_event_timestamp.is_none() { let mut clone = self.clone(); - clone.start_event_content.poll_start = replacement.poll_start.clone(); - clone.start_event_content.text = replacement.text.clone(); + clone.start_event_content.poll_start = replacement.poll_start; + clone.start_event_content.text = replacement.text; clone.has_been_edited = true; Some(clone) } else { 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 1de842ab20f..04090802c4f 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -42,7 +42,10 @@ mod local; mod remote; pub(super) use self::{ - content::{extract_room_msg_edit_content, ResponseData}, + content::{ + extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content, + ResponseData, + }, local::LocalEventTimelineItem, remote::{RemoteEventOrigin, RemoteEventTimelineItem}, }; diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 17422445073..ac420a2ab52 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -18,7 +18,7 @@ use std::{path::PathBuf, pin::Pin, sync::Arc, task::Poll}; -use event_item::{extract_edit_content, EventTimelineItemKind, TimelineItemHandle}; +use event_item::{extract_room_msg_edit_content, EventTimelineItemKind, TimelineItemHandle}; use eyeball_im::VectorDiff; use futures_core::Stream; use imbl::Vector; @@ -431,7 +431,7 @@ impl Timeline { { ReplyContent::Message(Message::from_event( original_message.content.clone(), - extract_edit_content(message_like_event.relations()), + extract_room_msg_edit_content(message_like_event.relations()), &self.items().await, )) } else { From ccf8bf86521fc99c1d35b69c950fb491aee86067 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 14:02:04 +0200 Subject: [PATCH 217/979] timeline(test): add test for latest poll with a bundled edit --- .../src/timeline/event_item/mod.rs | 53 +++++++++++++++++++ .../matrix-sdk-ui/src/timeline/tests/polls.rs | 2 +- crates/matrix-sdk/src/test_utils/events.rs | 26 ++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) 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 04090802c4f..d037f5cc640 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -824,6 +824,59 @@ mod tests { } } + #[async_test] + async fn test_latest_poll_includes_bundled_edit() { + // Given a sync event that is suitable to be used as a latest_event, and + // contains a bundled edit, + let room_id = room_id!("!q:x.uk"); + let user_id = user_id!("@t:o.uk"); + + let f = EventFactory::new(); + + let original_event_id = event_id!("$original"); + + let mut relations = BundledMessageLikeRelations::new(); + relations.replace = Some(Box::new( + f.poll_edit( + original_event_id, + "It's one banana, Michael, how much could it cost?", + vec!["1 dollar", "10 dollars", "100 dollars"], + ) + .event_id(event_id!("$edit")) + .sender(user_id) + .into_raw_sync(), + )); + + let event = f + .poll_start( + "It's one avocado, Michael, how much could it cost? 10 dollars?", + "It's one avocado, Michael, how much could it cost?", + vec!["1 dollar", "10 dollars", "100 dollars"], + ) + .event_id(original_event_id) + .bundled_relations(relations) + .sender(user_id) + .into_sync(); + + let client = logged_in_client(None).await; + + // When we construct a timeline event from it, + let timeline_item = + EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) + .await + .unwrap(); + + // Then its properties correctly translate. + assert_eq!(timeline_item.sender, user_id); + + let poll = timeline_item.poll_state(); + assert!(poll.has_been_edited); + assert_eq!( + poll.start_event_content.poll_start.question.text, + "It's one banana, Michael, how much could it cost?" + ); + } + #[async_test] async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage( ) { diff --git a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs index 10f9b1fcddd..9332e26701e 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs @@ -252,7 +252,7 @@ impl TestTimeline { } impl EventTimelineItem { - fn poll_state(self) -> PollState { + pub(crate) fn poll_state(self) -> PollState { match self.content() { TimelineItemContent::Poll(poll_state) => poll_state.clone(), _ => panic!("Not a poll state"), diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index 03bc123670c..5906b09abef 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -24,8 +24,8 @@ use ruma::{ end::PollEndEventContent, response::{PollResponseEventContent, SelectionsContentBlock}, unstable_start::{ - NewUnstablePollStartEventContent, UnstablePollAnswer, - UnstablePollStartContentBlock, UnstablePollStartEventContent, + NewUnstablePollStartEventContent, ReplacementUnstablePollStartEventContent, + UnstablePollAnswer, UnstablePollStartContentBlock, UnstablePollStartEventContent, }, }, reaction::ReactionEventContent, @@ -354,6 +354,28 @@ impl EventFactory { self.event(poll_start_content) } + /// Create a poll edit event given the new question and possible answers. + pub fn poll_edit( + &self, + edited_event_id: &EventId, + poll_question: impl Into, + answers: Vec>, + ) -> EventBuilder { + // PollAnswers 'constructor' is not public, so we need to deserialize them + let answers: Vec = answers + .into_iter() + .enumerate() + .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer)) + .collect(); + let poll_answers = answers.try_into().unwrap(); + let poll_start_content_block = + UnstablePollStartContentBlock::new(poll_question, poll_answers); + self.event(ReplacementUnstablePollStartEventContent::new( + poll_start_content_block, + edited_event_id.to_owned(), + )) + } + /// Create a poll response with the given answer id and the associated poll /// start event id. pub fn poll_response( From bd7f0d695bad453cc9df81a8f3fbc7b89c3ad572 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 14:06:29 +0200 Subject: [PATCH 218/979] timeline: add `TimelineItemContent::as_poll` That's more aligned with `as_message()`, and allows getting rid of one custom test helper. --- .../src/timeline/event_item/content/mod.rs | 6 ++++++ .../src/timeline/event_item/mod.rs | 2 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 6 +++--- .../matrix-sdk-ui/src/timeline/tests/polls.rs | 17 +++-------------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index da5f7c5a731..7df383701b6 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -285,6 +285,12 @@ impl TimelineItemContent { as_variant!(self, Self::Message) } + /// If `self` is of the [`Poll`][Self::Poll] variant, return the inner + /// [`PollState`]. + pub fn as_poll(&self) -> Option<&PollState> { + as_variant!(self, Self::Poll) + } + /// If `self` is of the [`UnableToDecrypt`][Self::UnableToDecrypt] variant, /// return the inner [`EncryptedMessage`]. pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> { 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 d037f5cc640..41f835f1b66 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -869,7 +869,7 @@ mod tests { // Then its properties correctly translate. assert_eq!(timeline_item.sender, user_id); - let poll = timeline_item.poll_state(); + let poll = timeline_item.content().as_poll().unwrap(); assert!(poll.has_been_edited); assert_eq!( poll.start_event_content.poll_start.question.text, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index ac420a2ab52..d4be98e8746 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -84,9 +84,9 @@ pub use self::{ event_item::{ AnyOtherFullStateEventContent, EncryptedMessage, EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, Message, - OtherState, PollResult, Profile, ReactionInfo, ReactionStatus, ReactionsByKeyBySender, - RepliedToEvent, RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineDetails, - TimelineEventItemId, TimelineItemContent, + OtherState, PollResult, PollState, Profile, ReactionInfo, ReactionStatus, + ReactionsByKeyBySender, RepliedToEvent, RoomMembershipChange, RoomPinnedEventsChange, + Sticker, TimelineDetails, TimelineEventItemId, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, item::{TimelineItem, TimelineItemKind}, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs index 9332e26701e..b26f2a3851e 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs @@ -14,9 +14,7 @@ use ruma::{ server_name, EventId, OwnedEventId, UserId, }; -use crate::timeline::{ - event_item::PollState, tests::TestTimeline, EventTimelineItem, TimelineItemContent, -}; +use crate::timeline::{event_item::PollState, tests::TestTimeline, EventTimelineItem}; #[async_test] async fn test_poll_is_displayed() { @@ -37,7 +35,7 @@ async fn test_edited_poll_is_displayed() { let event = timeline.poll_event().await; let event_id = event.event_id().unwrap(); timeline.send_poll_edit(&ALICE, event_id, fakes::poll_b()).await; - let poll_state = event.poll_state(); + let poll_state = event.content().as_poll().unwrap(); let edited_poll_state = timeline.poll_state().await; assert_poll_start_eq(&poll_state.start_event_content.poll_start, &fakes::poll_a()); @@ -197,7 +195,7 @@ impl TestTimeline { } async fn poll_state(&self) -> PollState { - self.event_items().await[0].clone().poll_state() + self.event_items().await[0].content().as_poll().unwrap().clone() } async fn send_poll_start(&self, sender: &UserId, content: UnstablePollStartContentBlock) { @@ -251,15 +249,6 @@ impl TestTimeline { } } -impl EventTimelineItem { - pub(crate) fn poll_state(self) -> PollState { - match self.content() { - TimelineItemContent::Poll(poll_state) => poll_state.clone(), - _ => panic!("Not a poll state"), - } - } -} - fn assert_poll_start_eq(a: &UnstablePollStartContentBlock, b: &UnstablePollStartContentBlock) { assert_eq!(a.question.text, b.question.text); assert_eq!(a.kind, b.kind); From 5c8d1d816e755d847501c485a16e8ea26b8a68ce Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 14:11:18 +0200 Subject: [PATCH 219/979] timeline: move adding a new msg to its own function --- .../src/timeline/event_handler.rs | 71 ++++++++++--------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 633ce170ed0..1ab86c7aa0a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -328,39 +328,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::RoomMessage(c) => { if should_add { - // Always remove the pending edit, if there's any. The reason is that if - // there's an edit in the relations mapping, we want to prefer it over any - // other pending edit, since it's more likely to be up to date, and we - // don't want to apply another pending edit on top of it. - let pending_edit = - as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) - .and_then(|event_id| { - Self::find_and_remove_pending( - &mut self.meta.pending_edits, - event_id, - ) - }) - .and_then(|edit| match edit.kind { - PendingEditKind::RoomMessage(replacement) => { - Some((Some(edit.event_json), replacement.new_content)) - } - _ => None, - }); - - let (edit_json, edit_content) = extract_room_msg_edit_content(relations) - .map(|content| { - let raw_event = as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event); - let edit_json = raw_event.and_then(extract_bundled_edit_event_json); - (edit_json, content) - } - ).or(pending_edit).unzip(); - - let edit_json = edit_json.flatten(); - - self.add_item( - TimelineItemContent::message(c, edit_content, self.items), - edit_json, - ); + self.handle_room_message(c, relations); } } @@ -486,6 +454,43 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { self.result } + /// Handles a new room message by adding it to the timeline. + #[instrument(skip_all)] + fn handle_room_message( + &mut self, + msg: RoomMessageEventContent, + relations: BundledMessageLikeRelations, + ) { + // Always remove the pending edit, if there's any. The reason is that if + // there's an edit in the relations mapping, we want to prefer it over any + // other pending edit, since it's more likely to be up to date, and we + // don't want to apply another pending edit on top of it. + let pending_edit = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) + .and_then(|event_id| { + Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id) + }) + .and_then(|edit| match edit.kind { + PendingEditKind::RoomMessage(replacement) => { + Some((Some(edit.event_json), replacement.new_content)) + } + _ => None, + }); + + let (edit_json, edit_content) = extract_room_msg_edit_content(relations) + .map(|content| { + let raw_event = + as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event); + let edit_json = raw_event.and_then(extract_bundled_edit_event_json); + (edit_json, content) + }) + .or(pending_edit) + .unzip(); + + let edit_json = edit_json.flatten(); + + self.add_item(TimelineItemContent::message(msg, edit_content, self.items), edit_json); + } + #[instrument(skip_all, fields(replacement_event_id = ?replacement.event_id))] fn handle_room_message_edit( &mut self, From 5a1728a468394c736d32c8d0e806a8a15522ac1a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 14:13:05 +0200 Subject: [PATCH 220/979] timeline: rename `find_and_remove_pending` to `maybe_unstash_pending_edit` --- crates/matrix-sdk-ui/src/timeline/event_handler.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 1ab86c7aa0a..356242a328c 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -467,7 +467,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // don't want to apply another pending edit on top of it. let pending_edit = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) .and_then(|event_id| { - Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id) + Self::maybe_unstash_pending_edit(&mut self.meta.pending_edits, event_id) }) .and_then(|edit| match edit.kind { PendingEditKind::RoomMessage(replacement) => { @@ -553,15 +553,16 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // forward-pagination: it's fine to overwrite the previous one, if // available. let edits = &mut self.meta.pending_edits; - let _ = Self::find_and_remove_pending(edits, &replaced_event_id); + let _ = Self::maybe_unstash_pending_edit(edits, &replaced_event_id); edits.push(replacement); debug!("Timeline item not found, stashing edit"); } } } - /// TODO rename to maybe_unstash_pending_edit? - fn find_and_remove_pending( + /// Look for a pending edit for the given event, and remove it from the list + /// and return it, if found. + fn maybe_unstash_pending_edit( edits: &mut RingBuffer, event_id: &EventId, ) -> Option { @@ -763,7 +764,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // don't want to apply another pending edit on top of it. let pending_edit = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) .and_then(|event_id| { - Self::find_and_remove_pending(&mut self.meta.pending_edits, event_id) + Self::maybe_unstash_pending_edit(&mut self.meta.pending_edits, event_id) }) .and_then(|edit| match edit.kind { PendingEditKind::Poll(replacement) => { From 6b543d105fecae53a435f7f3478e176d63ea212f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 14:16:29 +0200 Subject: [PATCH 221/979] timeline: avoid passing the raw event in two places --- .../matrix-sdk-ui/src/timeline/controller/state.rs | 14 +++----------- crates/matrix-sdk-ui/src/timeline/event_handler.rs | 3 ++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index b4f8b60f720..4cd8095a130 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -201,13 +201,7 @@ impl TimelineState { let mut day_divider_adjuster = DayDividerAdjuster::default(); TimelineEventHandler::new(&mut txn, ctx) - .handle_event( - &mut day_divider_adjuster, - content, - // Local events are never UTD, so no need to pass in a raw_event - this is only - // used to determine the type of UTD if there is one. - None, - ) + .handle_event(&mut day_divider_adjuster, content) .await; txn.adjust_day_dividers(day_divider_adjuster); @@ -586,7 +580,7 @@ impl TimelineStateTransaction<'_> { is_highlighted: event.push_actions.iter().any(Action::is_highlight), flow: Flow::Remote { event_id: event_id.clone(), - raw_event: raw.clone(), + raw_event: raw, encryption_info: event.encryption_info, txn_id, position, @@ -594,9 +588,7 @@ impl TimelineStateTransaction<'_> { should_add_new_items: should_add, }; - TimelineEventHandler::new(self, ctx) - .handle_event(day_divider_adjuster, event_kind, Some(&raw)) - .await + TimelineEventHandler::new(self, ctx).handle_event(day_divider_adjuster, event_kind).await } fn clear(&mut self) { diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 356242a328c..2be4e58a003 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -289,7 +289,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { mut self, day_divider_adjuster: &mut DayDividerAdjuster, event_kind: TimelineEventKind, - raw_event: Option<&Raw>, ) -> HandleEventResult { let span = tracing::Span::current(); @@ -334,6 +333,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::RoomEncrypted(c) => { // TODO: Handle replacements if the replaced event is also UTD + let raw_event = + as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => raw_event); let cause = UtdCause::determine(raw_event); self.add_item(TimelineItemContent::unable_to_decrypt(c, cause), None); From dc4cc02926de4a81ab04139507fcb7995c319563 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 14:22:10 +0200 Subject: [PATCH 222/979] timeline: add helpers for `Flow` to avoid redundant code --- .../src/timeline/event_handler.rs | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 2be4e58a003..219ef6bc2b2 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -99,6 +99,18 @@ pub(super) enum Flow { }, } +impl Flow { + /// If the flow is remote, returns the associated event id. + pub(crate) fn event_id(&self) -> Option<&EventId> { + as_variant!(self, Flow::Remote { event_id, .. } => event_id) + } + + /// If the flow is remote, returns the associated full raw event. + pub(crate) fn raw_event(&self) -> Option<&Raw> { + as_variant!(self, Flow::Remote { raw_event, .. } => raw_event) + } +} + pub(super) struct TimelineEventContext { pub(super) sender: OwnedUserId, pub(super) sender_profile: Option, @@ -333,15 +345,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { AnyMessageLikeEventContent::RoomEncrypted(c) => { // TODO: Handle replacements if the replaced event is also UTD - let raw_event = - as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => raw_event); + let raw_event = self.ctx.flow.raw_event(); let cause = UtdCause::determine(raw_event); self.add_item(TimelineItemContent::unable_to_decrypt(c, cause), None); // Let the hook know that we ran into an unable-to-decrypt that is added to the // timeline. if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() { - if let Flow::Remote { event_id, .. } = &self.ctx.flow { + if let Some(event_id) = &self.ctx.flow.event_id() { hook.on_utd(event_id, cause).await; } } @@ -466,7 +477,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // there's an edit in the relations mapping, we want to prefer it over any // other pending edit, since it's more likely to be up to date, and we // don't want to apply another pending edit on top of it. - let pending_edit = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) + let pending_edit = self + .ctx + .flow + .event_id() .and_then(|event_id| { Self::maybe_unstash_pending_edit(&mut self.meta.pending_edits, event_id) }) @@ -479,9 +493,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let (edit_json, edit_content) = extract_room_msg_edit_content(relations) .map(|content| { - let raw_event = - as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event); - let edit_json = raw_event.and_then(extract_bundled_edit_event_json); + let edit_json = self.ctx.flow.raw_event().and_then(extract_bundled_edit_event_json); (edit_json, content) }) .or(pending_edit) @@ -498,8 +510,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: Replacement, ) { if let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) { - let edit_json = - as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => raw_event).cloned(); + let edit_json = self.ctx.flow.raw_event().cloned(); if let Some(new_item) = self.apply_msg_edit(&item, replacement, edit_json) { trace!("Applied edit"); self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); @@ -711,8 +722,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return; }; - let edit_json = - as_variant!(&self.ctx.flow, Flow::Remote { raw_event, .. } => raw_event.clone()); + let edit_json = self.ctx.flow.raw_event().cloned(); let Some(new_item) = self.apply_poll_edit(item.inner, replacement, edit_json) else { return; @@ -763,7 +773,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // there's an edit in the relations mapping, we want to prefer it over any // other pending edit, since it's more likely to be up to date, and we // don't want to apply another pending edit on top of it. - let pending_edit = as_variant!(&self.ctx.flow, Flow::Remote { event_id, .. } => event_id) + let pending_edit = self + .ctx + .flow + .event_id() .and_then(|event_id| { Self::maybe_unstash_pending_edit(&mut self.meta.pending_edits, event_id) }) @@ -776,9 +789,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let (edit_json, edit_content) = extract_poll_edit_content(relations) .map(|content| { - let raw_event = - as_variant!(&self.ctx.flow, Flow::Remote { raw_event, ..} => raw_event); - let edit_json = raw_event.and_then(extract_bundled_edit_event_json); + let edit_json = self.ctx.flow.raw_event().and_then(extract_bundled_edit_event_json); (edit_json, content) }) .or(pending_edit) @@ -786,7 +797,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let mut poll_state = PollState::new(c, edit_content); - if let Flow::Remote { event_id, .. } = &self.ctx.flow { + if let Some(event_id) = self.ctx.flow.event_id() { // Applying the cache to remote events only because local echoes // don't have an event ID that could be referenced by responses yet. self.meta.pending_poll_events.apply_pending(event_id, &mut poll_state); @@ -1144,28 +1155,25 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return None; } - match &self.ctx.flow { - Flow::Local { .. } => None, - Flow::Remote { event_id, .. } => { - let reactions = self.meta.reactions.pending.remove(event_id)?; - let mut bundled = ReactionsByKeyBySender::default(); - - for (reaction_event_id, reaction) in reactions { - let group: &mut IndexMap = - bundled.entry(reaction.key).or_default(); - - group.insert( - reaction.sender_id, - ReactionInfo { - timestamp: reaction.timestamp, - status: ReactionStatus::RemoteToRemote(reaction_event_id), - }, - ); - } + self.ctx.flow.event_id().and_then(|event_id| { + let reactions = self.meta.reactions.pending.remove(event_id)?; + let mut bundled = ReactionsByKeyBySender::default(); + + for (reaction_event_id, reaction) in reactions { + let group: &mut IndexMap = + bundled.entry(reaction.key).or_default(); - Some(bundled) + group.insert( + reaction.sender_id, + ReactionInfo { + timestamp: reaction.timestamp, + status: ReactionStatus::RemoteToRemote(reaction_event_id), + }, + ); } - } + + Some(bundled) + }) } } From 5c353923cd4f02dcc3ada055cb1d86f24adc9aea Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 3 Oct 2024 14:34:48 +0200 Subject: [PATCH 223/979] timeline: add test for poll edit in relations overriding pending poll edit --- .../matrix-sdk-ui/src/timeline/tests/edit.rs | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs index 2e6329d7246..3d102408c89 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs @@ -217,7 +217,7 @@ async fn test_edit_updates_encryption_info() { } #[async_test] -async fn test_relations_edit_overrides_pending_edit() { +async fn test_relations_edit_overrides_pending_edit_msg() { let timeline = TestTimeline::new(); let mut stream = timeline.subscribe().await; @@ -278,3 +278,81 @@ async fn test_relations_edit_overrides_pending_edit() { assert_pending!(stream); } + +#[async_test] +async fn test_relations_edit_overrides_pending_edit_poll() { + let timeline = TestTimeline::new(); + let mut stream = timeline.subscribe().await; + + let f = &timeline.factory; + + let original_event_id = event_id!("$original"); + let edit1_event_id = event_id!("$edit1"); + let edit2_event_id = event_id!("$edit2"); + + // Pending edit is stashed, nothing comes from the stream. + timeline + .handle_live_event( + f.poll_edit( + original_event_id, + "Can the fake slim shady please stand up?", + vec!["Excuse me?"], + ) + .sender(*ALICE) + .event_id(edit1_event_id), + ) + .await; + assert_pending!(stream); + + // Now we receive the original event, with a bundled relations group. + let mut relations = BundledMessageLikeRelations::new(); + relations.replace = Some(Box::new( + f.poll_edit( + original_event_id, + "Can the real slim shady please stand up?", + vec!["Excuse me?", "Please stand up 🎵", "Please stand up 🎶"], + ) + .sender(*ALICE) + .event_id(edit2_event_id) + .into(), + )); + + let ev = f + .poll_start( + "Can the fake slim shady please stand down?\nExcuse me?", + "Can the fake slim shady please stand down?", + vec!["Excuse me?"], + ) + .sender(*ALICE) + .event_id(original_event_id) + .bundled_relations(relations); + + timeline.handle_live_event(ev).await; + + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + + // We receive the latest edit, not the pending one. + let event = item.as_event().unwrap(); + assert_eq!( + event + .latest_edit_json() + .expect("we should have an edit json") + .deserialize() + .unwrap() + .event_id(), + edit2_event_id + ); + + let poll = event.content().as_poll().unwrap(); + assert!(poll.has_been_edited); + assert_eq!( + poll.start_event_content.poll_start.question.text, + "Can the real slim shady please stand up?" + ); + assert_eq!(poll.start_event_content.poll_start.answers.len(), 3); + + let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(day_divider.is_day_divider()); + + assert_pending!(stream); +} From 351fbf60c17dfaca23c0c1e04df59f6a4c1cb9f0 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 7 Oct 2024 07:19:20 +0200 Subject: [PATCH 224/979] tests: serialize `Unsigned` with serde --- crates/matrix-sdk/src/test_utils/events.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index 5906b09abef..9ace4ccd49f 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -59,9 +59,11 @@ impl TimestampArg for u64 { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize)] struct Unsigned { + #[serde(skip_serializing_if = "Option::is_none")] transaction_id: Option, + #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")] relations: Option>>, } @@ -156,19 +158,7 @@ where } if let Some(unsigned) = self.unsigned { - let mut unsigned_json = json!({}); - - // We can't plain serialize `Unsigned`, otherwise this would result in some - // `null` fields when options are set to none, which Ruma rejects. - let unsigned_obj = unsigned_json.as_object_mut().unwrap(); - if let Some(transaction_id) = unsigned.transaction_id { - unsigned_obj.insert("transaction_id".to_owned(), json!(transaction_id)); - } - if let Some(relations) = unsigned.relations { - unsigned_obj.insert("m.relations".to_owned(), json!(relations)); - } - - map.insert("unsigned".to_owned(), unsigned_json); + map.insert("unsigned".to_owned(), json!(unsigned)); } if let Some(state_key) = self.state_key { From 93fce026068139f89a069993aee48f96fe50482e Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 7 Oct 2024 11:23:42 +0100 Subject: [PATCH 225/979] chore: Update Ruma to add media caption methods. fixup fixup --- Cargo.lock | 26 +++++----- Cargo.toml | 6 +-- bindings/matrix-sdk-ffi/src/room_list.rs | 4 +- .../matrix-sdk-ui/src/notification_client.rs | 2 +- .../tests/integration/room_list_service.rs | 51 +++++++++++++++++-- .../src/sliding_sync/list/sticky.rs | 2 +- labs/multiverse/src/main.rs | 2 +- 7 files changed, 69 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5b99265047..5bf59be5e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2264,9 +2264,9 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff6858c1f7e2a470c5403091866fa95b36fe0dbac5d771f932c15e5ff1ee501" +checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" dependencies = [ "log", "mac", @@ -2950,9 +2950,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d581ff8be69d08a2efa23a959d81aa22b739073f749f067348bd4f4ba4b69195" +checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" dependencies = [ "log", "phf", @@ -4965,7 +4965,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "assign", "js_int", @@ -4982,7 +4982,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "as_variant", "assign", @@ -5005,7 +5005,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "as_variant", "base64 0.22.1", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -5062,7 +5062,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "http", "js_int", @@ -5076,7 +5076,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.2.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "as_variant", "html5ever", @@ -5088,7 +5088,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "js_int", "thiserror", @@ -5097,7 +5097,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "cfg-if", "once_cell", @@ -5113,7 +5113,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index b83c6155104..f7cfc849063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ once_cell = "1.16.0" pin-project-lite = "0.2.9" rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } -ruma = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "b1c9a32f26f7aa76e20f96dbbb113250ed979112", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -61,7 +61,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "b1c9a32f26f7aa76e20f96dbbb113250ed979112" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" @@ -123,7 +123,7 @@ opt-level = 3 async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" } const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" } # Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937) -tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd"} +tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" } diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 7b518ec93b5..2f6f749c80f 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -716,7 +716,7 @@ pub struct RequiredState { #[derive(uniffi::Record)] pub struct RoomSubscription { pub required_state: Option>, - pub timeline_limit: Option, + pub timeline_limit: u32, pub include_heroes: Option, } @@ -726,7 +726,7 @@ impl From for http::request::RoomSubscription { required_state: val.required_state.map(|r| r.into_iter().map(|s| (s.key.into(), s.value)).collect() ).unwrap_or_default(), - timeline_limit: val.timeline_limit.map(|u| u.into()), + timeline_limit: val.timeline_limit.into(), include_heroes: val.include_heroes, }) } diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 1de35dfab59..f4774ec23f5 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -425,7 +425,7 @@ impl NotificationClient { &[room_id], Some(assign!(http::request::RoomSubscription::default(), { required_state, - timeline_limit: Some(uint!(16)) + timeline_limit: uint!(16) })), true, ); 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 b37ec389f9c..5090a22cdf0 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -376,6 +376,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 99]], + "timeline_limit": 0, }, }, }, @@ -400,6 +401,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 199]], + "timeline_limit": 0, }, }, }, @@ -424,6 +426,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 299]], + "timeline_limit": 0, }, }, }, @@ -448,6 +451,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 399]], + "timeline_limit": 0, }, }, }, @@ -483,6 +487,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -510,6 +515,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], + "timeline_limit": 0, }, }, }, @@ -537,6 +543,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], + "timeline_limit": 0, }, }, }, @@ -571,6 +578,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // The default range, in selective sync-mode. "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -595,6 +603,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Still the default range, in selective sync-mode. "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -618,6 +627,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode has changed to growing, with its initial range. "ranges": [[0, 99]], + "timeline_limit": 0, }, }, }, @@ -642,6 +652,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Due to previous error, the sync-mode is back to selective, with its initial range. "ranges": [[0, 19]], + "timeline_limit": 0, }, }, }, @@ -664,6 +675,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Sync-mode is now growing. "ranges": [[0, 99]], + "timeline_limit": 0, }, }, }, @@ -687,6 +699,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is still growing, and the range has made progress. "ranges": [[0, 199]], + "timeline_limit": 0, }, }, }, @@ -711,6 +724,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Due to previous error, the sync-mode is back to selective, with its initial range. "ranges": [[0, 19]], + "timeline_limit": 0, }, }, }, @@ -733,6 +747,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is now growing. "ranges": [[0, 99]], + "timeline_limit": 0, }, }, }, @@ -755,6 +770,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // No error. The range is making progress. "ranges": [[0, 199]], + "timeline_limit": 0, }, }, }, @@ -779,6 +795,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { // Range is making progress and is even reaching the maximum // number of rooms. "ranges": [[0, 209]], + "timeline_limit": 0, }, }, }, @@ -803,6 +820,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Due to previous error, the sync-mode is back to selective, with its initial range. "ranges": [[0, 19]], + "timeline_limit": 0, }, }, }, @@ -825,6 +843,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Sync-mode is now growing. "ranges": [[0, 99]], + "timeline_limit": 0, }, }, }, @@ -863,6 +882,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // The default range, in selective sync-mode. "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -893,6 +913,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is still selective, with its initial range. "ranges": [[0, 19]], + "timeline_limit": 0, }, }, }, @@ -916,6 +937,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is now growing, with its initial range. "ranges": [[0, 99]], + "timeline_limit": 0, }, }, }, @@ -947,6 +969,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is back to selective. "ranges": [[0, 19]], + "timeline_limit": 0, }, }, }, @@ -970,6 +993,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // Sync-mode is growing, with its initial range. "ranges": [[0, 99]], + "timeline_limit": 0, }, }, }, @@ -993,6 +1017,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // Range is making progress, and has reached its maximum. "ranges": [[0, 149]], + "timeline_limit": 0, }, }, }, @@ -1033,6 +1058,7 @@ async fn test_loading_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -1063,6 +1089,7 @@ async fn test_loading_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], + "timeline_limit": 0, }, }, }, @@ -1093,6 +1120,7 @@ async fn test_loading_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 11]], + "timeline_limit": 0, }, }, }, @@ -1140,6 +1168,7 @@ async fn test_loading_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -1186,6 +1215,7 @@ async fn test_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -1229,6 +1259,7 @@ async fn test_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], + "timeline_limit": 0, }, }, }, @@ -1277,6 +1308,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -1331,6 +1363,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], + "timeline_limit": 0, }, }, }, @@ -1440,6 +1473,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], + "timeline_limit": 0, }, }, }, @@ -1606,6 +1640,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], + "timeline_limit": 0, }, }, }, @@ -1676,6 +1711,7 @@ async fn test_room_sorting() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -1774,6 +1810,7 @@ async fn test_room_sorting() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 4]], + "timeline_limit": 0, }, }, }, @@ -1859,6 +1896,7 @@ async fn test_room_sorting() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 4]], + "timeline_limit": 0, }, }, }, @@ -1933,6 +1971,7 @@ async fn test_room_sorting() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 5]], + "timeline_limit": 0, }, }, }, @@ -2106,6 +2145,7 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -2141,7 +2181,7 @@ async fn test_room_subscription() -> Result<(), Error> { (StateEventType::RoomAvatar, "".to_owned()), (StateEventType::RoomCanonicalAlias, "".to_owned()), ], - timeline_limit: Some(uint!(30)), + timeline_limit: uint!(30), })), ); @@ -2151,6 +2191,7 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 2]], + "timeline_limit": 0, }, }, "room_subscriptions": { @@ -2184,7 +2225,7 @@ async fn test_room_subscription() -> Result<(), Error> { (StateEventType::RoomAvatar, "".to_owned()), (StateEventType::RoomCanonicalAlias, "".to_owned()), ], - timeline_limit: Some(uint!(30)), + timeline_limit: uint!(30), })), ); @@ -2194,6 +2235,7 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 2]], + "timeline_limit": 0, }, }, "room_subscriptions": { @@ -2227,7 +2269,7 @@ async fn test_room_subscription() -> Result<(), Error> { (StateEventType::RoomAvatar, "".to_owned()), (StateEventType::RoomCanonicalAlias, "".to_owned()), ], - timeline_limit: Some(uint!(30)), + timeline_limit: uint!(30), })), ); @@ -2240,6 +2282,7 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 2]], + "timeline_limit": 0, }, }, // NO `room_subscriptions`! @@ -2274,6 +2317,7 @@ async fn test_room_unread_notifications() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 19]], + "timeline_limit": 1, }, }, }, @@ -2305,6 +2349,7 @@ async fn test_room_unread_notifications() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 0]], + "timeline_limit": 0, }, }, }, diff --git a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs index ff6adb4f833..b0cb0bcdd1a 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs @@ -50,7 +50,7 @@ impl StickyData for SlidingSyncListStickyParameters { fn apply(&self, request: &mut Self::Request) { request.room_details.required_state = self.required_state.to_vec(); - request.room_details.timeline_limit = Some(self.timeline_limit.into()); + request.room_details.timeline_limit = self.timeline_limit.into(); request.include_heroes = self.include_heroes; request.filters = self.filters.clone(); } diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 3829920569e..4a8f7938f7d 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -411,7 +411,7 @@ impl App { .and_then(|room_id| self.ui_rooms.lock().unwrap().get(&room_id).cloned()) { let mut sub = RoomSubscription::default(); - sub.timeline_limit = Some(uint!(30)); + sub.timeline_limit = uint!(30); self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()], Some(sub)); self.current_room_subscription = Some(room); From a12a46b77706bf9ff4f034c50668b06d079dd296 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 7 Oct 2024 11:24:20 +0100 Subject: [PATCH 226/979] ffi: Add caption/formatted_caption to media timeline items. Also includes the computed filename too. --- bindings/matrix-sdk-ffi/src/ruma.rs | 76 ++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 7072270d8e8..20c00e62025 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -277,7 +277,7 @@ impl TryFrom for RumaMessageType { RumaImageMessageEventContent::new(content.body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.filename; + event_content.filename = content.raw_filename; Self::Image(event_content) } MessageType::Audio { content } => { @@ -285,7 +285,7 @@ impl TryFrom for RumaMessageType { RumaAudioMessageEventContent::new(content.body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.filename; + event_content.filename = content.raw_filename; Self::Audio(event_content) } MessageType::Video { content } => { @@ -293,7 +293,7 @@ impl TryFrom for RumaMessageType { RumaVideoMessageEventContent::new(content.body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.filename; + event_content.filename = content.raw_filename; Self::Video(event_content) } MessageType::File { content } => { @@ -301,7 +301,7 @@ impl TryFrom for RumaMessageType { RumaFileMessageEventContent::new(content.body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.filename; + event_content.filename = content.raw_filename; Self::File(event_content) } MessageType::Notice { content } => { @@ -337,7 +337,10 @@ impl From for MessageType { content: ImageMessageContent { body: c.body.clone(), formatted: c.formatted.as_ref().map(Into::into), - filename: c.filename.clone(), + raw_filename: c.filename.clone(), + filename: c.filename().to_owned(), + caption: c.caption().map(ToString::to_string), + formatted_caption: c.formatted_caption().map(Into::into), source: Arc::new(c.source.clone()), info: c.info.as_deref().map(Into::into), }, @@ -346,7 +349,10 @@ impl From for MessageType { content: AudioMessageContent { body: c.body.clone(), formatted: c.formatted.as_ref().map(Into::into), - filename: c.filename.clone(), + raw_filename: c.filename.clone(), + filename: c.filename().to_owned(), + caption: c.caption().map(ToString::to_string), + formatted_caption: c.formatted_caption().map(Into::into), source: Arc::new(c.source.clone()), info: c.info.as_deref().map(Into::into), audio: c.audio.map(Into::into), @@ -357,7 +363,10 @@ impl From for MessageType { content: VideoMessageContent { body: c.body.clone(), formatted: c.formatted.as_ref().map(Into::into), - filename: c.filename.clone(), + raw_filename: c.filename.clone(), + filename: c.filename().to_owned(), + caption: c.caption().map(ToString::to_string), + formatted_caption: c.formatted_caption().map(Into::into), source: Arc::new(c.source.clone()), info: c.info.as_deref().map(Into::into), }, @@ -366,7 +375,10 @@ impl From for MessageType { content: FileMessageContent { body: c.body.clone(), formatted: c.formatted.as_ref().map(Into::into), - filename: c.filename.clone(), + raw_filename: c.filename.clone(), + filename: c.filename().to_owned(), + caption: c.caption().map(ToString::to_string), + formatted_caption: c.formatted_caption().map(Into::into), source: Arc::new(c.source.clone()), info: c.info.as_deref().map(Into::into), }, @@ -440,18 +452,38 @@ pub struct EmoteMessageContent { #[derive(Clone, uniffi::Record)] pub struct ImageMessageContent { + /// The original body field, deserialized from the event. Prefer the use of + /// `filename` and `caption` over this. pub body: String, + /// The original formatted body field, deserialized from the event. Prefer + /// the use of `filename` and `formatted_caption` over this. pub formatted: Option, - pub filename: Option, + /// The original filename field, deserialized from the event. Prefer the use + /// of `filename` over this. + pub raw_filename: Option, + /// The computed filename, for use in a client. + pub filename: String, + pub caption: Option, + pub formatted_caption: Option, pub source: Arc, pub info: Option, } #[derive(Clone, uniffi::Record)] pub struct AudioMessageContent { + /// The original body field, deserialized from the event. Prefer the use of + /// `filename` and `caption` over this. pub body: String, + /// The original formatted body field, deserialized from the event. Prefer + /// the use of `filename` and `formatted_caption` over this. pub formatted: Option, - pub filename: Option, + /// The original filename field, deserialized from the event. Prefer the use + /// of `filename` over this. + pub raw_filename: Option, + /// The computed filename, for use in a client. + pub filename: String, + pub caption: Option, + pub formatted_caption: Option, pub source: Arc, pub info: Option, pub audio: Option, @@ -460,18 +492,38 @@ pub struct AudioMessageContent { #[derive(Clone, uniffi::Record)] pub struct VideoMessageContent { + /// The original body field, deserialized from the event. Prefer the use of + /// `filename` and `caption` over this. pub body: String, + /// The original formatted body field, deserialized from the event. Prefer + /// the use of `filename` and `formatted_caption` over this. pub formatted: Option, - pub filename: Option, + /// The original filename field, deserialized from the event. Prefer the use + /// of `filename` over this. + pub raw_filename: Option, + /// The computed filename, for use in a client. + pub filename: String, + pub caption: Option, + pub formatted_caption: Option, pub source: Arc, pub info: Option, } #[derive(Clone, uniffi::Record)] pub struct FileMessageContent { + /// The original body field, deserialized from the event. Prefer the use of + /// `filename` and `caption` over this. pub body: String, + /// The original formatted body field, deserialized from the event. Prefer + /// the use of `filename` and `formatted_caption` over this. pub formatted: Option, - pub filename: Option, + /// The original filename field, deserialized from the event. Prefer the use + /// of `filename` over this. + pub raw_filename: Option, + /// The computed filename, for use in a client. + pub filename: String, + pub caption: Option, + pub formatted_caption: Option, pub source: Arc, pub info: Option, } From 181ee643b1665623c927e3af52e7f9d824d52482 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 4 Oct 2024 12:03:04 +0100 Subject: [PATCH 227/979] crypto: Expose a way to pin a user's identity --- bindings/matrix-sdk-ffi/CHANGELOG.md | 1 + bindings/matrix-sdk-ffi/src/encryption.rs | 44 +++++++++++++++++++ .../matrix-sdk-crypto/src/identities/user.rs | 42 +++++++++++++++++- crates/matrix-sdk/CHANGELOG.md | 1 + .../src/encryption/identities/users.rs | 18 ++++++++ .../src/room/identity_status_changes.rs | 5 ++- 6 files changed, 108 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 44eaaa65eff..55a9474eb61 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -31,4 +31,5 @@ Breaking changes: Additions: +- Add `Encryption::get_user_identity` - Add `ClientBuilder::room_key_recipient_strategy` diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 34a0e794941..501097483fa 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -410,6 +410,50 @@ impl Encryption { pub async fn wait_for_e2ee_initialization_tasks(&self) { self.inner.wait_for_e2ee_initialization_tasks().await; } + + /// Get the E2EE identity of a user. + /// + /// Returns an error if this user does not exist, if there is an error + /// contacting the crypto store, or if our client is not logged in. + pub async fn get_user_identity( + &self, + user_id: String, + ) -> Result, ClientError> { + Ok(Arc::new(UserIdentity { + inner: self + .inner + .get_user_identity(user_id.as_str().try_into()?) + .await? + .ok_or(ClientError::new("User not found"))?, + })) + } +} + +/// The E2EE identity of a user. +#[derive(uniffi::Object)] +pub struct UserIdentity { + inner: matrix_sdk::encryption::identities::UserIdentity, +} + +#[uniffi::export] +impl UserIdentity { + /// Remember this identity, ensuring it does not result in a pin violation. + /// + /// When we first see a user, we assume their cryptographic identity has not + /// been tampered with by the homeserver or another entity with + /// man-in-the-middle capabilities. We remember this identity and call this + /// action "pinning". + /// + /// If the identity presented for the user changes later on, the newly + /// presented identity is considered to be in "pin violation". This + /// method explicitly accepts the new identity, allowing it to replace + /// the previously pinned one and bringing it out of pin violation. + /// + /// UIs should display a warning to the user when encountering an identity + /// which is not verified and is in pin violation. + pub(crate) async fn pin(&self) -> Result<(), ClientError> { + Ok(self.inner.pin().await?) + } } #[derive(uniffi::Object)] diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index e70c1201383..96c7e5677d5 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -125,6 +125,32 @@ impl UserIdentity { } } + /// Remember this identity, ensuring it does not result in a pin violation. + /// + /// When we first see a user, we assume their cryptographic identity has not + /// been tampered with by the homeserver or another entity with + /// man-in-the-middle capabilities. We remember this identity and call this + /// action "pinning". + /// + /// If the identity presented for the user changes later on, the newly + /// presented identity is considered to be in "pin violation". This + /// method explicitly accepts the new identity, allowing it to replace + /// the previously pinned one and bringing it out of pin violation. + /// + /// UIs should display a warning to the user when encountering an identity + /// which is not verified and is in pin violation. See + /// [`OtherUserIdentity::identity_needs_user_approval`]. + pub async fn pin(&self) -> Result<(), CryptoStoreError> { + match self { + UserIdentity::Own(_) => { + // Nothing to be done for our own identity: we already + // consider it trusted in this sense. + Ok(()) + } + UserIdentity::Other(u) => u.pin_current_master_key().await, + } + } + /// Was this identity previously verified, and is no longer? pub fn has_verification_violation(&self) -> bool { match self { @@ -737,7 +763,21 @@ impl OtherUserIdentityData { &self.self_signing_key } - /// Pin the current identity + /// Remember this identity, ensuring it does not result in a pin violation. + /// + /// When we first see a user, we assume their cryptographic identity has not + /// been tampered with by the homeserver or another entity with + /// man-in-the-middle capabilities. We remember this identity and call this + /// action "pinning". + /// + /// If the identity presented for the user changes later on, the newly + /// presented identity is considered to be in "pin violation". This + /// method explicitly accepts the new identity, allowing it to replace + /// the previously pinned one and bringing it out of pin violation. + /// + /// UIs should display a warning to the user when encountering an identity + /// which is not verified and is in pin violation. See + /// [`OtherUserIdentity::identity_needs_user_approval`]. pub(crate) fn pin(&self) { let mut m = self.pinned_master_key.write().unwrap(); *m = self.master_key.as_ref().clone() diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 8610707e991..915838e009d 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -28,6 +28,7 @@ Breaking changes: Additions: +- new `UserIdentity::pin` method. - new `ClientBuilder::with_decryption_trust_requirement` method. - new `ClientBuilder::with_room_key_recipient_strategy` method - new `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData setters, analogous to the GlobalAccountData diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index d71ca1e7e23..0ce4e303e5d 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -422,6 +422,24 @@ impl UserIdentity { self.inner.withdraw_verification().await } + /// Remember this identity, ensuring it does not result in a pin violation. + /// + /// When we first see a user, we assume their cryptographic identity has not + /// been tampered with by the homeserver or another entity with + /// man-in-the-middle capabilities. We remember this identity and call this + /// action "pinning". + /// + /// If the identity presented for the user changes later on, the newly + /// presented identity is considered to be in "pin violation". This + /// method explicitly accepts the new identity, allowing it to replace + /// the previously pinned one and bringing it out of pin violation. + /// + /// UIs should display a warning to the user when encountering an identity + /// which is not verified and is in pin violation. + pub async fn pin(&self) -> Result<(), CryptoStoreError> { + self.inner.pin().await + } + /// Get the public part of the Master key of this user identity. /// /// The public part of the Master key is usually used to uniquely identify diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index ce93f326a46..feab79e416a 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -430,9 +430,10 @@ mod tests { ); // Pin it - self.crypto_other_identity() + self.user_identity() .await - .pin_current_master_key() + .expect("User should exist") + .pin() .await .expect("Should not fail to pin"); } else { From 6c7acf6faa58fb131c6ad53b768e5f8d115cd0ef Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 4 Oct 2024 15:13:37 +0100 Subject: [PATCH 228/979] ffi: Expose the master_key method on UserIdentity --- bindings/matrix-sdk-ffi/CHANGELOG.md | 2 +- bindings/matrix-sdk-ffi/src/encryption.rs | 27 ++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 55a9474eb61..922f8f305c7 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -31,5 +31,5 @@ Breaking changes: Additions: -- Add `Encryption::get_user_identity` +- Add `Encryption::get_user_identity` which returns `UserIdentity` - Add `ClientBuilder::room_key_recipient_strategy` diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 501097483fa..579387b485e 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -413,19 +413,16 @@ impl Encryption { /// Get the E2EE identity of a user. /// - /// Returns an error if this user does not exist, if there is an error - /// contacting the crypto store, or if our client is not logged in. + /// Returns Ok(None) if this user does not exist. + /// + /// Returns an error if there was a problem contacting the crypto store, or + /// if our client is not logged in. pub async fn get_user_identity( &self, user_id: String, - ) -> Result, ClientError> { - Ok(Arc::new(UserIdentity { - inner: self - .inner - .get_user_identity(user_id.as_str().try_into()?) - .await? - .ok_or(ClientError::new("User not found"))?, - })) + ) -> Result>, ClientError> { + let identity = self.inner.get_user_identity(user_id.as_str().try_into()?).await?; + Ok(identity.map(|i| Arc::new(UserIdentity { inner: i }))) } } @@ -454,6 +451,16 @@ impl UserIdentity { pub(crate) async fn pin(&self) -> Result<(), ClientError> { Ok(self.inner.pin().await?) } + + /// Get the public part of the Master key of this user identity. + /// + /// The public part of the Master key is usually used to uniquely identify + /// the identity. + /// + /// Returns None if the master key does not actually contain any keys. + pub(crate) fn master_key(&self) -> Option { + self.inner.master_key().get_first_key().map(|k| k.to_base64()) + } } #[derive(uniffi::Object)] From 4cbc1629645fc71579bbcdcc896e86f4077a40b8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 7 Oct 2024 08:19:56 +0200 Subject: [PATCH 229/979] timeline: update replies when a message has been edited --- .../src/timeline/event_handler.rs | 33 ++++- .../tests/integration/timeline/edit.rs | 127 +++++++++++++++++- 2 files changed, 155 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 219ef6bc2b2..0550ea94158 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -68,6 +68,7 @@ use crate::{ controller::PendingEdit, event_item::{ReactionInfo, ReactionStatus}, reactions::PendingReaction, + RepliedToEvent, }, }; @@ -511,9 +512,33 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { ) { if let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) { let edit_json = self.ctx.flow.raw_event().cloned(); - if let Some(new_item) = self.apply_msg_edit(&item, replacement, edit_json) { + if let Some(new_item) = self.apply_msg_edit(&item, replacement.new_content, edit_json) { trace!("Applied edit"); - self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); + + let internal_id = item.internal_id.to_owned(); + + // Update all events that replied to this message with the edited content. + self.items.for_each(|mut entry| { + let Some(event_item) = entry.as_event() else { return }; + let Some(message) = event_item.content.as_message() else { return }; + let Some(in_reply_to) = message.in_reply_to() else { return }; + if replacement.event_id == in_reply_to.event_id { + let in_reply_to = InReplyToDetails { + event_id: in_reply_to.event_id.clone(), + event: TimelineDetails::Ready(Box::new( + RepliedToEvent::from_timeline_item(&new_item), + )), + }; + let new_reply_content = + TimelineItemContent::Message(message.with_in_reply_to(in_reply_to)); + let new_reply_item = + entry.with_kind(event_item.with_content(new_reply_content, None)); + ObservableVectorTransactionEntry::set(&mut entry, new_reply_item); + } + }); + + // Update the event itself. + self.items.set(item_pos, TimelineItem::new(new_item, internal_id)); self.result.items_updated += 1; } } else if let Flow::Remote { position, raw_event, .. } = &self.ctx.flow { @@ -589,7 +614,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { fn apply_msg_edit( &self, item: &EventTimelineItem, - replacement: Replacement, + new_content: RoomMessageEventContentWithoutRelation, edit_json: Option>, ) -> Option { if self.ctx.sender != item.sender() { @@ -609,7 +634,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { }; let mut new_msg = msg.clone(); - new_msg.apply_edit(replacement.new_content); + new_msg.apply_edit(new_content); let mut new_item = item.with_content(TimelineItemContent::Message(new_msg), edit_json); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 0ad3b7353db..a4fed5da95c 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -410,7 +410,7 @@ async fn test_send_reply_edit() { .mount(&server) .await; - timeline + let edited = timeline .edit( &reply_item, EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( @@ -419,6 +419,7 @@ async fn test_send_reply_edit() { ) .await .unwrap(); + assert!(edited); // Let the send queue handle the event. yield_now().await; @@ -444,6 +445,130 @@ async fn test_send_reply_edit() { server.verify().await; } +#[async_test] +async fn test_edit_to_replied_updates_reply() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = + timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; + + let f = EventFactory::new(); + let event_id = event_id!("$original_event"); + let user_id = client.user_id().unwrap(); + + // When a room has two messages, one is a reply to the other… + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("bonjour").sender(user_id).event_id(event_id)) + .add_timeline_event(f.text_msg("hi back").reply_to(event_id).sender(*ALICE)) + .add_timeline_event(f.text_msg("yo").reply_to(event_id).sender(*BOB)), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // (I see all the messages in the timeline.) + let replied_to_item = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { + assert_eq!(value.content().as_message().unwrap().body(), "bonjour"); + assert!(value.is_editable()); + value + }); + + assert_next_matches!(timeline_stream, VectorDiff::PushBack { value: reply_item } => { + let reply_message = reply_item.content().as_message().unwrap(); + assert_eq!(reply_message.body(), "hi back"); + + let in_reply_to = reply_message.in_reply_to().unwrap(); + assert_eq!(in_reply_to.event_id, event_id); + + assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event); + assert_eq!(replied_to.content().as_message().unwrap().body(), "bonjour"); + }); + + assert_next_matches!(timeline_stream, VectorDiff::PushBack { value: reply_item } => { + let reply_message = reply_item.content().as_message().unwrap(); + assert_eq!(reply_message.body(), "yo"); + + let in_reply_to = reply_message.in_reply_to().unwrap(); + assert_eq!(in_reply_to.event_id, event_id); + + assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event); + assert_eq!(replied_to.content().as_message().unwrap().body(), "bonjour"); + }); + + mock_encryption_state(&server, false).await; + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$edit_event" })), + ) + .expect(1) + .mount(&server) + .await; + + // If I edit the first message,… + let edited = timeline + .edit( + &replied_to_item, + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "hello world", + )), + ) + .await + .unwrap(); + assert!(edited); + + yield_now().await; // let the send queue handle the edit. + + // The reply events are updated with the edited replied-to content. + assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => { + let reply_message = value.content().as_message().unwrap(); + assert_eq!(reply_message.body(), "hi back"); + assert!(!reply_message.is_edited()); + + let in_reply_to = reply_message.in_reply_to().unwrap(); + assert_eq!(in_reply_to.event_id, event_id); + assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event); + assert_eq!(replied_to.content().as_message().unwrap().body(), "hello world"); + }); + + assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => { + let reply_message = value.content().as_message().unwrap(); + assert_eq!(reply_message.body(), "yo"); + assert!(!reply_message.is_edited()); + + let in_reply_to = reply_message.in_reply_to().unwrap(); + assert_eq!(in_reply_to.event_id, event_id); + assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event); + assert_eq!(replied_to.content().as_message().unwrap().body(), "hello world"); + }); + + // And the edit happens. + assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => { + let msg = value.content().as_message().unwrap(); + assert_eq!(msg.body(), "hello world"); + assert!(msg.is_edited()); + }); + + sleep(Duration::from_millis(200)).await; + + server.verify().await; +} + #[async_test] async fn test_send_edit_poll() { let room_id = room_id!("!a98sd12bjh:example.org"); From 6f0fbf92e4bf9a00eefbb450d5021086a1088f01 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 7 Oct 2024 13:28:12 +0200 Subject: [PATCH 230/979] Revert "Revert "chore(ui,ffi): Remove the `RoomList::entries` method."" This reverts commit af390328b58ccbbf0fb7687e8f6b4aa090676800. --- bindings/matrix-sdk-ffi/src/room_list.rs | 27 ------ .../src/room_list_service/mod.rs | 6 +- .../src/room_list_service/room_list.rs | 12 ++- .../tests/integration/room_list_service.rs | 92 ------------------- 4 files changed, 10 insertions(+), 127 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 2f6f749c80f..b9e4dca9623 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -182,33 +182,6 @@ impl RoomList { }) } - fn entries(&self, listener: Box) -> Arc { - let this = self.inner.clone(); - let utd_hook = self.room_list_service.utd_hook.clone(); - - Arc::new(TaskHandle::new(RUNTIME.spawn(async move { - let (entries, entries_stream) = this.entries(); - - pin_mut!(entries_stream); - - listener.on_update(vec![RoomListEntriesUpdate::Append { - values: entries - .into_iter() - .map(|room| Arc::new(RoomListItem::from(room, utd_hook.clone()))) - .collect(), - }]); - - while let Some(diffs) = entries_stream.next().await { - listener.on_update( - diffs - .into_iter() - .map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone())) - .collect(), - ); - } - }))) - } - fn entries_with_dynamic_adapters( self: Arc, page_size: u32, diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index f6045837f9e..7f3f27b5aa1 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -44,9 +44,9 @@ //! fluid user experience for a Matrix client. //! //! [`RoomListService::all_rooms`] provides a way to get a [`RoomList`] for all -//! the rooms. From that, calling [`RoomList::entries`] provides a way to get a -//! stream of room list entry. This stream can be filtered, and the filter can -//! be changed over time. +//! the rooms. From that, calling [`RoomList::entries_with_dynamic_adapters`] +//! provides a way to get a stream of rooms. This stream is sorted, can be +//! filtered, and the filter can be changed over time. //! //! [`RoomListService::state`] provides a way to get a stream of the state //! machine's state, which can be pretty helpful for the client app. diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index 0cde4bbcd40..8b4ecf9ab4c 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -118,8 +118,8 @@ impl RoomList { self.loading_state.subscribe() } - /// Get all previous rooms, in addition to a [`Stream`] to rooms' updates. - pub fn entries(&self) -> (Vector, impl Stream>> + '_) { + /// Get a stream of rooms. + fn entries(&self) -> (Vector, impl Stream>> + '_) { let (rooms, stream) = self.client.rooms_stream(); let map_room = |room| Room::new(room, &self.sliding_sync); @@ -130,9 +130,11 @@ impl RoomList { ) } - /// Similar to [`Self::entries`] except that it's possible to provide a - /// filter that will filter out room list entries, and that it's also - /// possible to “paginate” over the entries by `page_size`. + /// Get a configurable stream of rooms. + /// + /// It's possible to provide a filter that will filter out room list + /// entries, and that it's also possible to “paginate” over the entries by + /// `page_size`. The rooms are also sorted. /// /// The returned stream will only start yielding diffs once a filter is set /// through the returned [`RoomListDynamicEntriesController`]. For every 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 5090a22cdf0..12c2bd03668 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -1196,98 +1196,6 @@ async fn test_loading_states() -> Result<(), Error> { Ok(()) } -#[async_test] -async fn test_entries_stream() -> Result<(), Error> { - let (_, server, room_list) = new_room_list_service().await?; - - let sync = room_list.sync(); - pin_mut!(sync); - - let all_rooms = room_list.all_rooms().await?; - - let (previous_entries, entries_stream) = all_rooms.entries(); - pin_mut!(entries_stream); - - sync_then_assert_request_and_fake_response! { - [server, room_list, sync] - states = Init => SettingUp, - assert request >= { - "lists": { - ALL_ROOMS: { - "ranges": [[0, 19]], - "timeline_limit": 1, - }, - }, - }, - respond with = { - "pos": "0", - "lists": { - ALL_ROOMS: { - "count": 10, - }, - }, - "rooms": { - "!r0:bar.org": { - "initial": true, - "timeline": [], - }, - "!r1:bar.org": { - "initial": true, - "timeline": [], - }, - "!r2:bar.org": { - "initial": true, - "timeline": [], - }, - }, - }, - }; - - assert!(previous_entries.is_empty()); - assert_entries_batch! { - [entries_stream] - push back [ "!r0:bar.org" ]; - push back [ "!r1:bar.org" ]; - push back [ "!r2:bar.org" ]; - end; - }; - - sync_then_assert_request_and_fake_response! { - [server, room_list, sync] - states = SettingUp => Running, - assert request >= { - "lists": { - ALL_ROOMS: { - "ranges": [[0, 9]], - "timeline_limit": 0, - }, - }, - }, - respond with = { - "pos": "1", - "lists": { - ALL_ROOMS: { - "count": 9, - }, - }, - "rooms": { - "!r3:bar.org": { - "initial": true, - "timeline": [], - }, - }, - }, - }; - - assert_entries_batch! { - [entries_stream] - push back [ "!r3:bar.org" ]; - end; - }; - - Ok(()) -} - #[async_test] async fn test_dynamic_entries_stream() -> Result<(), Error> { let (client, server, room_list) = new_room_list_service().await?; From 2967b73aff9edf6a1798b22f5198cc47094618a7 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 7 Oct 2024 16:47:11 +0300 Subject: [PATCH 231/979] ci: speed up iOS bindings tests by building them on the `dev` profile - speed regression introduced when switching the default bindings profile to `reldbg` in #4020 --- .github/workflows/bindings_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index a8b1636ef1b..9fa0bc84e13 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -175,7 +175,7 @@ jobs: run: swift test - name: Build Framework - run: target/debug/xtask swift build-framework --target=aarch64-apple-ios + run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev complement-crypto: name: "Run Complement Crypto tests" From ff7e8c75ee320eec483d329f9f2baa9148e0bd48 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 7 Oct 2024 15:58:37 +0200 Subject: [PATCH 232/979] ci: try using macos-14 runners for swift-related tasks --- .github/workflows/bindings_ci.yml | 4 ++-- .github/workflows/xtask.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 9fa0bc84e13..04268861f2d 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -131,7 +131,7 @@ jobs: test-apple: name: matrix-rust-components-swift needs: xtask - runs-on: macos-12 + runs-on: macos-14 if: github.event_name == 'push' || !github.event.pull_request.draft steps: @@ -186,7 +186,7 @@ jobs: test-crypto-apple-framework-generation: name: Generate Crypto FFI Apple XCFramework - runs-on: macos-12 + runs-on: macos-14 if: github.event_name == 'push' || !github.event.pull_request.draft steps: diff --git a/.github/workflows/xtask.yml b/.github/workflows/xtask.yml index a5398d580a5..6f369b4cc1c 100644 --- a/.github/workflows/xtask.yml +++ b/.github/workflows/xtask.yml @@ -35,7 +35,7 @@ jobs: os-name: 🐧 cachekey-id: linux - - os: macos-12 + - os: macos-14 os-name: 🍏 cachekey-id: macos From a9cfba2c03542b05cd884dc0c3fd5292a327cbaf Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 27 Sep 2024 08:21:16 +0200 Subject: [PATCH 233/979] chore(cargo): Update `ruma`. --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5bf59be5e26..0f672765b80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4965,7 +4965,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "assign", "js_int", @@ -4982,7 +4982,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", "assign", @@ -5005,7 +5005,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", "base64 0.22.1", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -5062,7 +5062,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "http", "js_int", @@ -5076,7 +5076,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.2.0" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", "html5ever", @@ -5088,7 +5088,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "js_int", "thiserror", @@ -5097,7 +5097,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "cfg-if", "once_cell", @@ -5113,7 +5113,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=b1c9a32f26f7aa76e20f96dbbb113250ed979112#b1c9a32f26f7aa76e20f96dbbb113250ed979112" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index f7cfc849063..456d92b226a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ once_cell = "1.16.0" pin-project-lite = "0.2.9" rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } -ruma = { git = "https://github.com/ruma/ruma", rev = "b1c9a32f26f7aa76e20f96dbbb113250ed979112", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -61,7 +61,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "b1c9a32f26f7aa76e20f96dbbb "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "b1c9a32f26f7aa76e20f96dbbb113250ed979112" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" From 4d45b02e916df490b506ab33f49f85fa14d9b7dd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 7 Oct 2024 15:31:42 +0200 Subject: [PATCH 234/979] fix(ui): Consider `timeline_limit` in sliding sync as non-sticky. This patch changes the behaviour of `timeline_limit` in sliding sync requests. It previously was sticky, but since it's now mandatory with MSC4186, it's preferable it to be non-sticky, otherwise in some scenarios it might default to 0 (its default value). How? If the server doesn't reply with our `txn_id` (because it doesn't support sticky parameters or because it misses a `txn_id`), the next request will be built with a default `timeline_limit` value, which is zero, and won't get updated to the `timeline_limit` value from `SlidingSyncListStickyParameters`. This is not good. Instead, we must consider `timeline_limit` as non-sticky, and moves it from `SlidingSyncListStickyParameters` to `SlidingSyncListInner`. This is what this patch does. --- .../tests/integration/room_list_service.rs | 66 +++++++++---------- .../src/sliding_sync/list/builder.rs | 2 +- .../matrix-sdk/src/sliding_sync/list/mod.rs | 18 ++--- .../src/sliding_sync/list/sticky.rs | 18 +---- 4 files changed, 45 insertions(+), 59 deletions(-) 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 12c2bd03668..674cbddbd59 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -376,7 +376,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 99]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -401,7 +401,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 199]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -426,7 +426,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 299]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -451,7 +451,7 @@ async fn test_sync_all_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 399]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -515,7 +515,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -543,7 +543,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -627,7 +627,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode has changed to growing, with its initial range. "ranges": [[0, 99]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -652,7 +652,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Due to previous error, the sync-mode is back to selective, with its initial range. "ranges": [[0, 19]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -675,7 +675,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Sync-mode is now growing. "ranges": [[0, 99]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -699,7 +699,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is still growing, and the range has made progress. "ranges": [[0, 199]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -724,7 +724,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Due to previous error, the sync-mode is back to selective, with its initial range. "ranges": [[0, 19]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -747,7 +747,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is now growing. "ranges": [[0, 99]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -770,7 +770,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // No error. The range is making progress. "ranges": [[0, 199]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -795,7 +795,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { // Range is making progress and is even reaching the maximum // number of rooms. "ranges": [[0, 209]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -820,7 +820,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Due to previous error, the sync-mode is back to selective, with its initial range. "ranges": [[0, 19]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -843,7 +843,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { ALL_ROOMS: { // Sync-mode is now growing. "ranges": [[0, 99]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -913,7 +913,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is still selective, with its initial range. "ranges": [[0, 19]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -937,7 +937,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is now growing, with its initial range. "ranges": [[0, 99]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -969,7 +969,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // The sync-mode is back to selective. "ranges": [[0, 19]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -993,7 +993,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // Sync-mode is growing, with its initial range. "ranges": [[0, 99]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1017,7 +1017,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { ALL_ROOMS: { // Range is making progress, and has reached its maximum. "ranges": [[0, 149]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1089,7 +1089,7 @@ async fn test_loading_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1120,7 +1120,7 @@ async fn test_loading_states() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 11]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1271,7 +1271,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1381,7 +1381,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1548,7 +1548,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 9]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1718,7 +1718,7 @@ async fn test_room_sorting() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 4]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1804,7 +1804,7 @@ async fn test_room_sorting() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 4]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -1879,7 +1879,7 @@ async fn test_room_sorting() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 5]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, @@ -2099,7 +2099,7 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 2]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, "room_subscriptions": { @@ -2143,7 +2143,7 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 2]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, "room_subscriptions": { @@ -2190,7 +2190,7 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 2]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, // NO `room_subscriptions`! @@ -2257,7 +2257,7 @@ async fn test_room_unread_notifications() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 0]], - "timeline_limit": 0, + "timeline_limit": 1, }, }, }, diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs index fe4b2d1638a..312acbe93de 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -177,9 +177,9 @@ impl SlidingSyncListBuilder { self.required_state, self.include_heroes, self.filters, - self.timeline_limit, ), )), + timeline_limit: StdRwLock::new(self.timeline_limit), name: self.name, cache_policy: self.cache_policy, diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index c0e07a1d8cb..a3b3ec3b0f4 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -133,12 +133,12 @@ impl SlidingSyncList { /// Get the timeline limit. pub fn timeline_limit(&self) -> Bound { - self.inner.sticky.read().unwrap().data().timeline_limit() + *self.inner.timeline_limit.read().unwrap() } /// Set timeline limit. pub fn set_timeline_limit(&self, timeline: Bound) { - self.inner.sticky.write().unwrap().data_mut().set_timeline_limit(timeline); + *self.inner.timeline_limit.write().unwrap() = timeline; } /// Get the maximum number of rooms. See [`Self::maximum_number_of_rooms`] @@ -233,6 +233,9 @@ pub(super) struct SlidingSyncListInner { /// knows). sticky: StdRwLock>, + /// The maximum number of timeline events to query for. + timeline_limit: StdRwLock, + /// The total number of rooms that is possible to interact with for the /// given list. /// @@ -307,12 +310,11 @@ impl SlidingSyncListInner { /// request generator. #[instrument(skip(self), fields(name = self.name))] fn request(&self, ranges: Ranges, txn_id: &mut LazyTransactionId) -> http::request::List { - use ruma::UInt; - - let ranges = - ranges.into_iter().map(|r| (UInt::from(*r.start()), UInt::from(*r.end()))).collect(); + let ranges = ranges.into_iter().map(|r| ((*r.start()).into(), (*r.end()).into())).collect(); let mut request = assign!(http::request::List::default(), { ranges }); + request.room_details.timeline_limit = (*self.timeline_limit.read().unwrap()).into(); + { let mut sticky = self.sticky.write().unwrap(); sticky.maybe_apply(&mut request, txn_id); @@ -594,10 +596,10 @@ mod tests { .timeline_limit(7) .build(sender); - assert_eq!(list.inner.sticky.read().unwrap().data().timeline_limit(), 7); + assert_eq!(list.timeline_limit(), 7); list.set_timeline_limit(42); - assert_eq!(list.inner.sticky.read().unwrap().data().timeline_limit(), 42); + assert_eq!(list.timeline_limit(), 42); } macro_rules! assert_ranges { diff --git a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs index b0cb0bcdd1a..f5b12798be8 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs @@ -1,7 +1,6 @@ use matrix_sdk_base::sliding_sync::http; use ruma::events::StateEventType; -use super::Bound; use crate::sliding_sync::sticky_parameters::StickyData; /// The set of `SlidingSyncList` request parameters that are *sticky*, as @@ -17,9 +16,6 @@ pub(super) struct SlidingSyncListStickyParameters { /// Any filters to apply to the query. filters: Option, - - /// The maximum number of timeline events to query for. - timeline_limit: Bound, } impl SlidingSyncListStickyParameters { @@ -27,21 +23,10 @@ impl SlidingSyncListStickyParameters { required_state: Vec<(StateEventType, String)>, include_heroes: Option, filters: Option, - timeline_limit: Bound, ) -> Self { // Consider that each list will have at least one parameter set, so invalidate // it by default. - Self { required_state, include_heroes, filters, timeline_limit } - } -} - -impl SlidingSyncListStickyParameters { - pub(super) fn timeline_limit(&self) -> Bound { - self.timeline_limit - } - - pub(super) fn set_timeline_limit(&mut self, timeline: Bound) { - self.timeline_limit = timeline; + Self { required_state, include_heroes, filters } } } @@ -50,7 +35,6 @@ impl StickyData for SlidingSyncListStickyParameters { fn apply(&self, request: &mut Self::Request) { request.room_details.required_state = self.required_state.to_vec(); - request.room_details.timeline_limit = self.timeline_limit.into(); request.include_heroes = self.include_heroes; request.filters = self.filters.clone(); } From 19b9a73ecc3e31d502dbf0c5850bfdfaddf02afe Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 8 Oct 2024 10:56:08 +0200 Subject: [PATCH 235/979] ffi: add async_runtime annotation for impl block with async fun --- bindings/matrix-sdk-ffi/src/encryption.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 579387b485e..7f6e35b4aac 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -432,7 +432,7 @@ pub struct UserIdentity { inner: matrix_sdk::encryption::identities::UserIdentity, } -#[uniffi::export] +#[uniffi::export(async_runtime = "tokio")] impl UserIdentity { /// Remember this identity, ensuring it does not result in a pin violation. /// From 1fc3450eacf269b994b6bdccabb8ad4c9adb70d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 27 Sep 2024 09:59:10 +0200 Subject: [PATCH 236/979] ffi & sdk: add room knocking to Client --- bindings/matrix-sdk-ffi/src/client.rs | 7 +++++ bindings/matrix-sdk-ffi/src/room.rs | 2 ++ crates/matrix-sdk-base/src/client.rs | 26 +++++++++++++++++++ crates/matrix-sdk-base/src/rooms/normal.rs | 17 ++++++++++-- .../matrix-sdk-base/src/sliding_sync/mod.rs | 5 +++- crates/matrix-sdk/src/client/mod.rs | 14 ++++++++-- .../matrix-sdk/tests/integration/room/left.rs | 26 +++++++++++++++++++ 7 files changed, 92 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index d5f0f07024c..e37cbba6f8b 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -972,6 +972,13 @@ impl Client { Ok(Arc::new(Room::new(room))) } + /// Knock on a room to join it using its ID or alias. + pub async fn knock(&self, room_id_or_alias: String) -> Result, ClientError> { + let room_id = RoomOrAliasId::parse(&room_id_or_alias)?; + let room = self.inner.knock(room_id).await?; + Ok(Arc::new(Room::new(room))) + } + pub async fn get_recently_visited_rooms(&self) -> Result, ClientError> { Ok(self .inner diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 8c21f7ca55f..d12a3f0c945 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -50,6 +50,7 @@ pub enum Membership { Invited, Joined, Left, + Knocked, } impl From for Membership { @@ -58,6 +59,7 @@ impl From for Membership { RoomState::Invited => Membership::Invited, RoomState::Joined => Membership::Joined, RoomState::Left => Membership::Left, + RoomState::Knocked => Membership::Knocked, } } } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index e86b9acccca..4ec70c10f5c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -792,6 +792,32 @@ impl BaseClient { None } + /// User has knocked on a room. + /// + /// Update the internal and cached state accordingly. Return the final Room. + pub async fn room_knocked(&self, room_id: &RoomId) -> Result { + let room = self.store.get_or_create_room( + room_id, + RoomState::Knocked, + self.room_info_notable_update_sender.clone(), + ); + + if room.state() != RoomState::Knocked { + let _sync_lock = self.sync_lock().lock().await; + + let mut room_info = room.clone_info(); + room_info.mark_as_knocked(); + room_info.mark_state_partially_synced(); + room_info.mark_members_missing(); // the own member event changed + let mut changes = StateChanges::default(); + changes.add_room(room_info.clone()); + self.store.save_changes(&changes).await?; // Update the store + room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP); + } + + Ok(room) + } + /// User has joined a room. /// /// Update the internal and cached state accordingly. Return the final Room. diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 38c83972cbf..f87ed5b3599 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -189,8 +189,10 @@ pub enum RoomState { Joined, /// The room is in a left state. Left, - /// The room is in a invited state. + /// The room is in an invited state. Invited, + /// The room is in a knocked state. + Knocked, } impl From<&MembershipState> for RoomState { @@ -201,7 +203,7 @@ impl From<&MembershipState> for RoomState { MembershipState::Ban => Self::Left, MembershipState::Invite => Self::Invited, MembershipState::Join => Self::Joined, - MembershipState::Knock => Self::Left, + MembershipState::Knock => Self::Knocked, MembershipState::Leave => Self::Left, _ => panic!("Unexpected MembershipState: {}", membership_state), } @@ -436,6 +438,9 @@ impl Room { }, } } + + // TODO: implement logic once we have the stripped events as we'd have with an Invite + RoomState::Knocked => Ok(false), } } @@ -1150,6 +1155,11 @@ impl RoomInfo { self.room_state = RoomState::Invited; } + /// Mark this Room as knocked. + pub fn mark_as_knocked(&mut self) { + self.room_state = RoomState::Knocked; + } + /// Set the membership RoomState of this Room pub fn set_state(&mut self, room_state: RoomState) { self.room_state = room_state; @@ -1685,6 +1695,8 @@ bitflags! { const INVITED = 0b00000010; /// The room is in a left state. const LEFT = 0b00000100; + /// The room is in a knocked state. + const KNOCKED = 0b00001000; } } @@ -1699,6 +1711,7 @@ impl RoomStateFilter { RoomState::Joined => Self::JOINED, RoomState::Left => Self::LEFT, RoomState::Invited => Self::INVITED, + RoomState::Knocked => Self::KNOCKED, }; self.contains(bit_state) diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 2d88ceef713..a2a5dbb00d9 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -260,7 +260,7 @@ impl BaseClient { .or_insert_with(LeftRoomUpdate::default) .account_data .append(&mut raw.to_vec()), - RoomState::Invited => {} + RoomState::Invited | RoomState::Knocked => {} } } } @@ -528,6 +528,9 @@ impl BaseClient { )), RoomState::Invited => Ok((room_info, None, None, invited_room)), + + // TODO: implement special logic for retrieving the knocked room info + RoomState::Knocked => Ok((room_info, None, None, None)), } } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 9905e158441..4b28daf873b 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -53,6 +53,7 @@ use ruma::{ get_supported_versions, }, filter::{create_filter::v3::Request as FilterUploadRequest, FilterDefinition}, + knock::knock_room, membership::{join_room_by_id, join_room_by_id_or_alias}, room::create_room, session::login::v3::DiscoveryInfo, @@ -66,8 +67,8 @@ use ruma::{ assign, push::Ruleset, time::Instant, - DeviceId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, - RoomOrAliasId, ServerName, UInt, UserId, + DeviceId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, + RoomAliasId, RoomId, RoomOrAliasId, ServerName, UInt, UserId, }; use serde::de::DeserializeOwned; use tokio::sync::{broadcast, Mutex, OnceCell, RwLock, RwLockReadGuard}; @@ -2239,6 +2240,15 @@ impl Client { self.inner.sync_beat.listen().await; } } + + /// Knock on a room given its `room_id_or_alias` to ask for permission to + /// join it. + pub async fn knock(&self, room_id_or_alias: OwnedRoomOrAliasId) -> Result { + let request = knock_room::v3::Request::new(room_id_or_alias); + let response = self.send(request, None).await?; + let base_room = self.inner.base_client.room_knocked(&response.room_id).await?; + Ok(Room::new(self.clone(), base_room)) + } } /// A weak reference to the inner client, useful when trying to get a handle diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs index 2040d5b6b33..fb359820cab 100644 --- a/crates/matrix-sdk/tests/integration/room/left.rs +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -3,6 +3,7 @@ use std::time::Duration; use matrix_sdk::config::SyncSettings; use matrix_sdk_base::RoomState; use matrix_sdk_test::{async_test, test_json, DEFAULT_TEST_ROOM_ID}; +use ruma::OwnedRoomOrAliasId; use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, @@ -56,3 +57,28 @@ async fn test_rejoin_room() { room.join().await.unwrap(); assert!(!room.is_state_fully_synced()) } + +#[async_test] +async fn test_knocking() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/unstable/xyz.amorgan.knock/knock/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "room_id": *DEFAULT_TEST_ROOM_ID })), + ) + .mount(&server) + .await; + mock_sync(&server, &*test_json::LEAVE_SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + assert_eq!(room.state(), RoomState::Left); + + let room = + client.knock(OwnedRoomOrAliasId::from((*DEFAULT_TEST_ROOM_ID).to_owned())).await.unwrap(); + assert_eq!(room.state(), RoomState::Knocked); +} From 2dcf06fad2eeafe8c4586d752d9791f5f177f062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 8 Sep 2024 11:19:29 +0200 Subject: [PATCH 237/979] sdk: Add support for authenticated media stable feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was added post-merge to the MSC for servers that support authenticated media but do not support all of Matrix 1.11 yet. Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/config/request.rs | 27 +++- crates/matrix-sdk/src/http_client/mod.rs | 6 + crates/matrix-sdk/src/media.rs | 29 +++- crates/matrix-sdk/tests/integration/media.rs | 137 ++++++++++++++++++- 4 files changed, 190 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/config/request.rs b/crates/matrix-sdk/src/config/request.rs index 88b66e4ff05..6565e4d479c 100644 --- a/crates/matrix-sdk/src/config/request.rs +++ b/crates/matrix-sdk/src/config/request.rs @@ -19,6 +19,7 @@ use std::{ }; use matrix_sdk_common::debug::DebugStructExt; +use ruma::api::MatrixVersion; use crate::http_client::DEFAULT_REQUEST_TIMEOUT; @@ -47,19 +48,27 @@ pub struct RequestConfig { pub(crate) retry_timeout: Option, pub(crate) max_concurrent_requests: Option, pub(crate) force_auth: bool, + pub(crate) force_matrix_version: Option, } #[cfg(not(tarpaulin_include))] impl Debug for RequestConfig { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { timeout, retry_limit, retry_timeout, force_auth, max_concurrent_requests } = - self; + let Self { + timeout, + retry_limit, + retry_timeout, + force_auth, + max_concurrent_requests, + force_matrix_version, + } = self; let mut res = fmt.debug_struct("RequestConfig"); res.field("timeout", timeout) .maybe_field("retry_limit", retry_limit) .maybe_field("retry_timeout", retry_timeout) - .maybe_field("max_concurrent_requests", max_concurrent_requests); + .maybe_field("max_concurrent_requests", max_concurrent_requests) + .maybe_field("force_matrix_version", force_matrix_version); if *force_auth { res.field("force_auth", &true); @@ -77,6 +86,7 @@ impl Default for RequestConfig { retry_timeout: Default::default(), max_concurrent_requests: Default::default(), force_auth: false, + force_matrix_version: Default::default(), } } } @@ -142,6 +152,17 @@ impl RequestConfig { self.force_auth = true; self } + + /// Force the Matrix version used to select which version of the endpoint to + /// use. + /// + /// Can be used to force the use of a stable endpoint when the versions + /// advertised by the homeserver do not support it. + #[must_use] + pub(crate) fn force_matrix_version(mut self, version: MatrixVersion) -> Self { + self.force_matrix_version = Some(version); + self + } } #[cfg(test)] diff --git a/crates/matrix-sdk/src/http_client/mod.rs b/crates/matrix-sdk/src/http_client/mod.rs index 5ac4b36aa8c..6cc674bf64e 100644 --- a/crates/matrix-sdk/src/http_client/mod.rs +++ b/crates/matrix-sdk/src/http_client/mod.rs @@ -108,6 +108,12 @@ impl HttpClient { { trace!(request_type = type_name::(), "Serializing request"); + let server_versions = if config.force_matrix_version.is_some() { + config.force_matrix_version.as_slice() + } else { + server_versions + }; + let send_access_token = match access_token { Some(access_token) => { if config.force_auth { diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index d229a7526e4..e8f62372ed3 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -274,15 +274,34 @@ impl Media { } }; - // Use the authenticated endpoints when the server supports Matrix 1.11. - let use_auth = self.client.server_versions().await?.contains(&MatrixVersion::V1_11); + // Use the authenticated endpoints when the server supports Matrix 1.11 or the + // authenticated media stable feature. + const AUTHENTICATED_MEDIA_STABLE_FEATURE: &str = "org.matrix.msc3916.stable"; + + let (use_auth, request_config) = + if self.client.server_versions().await?.contains(&MatrixVersion::V1_11) { + (true, None) + } else if self + .client + .unstable_features() + .await? + .get(AUTHENTICATED_MEDIA_STABLE_FEATURE) + .is_some_and(|is_supported| *is_supported) + { + // We need to force the use of the stable endpoint with the Matrix version + // because Ruma does not handle stable features. + let request_config = self.client.request_config(); + (true, Some(request_config.force_matrix_version(MatrixVersion::V1_11))) + } else { + (false, None) + }; let content: Vec = match &request.source { MediaSource::Encrypted(file) => { let content = if use_auth { let request = authenticated_media::get_content::v1::Request::from_uri(&file.url)?; - self.client.send(request, None).await?.file + self.client.send(request, request_config).await?.file } else { #[allow(deprecated)] let request = media::get_content::v3::Request::from_url(&file.url)?; @@ -321,7 +340,7 @@ impl Media { request.method = Some(settings.size.method.clone()); request.animated = Some(settings.animated); - self.client.send(request, None).await?.file + self.client.send(request, request_config).await?.file } else { #[allow(deprecated)] let request = { @@ -339,7 +358,7 @@ impl Media { } } else if use_auth { let request = authenticated_media::get_content::v1::Request::from_uri(uri)?; - self.client.send(request, None).await?.file + self.client.send(request, request_config).await?.file } else { #[allow(deprecated)] let request = media::get_content::v3::Request::from_url(uri)?; diff --git a/crates/matrix-sdk/tests/integration/media.rs b/crates/matrix-sdk/tests/integration/media.rs index ecb35d4e330..ae35fe5a41b 100644 --- a/crates/matrix-sdk/tests/integration/media.rs +++ b/crates/matrix-sdk/tests/integration/media.rs @@ -22,6 +22,17 @@ use wiremock::{ async fn test_get_media_content_no_auth() { let (client, server) = logged_in_client_with_server().await; + // The client will call this endpoint to get the list of unstable features. + Mock::given(method("GET")) + .and(path("/_matrix/client/versions")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "versions": ["r0.6.1"], + }))) + .named("versions") + .expect(1) + .mount(&server) + .await; + let media = client.media(); let request = MediaRequest { @@ -98,6 +109,17 @@ async fn test_get_media_content_no_auth() { async fn test_get_media_file_no_auth() { let (client, server) = logged_in_client_with_server().await; + // The client will call this endpoint to get the list of unstable features. + Mock::given(method("GET")) + .and(path("/_matrix/client/versions")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "versions": ["r0.6.1"], + }))) + .named("versions") + .expect(1) + .mount(&server) + .await; + let event_content = ImageMessageEventContent::plain( "filename.jpg".into(), mxc_uri!("mxc://example.org/image").to_owned(), @@ -168,7 +190,7 @@ async fn test_get_media_file_no_auth() { } #[async_test] -async fn test_get_media_file_with_auth() { +async fn test_get_media_file_with_auth_matrix_1_11() { // The server must advertise support for v1.11 for authenticated media support, // so we make the request instead of assuming. let server = wiremock::MockServer::start().await; @@ -276,3 +298,116 @@ async fn test_get_media_file_with_auth() { }; client.media().get_thumbnail(&event_content, settings, true).await.unwrap(); } + +#[async_test] +async fn test_get_media_file_with_auth_matrix_stable_feature() { + // The server must advertise support for the stable feature for authenticated + // media support, so we make the request instead of assuming. + let server = wiremock::MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/_matrix/client/versions")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "versions": ["v1.7", "v1.8", "v1.9", "v1.10"], + "unstable_features": { + "org.matrix.msc3916.stable": true, + }, + }))) + .named("versions") + .expect(1) + .mount(&server) + .await; + + // Build client. + let client = Client::builder() + .homeserver_url(server.uri()) + .request_config(RequestConfig::new().disable_retry()) + .build() + .await + .unwrap(); + + // Restore session. + client + .matrix_auth() + .restore_session(MatrixSession { + meta: SessionMeta { + user_id: user_id!("@example:localhost").to_owned(), + device_id: device_id!("DEVICEID").to_owned(), + }, + tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None }, + }) + .await + .unwrap(); + + // Build event content. + let event_content = ImageMessageEventContent::plain( + "filename.jpg".into(), + mxc_uri!("mxc://example.org/image").to_owned(), + ) + .info(Box::new(assign!(ImageInfo::new(), { + height: Some(uint!(398)), + width: Some(uint!(394)), + mimetype: Some("image/jpeg".into()), + size: Some(uint!(31037)), + }))); + + // Get the full file. + Mock::given(method("GET")) + .and(path("/_matrix/client/v1/media/download/example.org/image")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_raw("binaryjpegdata", "image/jpeg")) + .named("get_file") + .expect(1) + .mount(&server) + .await; + + client.media().get_file(&event_content, false).await.unwrap(); + + // Get a thumbnail, not animated. + Mock::given(method("GET")) + .and(path("/_matrix/client/v1/media/thumbnail/example.org/image")) + .and(query_param("method", "scale")) + .and(query_param("width", "100")) + .and(query_param("height", "100")) + .and(query_param("animated", "false")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200).set_body_raw("smallerbinaryjpegdata", "image/jpeg"), + ) + .expect(1) + .named("get_thumbnail_no_animated") + .mount(&server) + .await; + + client + .media() + .get_thumbnail( + &event_content, + MediaThumbnailSettings::new(Method::Scale, uint!(100), uint!(100)), + true, + ) + .await + .unwrap(); + + // Get a thumbnail, animated. + Mock::given(method("GET")) + .and(path("/_matrix/client/v1/media/thumbnail/example.org/image")) + .and(query_param("method", "crop")) + .and(query_param("width", "100")) + .and(query_param("height", "100")) + .and(query_param("animated", "true")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200).set_body_raw("smallerbinaryjpegdata", "image/jpeg"), + ) + .expect(1) + .named("get_thumbnail_animated_true") + .mount(&server) + .await; + + let settings = MediaThumbnailSettings { + size: MediaThumbnailSize { method: Method::Crop, width: uint!(100), height: uint!(100) }, + animated: true, + }; + client.media().get_thumbnail(&event_content, settings, true).await.unwrap(); +} From 867d9c71fd9d1e4526fc0dd3ce1091a9287b05ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 8 Oct 2024 11:18:17 +0200 Subject: [PATCH 238/979] sdk-base: add `prev_room_state` to `RoomInfo` This is useful for the knocking feature since we'll be able to differentiate between rooms that you were just invited from rooms that you knocked and then were granted access, or rooms that you left and rooms where your knocking attempt was rejected. The `mark_room_as_*` functions have been updated so they reuse the same `set_state` function underneath, which only updates the previous state if the new one doesn't match. --- crates/matrix-sdk-base/src/rooms/normal.rs | 68 ++++++++++++++++--- .../src/store/migration_helpers.rs | 1 + 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index f87ed5b3599..b5918c15e31 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -182,7 +182,7 @@ impl RoomSummary { } /// Enum keeping track in which state the room is, e.g. if our own user is -/// joined, invited, or has left the room. +/// joined, RoomState::Invited, or has left the room. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum RoomState { /// The room is in a joined state. @@ -275,6 +275,11 @@ impl Room { self.inner.read().room_state } + /// Get the previous state of the room, if it had any. + pub fn prev_state(&self) -> Option { + self.inner.read().prev_room_state + } + /// Whether this room's [`RoomType`] is `m.space`. pub fn is_space(&self) -> bool { self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space) @@ -864,8 +869,8 @@ impl Room { /// Get the `RoomMember` with the given `user_id`. /// /// Returns `None` if the member was never part of this room, otherwise - /// return a `RoomMember` that can be in a joined, invited, left, banned - /// state. + /// return a `RoomMember` that can be in a joined, RoomState::Invited, left, + /// banned state. /// /// Async because it can read from storage. pub async fn get_member(&self, user_id: &UserId) -> StoreResult> { @@ -1037,6 +1042,9 @@ pub struct RoomInfo { /// The state of the room. pub(crate) room_state: RoomState, + /// The previous state of the room, if any. + pub(crate) prev_room_state: Option, + /// The unread notifications counts, as returned by the server. /// /// These might be incorrect for encrypted rooms, since the server doesn't @@ -1122,6 +1130,7 @@ impl RoomInfo { version: 1, room_id: room_id.into(), room_state, + prev_room_state: None, notification_counts: Default::default(), summary: Default::default(), members_synced: false, @@ -1142,27 +1151,30 @@ impl RoomInfo { /// Mark this Room as joined. pub fn mark_as_joined(&mut self) { - self.room_state = RoomState::Joined; + self.set_state(RoomState::Joined); } /// Mark this Room as left. pub fn mark_as_left(&mut self) { - self.room_state = RoomState::Left; + self.set_state(RoomState::Left); } /// Mark this Room as invited. pub fn mark_as_invited(&mut self) { - self.room_state = RoomState::Invited; + self.set_state(RoomState::Invited); } /// Mark this Room as knocked. pub fn mark_as_knocked(&mut self) { - self.room_state = RoomState::Knocked; + self.set_state(RoomState::Knocked); } /// Set the membership RoomState of this Room pub fn set_state(&mut self, room_state: RoomState) { - self.room_state = room_state; + if room_state != self.room_state { + self.prev_room_state = Some(self.room_state); + self.room_state = room_state; + } } /// Mark this Room as having all the members synced. @@ -1822,7 +1834,8 @@ mod tests { use crate::{ rooms::RoomNotableTags, store::{IntoStateStore, MemoryStore, StateChanges, StateStore}, - BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, SessionMeta, + BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, + RoomInfoNotableUpdateReasons, SessionMeta, }; #[test] @@ -1840,6 +1853,7 @@ mod tests { version: 1, room_id: room_id!("!gda78o:server.tld").into(), room_state: RoomState::Invited, + prev_room_state: None, notification_counts: UnreadNotificationsCount { highlight_count: 1, notification_count: 2, @@ -1874,6 +1888,7 @@ mod tests { "version": 1, "room_id": "!gda78o:server.tld", "room_state": "Invited", + "prev_room_state": null, "notification_counts": { "highlight_count": 1, "notification_count": 2, @@ -1945,6 +1960,7 @@ mod tests { let info_json = json!({ "room_id": "!gda78o:server.tld", "room_state": "Invited", + "prev_room_state": null, "notification_counts": { "highlight_count": 1, "notification_count": 2, @@ -2021,7 +2037,8 @@ mod tests { let info_json = json!({ "room_id": "!gda78o:server.tld", - "room_state": "Invited", + "room_state": "Joined", + "prev_room_state": "Invited", "notification_counts": { "highlight_count": 1, "notification_count": 2, @@ -2061,7 +2078,8 @@ mod tests { 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.room_state, RoomState::Joined); + assert_eq!(info.prev_room_state, Some(RoomState::Invited)); assert_eq!(info.notification_counts.highlight_count, 1); assert_eq!(info.notification_counts.notification_count, 2); assert_eq!( @@ -3179,4 +3197,32 @@ mod tests { let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined); assert_eq!(new_room_info.version, 1); } + + #[async_test] + async fn test_prev_room_state_is_updated() { + let (_store, room) = make_room_test_helper(RoomState::Invited); + assert_eq!(room.prev_state(), None); + assert_eq!(room.state(), RoomState::Invited); + + // Invited -> Joined + let mut room_info = room.clone_info(); + room_info.mark_as_joined(); + room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP); + assert_eq!(room.prev_state(), Some(RoomState::Invited)); + assert_eq!(room.state(), RoomState::Joined); + + // No change when the same state is used + let mut room_info = room.clone_info(); + room_info.mark_as_joined(); + room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP); + assert_eq!(room.prev_state(), Some(RoomState::Invited)); + assert_eq!(room.state(), RoomState::Joined); + + // Joined -> Left + let mut room_info = room.clone_info(); + room_info.mark_as_left(); + room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP); + assert_eq!(room.prev_state(), Some(RoomState::Joined)); + assert_eq!(room.state(), RoomState::Left); + } } diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index 454f3afc977..76acb86cf48 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -114,6 +114,7 @@ impl RoomInfoV1 { version: 0, room_id, room_state: room_type, + prev_room_state: None, notification_counts, summary, members_synced, From 4bcb9b7d9ff6f0fa36f730d689b241f93bd573c9 Mon Sep 17 00:00:00 2001 From: boxdot Date: Tue, 8 Oct 2024 15:12:40 +0200 Subject: [PATCH 239/979] fix: Fix a deadlock between `bootstrap_cross_signing` and `sync` (#4060) `bootstrap_cross_signing` holds a lock on the private identity. In case a new identity is created, it will try to acquire a lock on `account`. The latter is locked by `sync`, which tries to acquire a lock on the private identity. Note that the `bootstrap_cross_signing` call is executed in a separate task e.g. in `restore_session`. In particular, this task and `sync` both race to acquire locks described above. Signed-off-by: boxdot --- crates/matrix-sdk-crypto/src/machine/mod.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 949ca1e56f5..80b2c1d44d5 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -642,28 +642,32 @@ impl OlmMachine { &self, reset: bool, ) -> StoreResult { - let mut identity = self.inner.user_identity.lock().await; + // Don't hold the lock, otherwise we might deadlock in + // `bootstrap_cross_signing()` on `account` if a sync task is already + // running (which locks `account`), or we will deadlock + // in `upload_device_keys()` which locks private identity again. + let identity = self.inner.user_identity.lock().await.clone(); let (upload_signing_keys_req, upload_signatures_req) = if reset || identity.is_empty().await { info!("Creating new cross signing identity"); - let (new_identity, upload_signing_keys_req, upload_signatures_req) = { + let (identity, upload_signing_keys_req, upload_signatures_req) = { let cache = self.inner.store.cache().await?; let account = cache.account().await?; account.bootstrap_cross_signing().await }; - *identity = new_identity; - let public = identity.to_public_identity().await.expect( "Couldn't create a public version of the identity from a new private identity", ); + *self.inner.user_identity.lock().await = identity.clone(); + self.store() .save_changes(Changes { identities: IdentityChanges { new: vec![public.into()], ..Default::default() }, - private_identity: Some(identity.clone()), + private_identity: Some(identity), ..Default::default() }) .await?; @@ -682,11 +686,6 @@ impl OlmMachine { (upload_signing_keys_req, upload_signatures_req) }; - // `upload_device_keys()` will attempt to sign the device keys using this - // `identity`, it will attempt to acquire the lock, so we need to drop - // here to avoid a deadlock. - drop(identity); - // If there are any *device* keys to upload (i.e. the account isn't shared), // upload them before we upload the signatures, since the signatures may // reference keys to be uploaded. From 736aa0351cc872410c457afea1bae78a35fd20b9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 8 Oct 2024 15:36:22 +0200 Subject: [PATCH 240/979] ffi: add our own macro for processing exports Including one that will always warn if used with async functions, and the other one always setting the tokio runtime if used for async stuff. --- Cargo.lock | 12 +++ Cargo.toml | 1 + bindings/matrix-sdk-crypto-ffi/Cargo.toml | 1 + .../src/backup_recovery_key.rs | 2 +- .../src/dehydrated_devices.rs | 6 +- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 18 ++--- bindings/matrix-sdk-crypto-ffi/src/logger.rs | 4 +- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 2 +- .../matrix-sdk-crypto-ffi/src/verification.rs | 14 ++-- bindings/matrix-sdk-ffi/Cargo.toml | 1 + bindings/matrix-sdk-ffi/src/authentication.rs | 4 +- bindings/matrix-sdk-ffi/src/client.rs | 18 ++--- bindings/matrix-sdk-ffi/src/client_builder.rs | 6 +- bindings/matrix-sdk-ffi/src/element.rs | 2 +- bindings/matrix-sdk-ffi/src/encryption.rs | 16 ++-- bindings/matrix-sdk-ffi/src/event.rs | 2 +- bindings/matrix-sdk-ffi/src/lib.rs | 2 +- bindings/matrix-sdk-ffi/src/notification.rs | 2 +- .../src/notification_settings.rs | 4 +- bindings/matrix-sdk-ffi/src/platform.rs | 2 +- bindings/matrix-sdk-ffi/src/room.rs | 12 +-- .../src/room_directory_search.rs | 4 +- bindings/matrix-sdk-ffi/src/room_list.rs | 20 ++--- bindings/matrix-sdk-ffi/src/room_member.rs | 6 +- bindings/matrix-sdk-ffi/src/ruma.rs | 16 ++-- .../src/session_verification.rs | 6 +- bindings/matrix-sdk-ffi/src/sync_service.rs | 8 +- bindings/matrix-sdk-ffi/src/task_handle.rs | 2 +- .../matrix-sdk-ffi/src/timeline/content.rs | 2 +- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 18 ++--- .../src/timeline_event_filter.rs | 2 +- bindings/matrix-sdk-ffi/src/tracing.rs | 4 +- bindings/matrix-sdk-ffi/src/widget.rs | 14 ++-- crates/matrix-sdk/Cargo.toml | 3 +- .../matrix-sdk/src/oidc/auth_code_builder.rs | 2 +- testing/matrix-sdk-ffi-macros/Cargo.toml | 24 ++++++ testing/matrix-sdk-ffi-macros/README.md | 14 ++++ testing/matrix-sdk-ffi-macros/src/lib.rs | 78 +++++++++++++++++++ 38 files changed, 243 insertions(+), 111 deletions(-) create mode 100644 testing/matrix-sdk-ffi-macros/Cargo.toml create mode 100644 testing/matrix-sdk-ffi-macros/README.md create mode 100644 testing/matrix-sdk-ffi-macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0f672765b80..88a5555799d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3136,6 +3136,7 @@ dependencies = [ "mas-oidc-client", "matrix-sdk-base", "matrix-sdk-common", + "matrix-sdk-ffi-macros", "matrix-sdk-indexeddb", "matrix-sdk-sqlite", "matrix-sdk-test", @@ -3293,6 +3294,7 @@ dependencies = [ "js_int", "matrix-sdk-common", "matrix-sdk-crypto", + "matrix-sdk-ffi-macros", "matrix-sdk-sqlite", "pbkdf2", "rand", @@ -3323,6 +3325,7 @@ dependencies = [ "language-tags", "log-panics", "matrix-sdk", + "matrix-sdk-ffi-macros", "matrix-sdk-ui", "mime", "once_cell", @@ -3344,6 +3347,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "matrix-sdk-ffi-macros" +version = "0.7.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "matrix-sdk-indexeddb" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 456d92b226a..448a450312f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" } matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" } matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" } +matrix-sdk-ffi-macros = { path = "testing/matrix-sdk-ffi-macros", version = "0.7.0" } matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false } matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" } matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false } diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index 50dcaf137fe..3432c965ef8 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -26,6 +26,7 @@ futures-util = { workspace = true } hmac = "0.12.1" http = { workspace = true } matrix-sdk-common = { workspace = true, features = ["uniffi"] } +matrix-sdk-ffi-macros = { workspace = true } pbkdf2 = "0.12.2" rand = { workspace = true } ruma = { workspace = true } diff --git a/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs b/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs index b9b97062e6a..05c17b8aec5 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/backup_recovery_key.rs @@ -69,7 +69,7 @@ impl BackupRecoveryKey { const PBKDF_ROUNDS: i32 = 500_000; } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl BackupRecoveryKey { /// Create a new random [`BackupRecoveryKey`]. #[allow(clippy::new_without_default)] diff --git a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs index c57d5ae4bbf..585eb7a9be1 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs @@ -53,7 +53,7 @@ impl Drop for DehydratedDevices { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl DehydratedDevices { pub fn create(&self) -> Result, DehydrationError> { let inner = self.runtime.block_on(self.inner.create())?; @@ -107,7 +107,7 @@ impl Drop for RehydratedDevice { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl RehydratedDevice { pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> { let events: Vec> = serde_json::from_str(&events)?; @@ -133,7 +133,7 @@ impl Drop for DehydratedDevice { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl DehydratedDevice { pub fn keys_for_upload( &self, diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 769c7876fef..125abd99ed3 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -196,7 +196,7 @@ impl From for MigrationError { /// /// * `progress_listener` - A callback that can be used to introspect the /// progress of the migration. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn migrate( data: MigrationData, path: String, @@ -359,7 +359,7 @@ async fn save_changes( /// /// * `progress_listener` - A callback that can be used to introspect the /// progress of the migration. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn migrate_sessions( data: SessionMigrationData, path: String, @@ -532,7 +532,7 @@ fn collect_sessions( /// * `passphrase` - The passphrase that should be used to encrypt the data at /// rest in the Sqlite store. **Warning**, if no passphrase is given, the /// store and all its data will remain unencrypted. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn migrate_room_settings( room_settings: HashMap, path: String, @@ -558,7 +558,7 @@ pub fn migrate_room_settings( } /// Callback that will be passed over the FFI to report progress -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait ProgressListener { /// The callback that should be called on the Rust side /// @@ -794,7 +794,7 @@ pub struct BackupKeys { backup_version: String, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl BackupKeys { /// Get the recovery key that we're holding on to. pub fn recovery_key(&self) -> Arc { @@ -891,7 +891,7 @@ fn parse_user_id(user_id: &str) -> Result { ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e)) } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] fn version_info() -> VersionInfo { VersionInfo { version: matrix_sdk_crypto::VERSION.to_owned(), @@ -915,12 +915,12 @@ pub struct VersionInfo { pub git_description: String, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] fn version() -> String { matrix_sdk_crypto::VERSION.to_owned() } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] fn vodozemac_version() -> String { vodozemac::VERSION.to_owned() } @@ -935,7 +935,7 @@ pub struct PkEncryption { inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl PkEncryption { /// Create a new [`PkEncryption`] object from a `Curve25519PublicKey` /// encoded as Base64. diff --git a/bindings/matrix-sdk-crypto-ffi/src/logger.rs b/bindings/matrix-sdk-crypto-ffi/src/logger.rs index 328999ea15a..6b53a352a77 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/logger.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/logger.rs @@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter}; /// Trait that can be used to forward Rust logs over FFI to a language specific /// logger. -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait Logger: Send { /// Called every time the Rust side wants to post a log line. fn log(&self, log_line: String); @@ -42,7 +42,7 @@ pub struct LoggerWrapper { } /// Set the logger that should be used to forward Rust logs over FFI. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn set_logger(logger: Box) { let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) }; diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 8bb1fedd718..da12075bfe4 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -178,7 +178,7 @@ impl From for SignatureVerification { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl OlmMachine { /// Create a new `OlmMachine` /// diff --git a/bindings/matrix-sdk-crypto-ffi/src/verification.rs b/bindings/matrix-sdk-crypto-ffi/src/verification.rs index 179200503ba..e8c31c4a3a8 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/verification.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/verification.rs @@ -15,7 +15,7 @@ use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadReques /// Listener that will be passed over the FFI to report changes to a SAS /// verification. -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait SasListener: Send { /// The callback that should be called on the Rust side /// @@ -82,7 +82,7 @@ pub struct Verification { pub(crate) runtime: Handle, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl Verification { /// Try to represent the `Verification` as an `Sas` verification object, /// returns `None` if the verification is not a `Sas` verification. @@ -112,7 +112,7 @@ pub struct Sas { pub(crate) runtime: Handle, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl Sas { /// Get the user id of the other side. pub fn other_user_id(&self) -> String { @@ -276,7 +276,7 @@ impl Sas { /// Listener that will be passed over the FFI to report changes to a QrCode /// verification. -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait QrCodeListener: Send { /// The callback that should be called on the Rust side /// @@ -328,7 +328,7 @@ pub struct QrCode { pub(crate) runtime: Handle, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl QrCode { /// Get the user id of the other side. pub fn other_user_id(&self) -> String { @@ -522,7 +522,7 @@ pub struct ConfirmVerificationResult { /// Listener that will be passed over the FFI to report changes to a /// verification request. -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait VerificationRequestListener: Send { /// The callback that should be called on the Rust side /// @@ -562,7 +562,7 @@ pub struct VerificationRequest { pub(crate) runtime: Handle, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl VerificationRequest { /// The id of the other user that is participating in this verification /// request. diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index abab2bff692..02400423b17 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -28,6 +28,7 @@ eyeball-im = { workspace = true } extension-trait = "1.0.1" futures-util = { workspace = true } log-panics = { version = "2", features = ["with-backtrace"] } +matrix-sdk-ffi-macros = { workspace = true } matrix-sdk-ui = { workspace = true, features = ["uniffi"] } mime = "0.3.16" once_cell = { workspace = true } diff --git a/bindings/matrix-sdk-ffi/src/authentication.rs b/bindings/matrix-sdk-ffi/src/authentication.rs index 37aaff4e8e2..3a22244c519 100644 --- a/bindings/matrix-sdk-ffi/src/authentication.rs +++ b/bindings/matrix-sdk-ffi/src/authentication.rs @@ -29,7 +29,7 @@ pub struct HomeserverLoginDetails { pub(crate) supports_password_login: bool, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl HomeserverLoginDetails { /// The URL of the currently configured homeserver. pub fn url(&self) -> String { @@ -62,7 +62,7 @@ pub struct SsoHandler { pub(crate) url: String, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl SsoHandler { /// Returns the URL for starting SSO authentication. The URL should be /// opened in a web view. Once the web view succeeds, call `finish` with diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index e37cbba6f8b..314b19eab1e 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -140,25 +140,25 @@ impl From for RumaPushFormat { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait ClientDelegate: Sync + Send { fn did_receive_auth_error(&self, is_soft_logout: bool); fn did_refresh_tokens(&self); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait ClientSessionDelegate: Sync + Send { fn retrieve_session_from_keychain(&self, user_id: String) -> Result; fn save_session_in_keychain(&self, session: Session); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait ProgressWatcher: Send + Sync { fn transmission_progress(&self, progress: TransmissionProgress); } /// A listener to the global (client-wide) error reporter of the send queue. -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait SendQueueRoomErrorListener: Sync + Send { /// Called every time the send queue has ran into an error for a given room, /// which will disable the send queue for that particular room. @@ -260,7 +260,7 @@ impl Client { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl Client { /// Information about login options for the client's homeserver. pub async fn homeserver_login_details(&self) -> Arc { @@ -526,7 +526,7 @@ impl Client { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl Client { /// The sliding sync version. pub fn sliding_sync_version(&self) -> SlidingSyncVersion { @@ -1092,7 +1092,7 @@ impl Client { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait IgnoredUsersListener: Sync + Send { fn call(&self, ignored_user_ids: Vec); } @@ -1649,7 +1649,7 @@ impl From for AccountManagementActionFull { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] fn gen_transaction_id() -> String { TransactionId::new().to_string() } @@ -1667,7 +1667,7 @@ impl MediaFileHandle { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl MediaFileHandle { /// Get the media file's path. pub fn path(&self) -> Result { diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index ec0522fafd2..9b817dd4f66 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -47,7 +47,7 @@ pub struct QrCodeData { inner: qrcode::QrCodeData, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl QrCodeData { /// Attempt to decode a slice of bytes into a [`QrCodeData`] object. /// @@ -159,7 +159,7 @@ pub enum QrLoginProgress { Done, } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait QrLoginProgressListener: Sync + Send { fn on_update(&self, state: QrLoginProgress); } @@ -270,7 +270,7 @@ pub struct ClientBuilder { request_config: Option, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl ClientBuilder { #[uniffi::constructor] pub fn new() -> Arc { diff --git a/bindings/matrix-sdk-ffi/src/element.rs b/bindings/matrix-sdk-ffi/src/element.rs index 6ff2dff5b53..87722b4d278 100644 --- a/bindings/matrix-sdk-ffi/src/element.rs +++ b/bindings/matrix-sdk-ffi/src/element.rs @@ -16,7 +16,7 @@ pub struct ElementWellKnown { } /// Helper function to parse a string into a ElementWellKnown struct -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn make_element_well_known(string: String) -> Result { serde_json::from_str(&string).map_err(ClientError::new) } diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 7f6e35b4aac..cd7c6e94fc1 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -23,22 +23,22 @@ pub struct Encryption { pub(crate) _client: Arc, } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait BackupStateListener: Sync + Send { fn on_update(&self, status: BackupState); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait BackupSteadyStateListener: Sync + Send { fn on_update(&self, status: BackupUploadState); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait RecoveryStateListener: Sync + Send { fn on_update(&self, status: RecoveryState); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait VerificationStateListener: Sync + Send { fn on_update(&self, status: VerificationState); } @@ -162,7 +162,7 @@ impl From for RecoveryState { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait EnableRecoveryProgressListener: Sync + Send { fn on_update(&self, status: EnableRecoveryProgress); } @@ -212,7 +212,7 @@ impl From for VerificationState { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl Encryption { /// Get the public ed25519 key of our own device. This is usually what is /// called the fingerprint of the device. @@ -432,7 +432,7 @@ pub struct UserIdentity { inner: matrix_sdk::encryption::identities::UserIdentity, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl UserIdentity { /// Remember this identity, ensuring it does not result in a pin violation. /// @@ -468,7 +468,7 @@ pub struct IdentityResetHandle { pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl IdentityResetHandle { /// Get the underlying [`CrossSigningResetAuthType`] this identity reset /// process is using. diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index 3cd4304a281..b5b98d792c8 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -20,7 +20,7 @@ use crate::{ #[derive(uniffi::Object)] pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent); -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl TimelineEvent { pub fn event_id(&self) -> String { self.0.event_id().to_string() diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 801887b6fee..0e404a848ff 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -44,7 +44,7 @@ use self::{ uniffi::include_scaffolding!("api"); -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] fn sdk_git_sha() -> String { env!("VERGEN_GIT_SHA").to_owned() } diff --git a/bindings/matrix-sdk-ffi/src/notification.rs b/bindings/matrix-sdk-ffi/src/notification.rs index eda3f6232a1..35d047b94a2 100644 --- a/bindings/matrix-sdk-ffi/src/notification.rs +++ b/bindings/matrix-sdk-ffi/src/notification.rs @@ -88,7 +88,7 @@ pub struct NotificationClient { pub(crate) _client: Arc, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl NotificationClient { /// See also documentation of /// `MatrixNotificationClient::get_notification`. diff --git a/bindings/matrix-sdk-ffi/src/notification_settings.rs b/bindings/matrix-sdk-ffi/src/notification_settings.rs index f758a2fda97..1a01011a889 100644 --- a/bindings/matrix-sdk-ffi/src/notification_settings.rs +++ b/bindings/matrix-sdk-ffi/src/notification_settings.rs @@ -49,7 +49,7 @@ impl From for SdkRoomNotificationMode { } /// Delegate to notify of changes in push rules -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait NotificationSettingsDelegate: Sync + Send { fn settings_did_change(&self); } @@ -98,7 +98,7 @@ impl Drop for NotificationSettings { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl NotificationSettings { pub fn set_delegate(&self, delegate: Option>) { if let Some(delegate) = delegate { diff --git a/bindings/matrix-sdk-ffi/src/platform.rs b/bindings/matrix-sdk-ffi/src/platform.rs index 7e61b93a2fb..a811474f9f3 100644 --- a/bindings/matrix-sdk-ffi/src/platform.rs +++ b/bindings/matrix-sdk-ffi/src/platform.rs @@ -242,7 +242,7 @@ pub struct TracingConfiguration { write_to_files: Option, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn setup_tracing(config: TracingConfiguration) { log_panics(); diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index d12a3f0c945..d5d36e57a19 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -82,7 +82,7 @@ impl Room { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl Room { pub fn id(&self) -> String { self.inner.room_id().to_string() @@ -861,7 +861,7 @@ impl Room { } /// Generates a `matrix.to` permalink to the given room alias. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn matrix_to_room_alias_permalink( room_alias: String, ) -> std::result::Result { @@ -917,17 +917,17 @@ impl From for RoomPowerLevels { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait RoomInfoListener: Sync + Send { fn call(&self, room_info: RoomInfo); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait TypingNotificationsListener: Sync + Send { fn call(&self, typing_user_ids: Vec); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait IdentityStatusChangeListener: Sync + Send { fn call(&self, identity_status_change: Vec); } @@ -943,7 +943,7 @@ impl RoomMembersIterator { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl RoomMembersIterator { fn len(&self) -> u32 { self.chunk_iterator.len() diff --git a/bindings/matrix-sdk-ffi/src/room_directory_search.rs b/bindings/matrix-sdk-ffi/src/room_directory_search.rs index b3af0058b94..0666b50e3b3 100644 --- a/bindings/matrix-sdk-ffi/src/room_directory_search.rs +++ b/bindings/matrix-sdk-ffi/src/room_directory_search.rs @@ -79,7 +79,7 @@ impl RoomDirectorySearch { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl RoomDirectorySearch { pub async fn next_page(&self) -> Result<(), ClientError> { let mut inner = self.inner.write().await; @@ -169,7 +169,7 @@ impl From> } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug { fn on_update(&self, room_entries_update: Vec); } diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index b9e4dca9623..9f70e9e71f3 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -85,7 +85,7 @@ pub struct RoomListService { pub(crate) utd_hook: Option>, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl RoomListService { fn state(&self, listener: Box) -> Arc { let state_stream = self.inner.state(); @@ -162,7 +162,7 @@ pub struct RoomList { inner: Arc, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl RoomList { fn loading_state( &self, @@ -292,7 +292,7 @@ pub struct RoomListEntriesWithDynamicAdaptersResult { entries_stream: Arc, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl RoomListEntriesWithDynamicAdaptersResult { fn controller(&self) -> Arc { self.controller.clone() @@ -370,17 +370,17 @@ impl From for RoomListLo } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait RoomListServiceStateListener: Send + Sync + Debug { fn on_update(&self, state: RoomListServiceState); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait RoomListLoadingStateListener: Send + Sync + Debug { fn on_update(&self, state: RoomListLoadingState); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug { fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator); } @@ -443,7 +443,7 @@ impl RoomListEntriesUpdate { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait RoomListEntriesListener: Send + Sync + Debug { fn on_update(&self, room_entries_update: Vec); } @@ -461,7 +461,7 @@ impl RoomListDynamicEntriesController { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl RoomListDynamicEntriesController { fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool { self.inner.set_filter(kind.into()) @@ -549,7 +549,7 @@ impl RoomListItem { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl RoomListItem { fn id(&self) -> String { self.inner.id().to_string() @@ -711,7 +711,7 @@ pub struct UnreadNotificationsCount { notification_count: u32, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl UnreadNotificationsCount { fn highlight_count(&self) -> u32 { self.highlight_count diff --git a/bindings/matrix-sdk-ffi/src/room_member.rs b/bindings/matrix-sdk-ffi/src/room_member.rs index 7fe579b1154..96aed25a00f 100644 --- a/bindings/matrix-sdk-ffi/src/room_member.rs +++ b/bindings/matrix-sdk-ffi/src/room_member.rs @@ -42,20 +42,20 @@ impl From for Membershi } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole { // It's not possible to expose the constructor on the Enum through Uniffi ☹️ RoomMemberRole::suggested_role_for_power_level(power_level) } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 { // It's not possible to expose methods on an Enum through Uniffi ☹️ role.suggested_power_level() } /// Generates a `matrix.to` permalink to the given userID. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn matrix_to_user_permalink(user_id: String) -> Result { let user_id = UserId::parse(user_id)?; Ok(user_id.matrix_to_uri().to_string()) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 20c00e62025..fa4bdbb95b5 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -90,7 +90,7 @@ impl From for ruma::api::client::uiaa::AuthData { /// Parse a matrix entity from a given URI, be it either /// a `matrix.to` link or a `matrix:` URI -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn parse_matrix_entity_from(uri: String) -> Option { if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) { return Some(MatrixEntity { @@ -154,33 +154,33 @@ impl From<&RumaMatrixId> for MatrixId { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn media_source_from_url(url: String) -> Arc { Arc::new(MediaSource::Plain(url.into())) } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn message_event_content_new( msgtype: MessageType, ) -> Result, ClientError> { Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?))) } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn message_event_content_from_markdown( md: String, ) -> Arc { Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md))) } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn message_event_content_from_markdown_as_emote( md: String, ) -> Arc { Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md))) } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn message_event_content_from_html( body: String, html_body: String, @@ -190,7 +190,7 @@ pub fn message_event_content_from_html( ))) } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn message_event_content_from_html_as_emote( body: String, html_body: String, @@ -918,7 +918,7 @@ impl From for PollKind { /// Creates a [`RoomMessageEventContentWithoutRelation`] given a /// [`MessageContent`] value. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn content_without_relation_from_message( message: MessageContent, ) -> Result, ClientError> { diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index a34ecfa6aa3..f0cf5437425 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -20,7 +20,7 @@ pub struct SessionVerificationEmoji { description: String, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl SessionVerificationEmoji { pub fn symbol(&self) -> String { self.symbol.clone() @@ -37,7 +37,7 @@ pub enum SessionVerificationData { Decimals { values: Vec }, } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait SessionVerificationControllerDelegate: Sync + Send { fn did_accept_verification_request(&self); fn did_start_sas_verification(&self); @@ -58,7 +58,7 @@ pub struct SessionVerificationController { sas_verification: Arc>>, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl SessionVerificationController { pub async fn is_verified(&self) -> Result { let device = diff --git a/bindings/matrix-sdk-ffi/src/sync_service.rs b/bindings/matrix-sdk-ffi/src/sync_service.rs index 329784c3e1b..3d3d30b0a66 100644 --- a/bindings/matrix-sdk-ffi/src/sync_service.rs +++ b/bindings/matrix-sdk-ffi/src/sync_service.rs @@ -51,7 +51,7 @@ impl From for SyncServiceState { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait SyncServiceStateObserver: Send + Sync + Debug { fn on_update(&self, state: SyncServiceState); } @@ -62,7 +62,7 @@ pub struct SyncService { utd_hook: Option>, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl SyncService { pub fn room_list_service(&self) -> Arc { Arc::new(RoomListService { @@ -110,7 +110,7 @@ impl SyncServiceBuilder { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl SyncServiceBuilder { pub fn with_cross_process_lock(self: Arc, app_identifier: Option) -> Arc { let this = unwrap_or_clone_arc(self); @@ -153,7 +153,7 @@ impl SyncServiceBuilder { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait UnableToDecryptDelegate: Sync + Send { fn on_utd(&self, info: UnableToDecryptInfo); } diff --git a/bindings/matrix-sdk-ffi/src/task_handle.rs b/bindings/matrix-sdk-ffi/src/task_handle.rs index e1e50bf6fbe..5a593fa55fa 100644 --- a/bindings/matrix-sdk-ffi/src/task_handle.rs +++ b/bindings/matrix-sdk-ffi/src/task_handle.rs @@ -17,7 +17,7 @@ impl TaskHandle { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl TaskHandle { // Cancel a task handle. pub fn cancel(&self) { diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index 3705e3ccd59..e39f2c0798d 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -195,7 +195,7 @@ impl InReplyToDetails { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl InReplyToDetails { pub fn event_id(&self) -> String { self.event_id.clone() diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 8ca950f5c02..9182fcf55fb 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -142,7 +142,7 @@ impl Timeline { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl Timeline { pub async fn add_listener(&self, listener: Box) -> Arc { let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await; @@ -688,7 +688,7 @@ pub struct SendHandle { inner: Mutex>, } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl SendHandle { /// Try to abort the sending of the current event. /// @@ -723,12 +723,12 @@ pub enum FocusEventError { Other { msg: String }, } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait TimelineListener: Sync + Send { fn on_update(&self, diff: Vec>); } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait PaginationStatusListener: Sync + Send { fn on_update(&self, status: LiveBackPaginationStatus); } @@ -778,7 +778,7 @@ impl TimelineDiff { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl TimelineDiff { pub fn change(&self) -> TimelineChange { match self { @@ -878,7 +878,7 @@ impl TimelineItem { } } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl TimelineItem { pub fn as_event(self: Arc) -> Option { let event_item = self.0.as_event()?; @@ -1108,7 +1108,7 @@ impl From for Receipt { #[derive(uniffi::Object)] pub struct EventTimelineItemDebugInfoProvider(Arc); -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl EventTimelineItemDebugInfoProvider { fn get(&self) -> EventTimelineItemDebugInfo { EventTimelineItemDebugInfo { @@ -1202,7 +1202,7 @@ impl SendAttachmentJoinHandle { } } -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl SendAttachmentJoinHandle { pub async fn join(&self) -> Result<(), RoomError> { let join_hdl = self.join_hdl.clone(); @@ -1274,7 +1274,7 @@ impl TryFrom for SdkEditedContent { #[derive(Clone, uniffi::Object)] pub struct EventShieldsProvider(Arc); -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl EventShieldsProvider { fn get_shields(&self, strict: bool) -> Option { self.0.get_shield(strict).map(Into::into) diff --git a/bindings/matrix-sdk-ffi/src/timeline_event_filter.rs b/bindings/matrix-sdk-ffi/src/timeline_event_filter.rs index 9d6472eaf4a..5da36b9b1e4 100644 --- a/bindings/matrix-sdk-ffi/src/timeline_event_filter.rs +++ b/bindings/matrix-sdk-ffi/src/timeline_event_filter.rs @@ -10,7 +10,7 @@ pub struct TimelineEventTypeFilter { inner: InnerTimelineEventTypeFilter, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl TimelineEventTypeFilter { #[uniffi::constructor] pub fn include(event_types: Vec) -> Arc { diff --git a/bindings/matrix-sdk-ffi/src/tracing.rs b/bindings/matrix-sdk-ffi/src/tracing.rs index 9e1207d62fb..cc50f14a1e3 100644 --- a/bindings/matrix-sdk-ffi/src/tracing.rs +++ b/bindings/matrix-sdk-ffi/src/tracing.rs @@ -19,7 +19,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind}; /// level + target) it is called with. Please make sure that the number of /// different combinations of those parameters this can be called with is /// constant in the final executable. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] fn log_event(file: String, line: Option, level: LogLevel, target: String, message: String) { static CALLSITES: Mutex> = Mutex::new(BTreeMap::new()); @@ -96,7 +96,7 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool { #[derive(uniffi::Object)] pub struct Span(tracing::Span); -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl Span { /// Create a span originating at the given callsite (file, line and column). /// diff --git a/bindings/matrix-sdk-ffi/src/widget.rs b/bindings/matrix-sdk-ffi/src/widget.rs index 9cdd5856033..2e5ca65c8a1 100644 --- a/bindings/matrix-sdk-ffi/src/widget.rs +++ b/bindings/matrix-sdk-ffi/src/widget.rs @@ -15,7 +15,7 @@ pub struct WidgetDriverAndHandle { pub handle: Arc, } -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn make_widget_driver(settings: WidgetSettings) -> Result { let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?); Ok(WidgetDriverAndHandle { @@ -29,7 +29,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result>); -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl WidgetDriver { pub async fn run( &self, @@ -96,7 +96,7 @@ impl From for WidgetSettings { /// * `room` - A matrix room which is used to query the logged in username /// * `props` - Properties from the client that can be used by a widget to adapt /// to the client. e.g. language, font-scale... -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] pub async fn generate_webview_url( widget_settings: WidgetSettings, room: Arc, @@ -241,7 +241,7 @@ impl From for matrix_sdk::widget::VirtualElemen /// /// * `props` - A struct containing the configuration parameters for a element /// call widget. -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn new_virtual_element_call_widget( props: VirtualElementCallWidgetOptions, ) -> Result { @@ -261,7 +261,7 @@ pub fn new_virtual_element_call_widget( /// Editing and extending the capabilities from this function is also possible, /// but should only be done as temporal workarounds until this function is /// adjusted -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] pub fn get_element_call_required_permissions( own_user_id: String, own_device_id: String, @@ -354,7 +354,7 @@ impl From for matrix_sdk::widget::ClientProperties { #[derive(uniffi::Object)] pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle); -#[uniffi::export(async_runtime = "tokio")] +#[matrix_sdk_ffi_macros::export_async] impl WidgetDriverHandle { /// Receive a message from the widget driver. /// @@ -469,7 +469,7 @@ impl From for WidgetEventFilter { } } -#[uniffi::export(callback_interface)] +#[matrix_sdk_ffi_macros::export(callback_interface)] pub trait WidgetCapabilitiesProvider: Send + Sync { fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities; } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 096ce0250e9..f2dcdf32933 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -44,7 +44,7 @@ sso-login = ["dep:axum", "dep:rand", "dep:tower"] image-proc = ["dep:image"] image-rayon = ["image-proc", "image?/rayon"] -uniffi = ["dep:uniffi", "matrix-sdk-base/uniffi"] +uniffi = ["dep:uniffi", "matrix-sdk-base/uniffi", "dep:matrix-sdk-ffi-macros"] experimental-oidc = [ "ruma/unstable-msc2967", @@ -92,6 +92,7 @@ language-tags = { version = "0.3.2", optional = true } mas-oidc-client = { version = "0.10.0", default-features = false, optional = true } matrix-sdk-base = { workspace = true } matrix-sdk-common = { workspace = true } +matrix-sdk-ffi-macros = { workspace = true, optional = true } matrix-sdk-indexeddb = { workspace = true, optional = true } matrix-sdk-sqlite = { workspace = true, optional = true } matrix-sdk-test = { workspace = true, optional = true } diff --git a/crates/matrix-sdk/src/oidc/auth_code_builder.rs b/crates/matrix-sdk/src/oidc/auth_code_builder.rs index 148c6ca7d7d..97b4c799743 100644 --- a/crates/matrix-sdk/src/oidc/auth_code_builder.rs +++ b/crates/matrix-sdk/src/oidc/auth_code_builder.rs @@ -246,7 +246,7 @@ pub struct OidcAuthorizationData { } #[cfg(feature = "uniffi")] -#[uniffi::export] +#[matrix_sdk_ffi_macros::export] impl OidcAuthorizationData { /// The login URL to use for authorization. pub fn login_url(&self) -> String { diff --git a/testing/matrix-sdk-ffi-macros/Cargo.toml b/testing/matrix-sdk-ffi-macros/Cargo.toml new file mode 100644 index 00000000000..54fb31ea7d5 --- /dev/null +++ b/testing/matrix-sdk-ffi-macros/Cargo.toml @@ -0,0 +1,24 @@ +[package] +description = "Helper macros to write FFI bindings" +edition = "2021" +homepage = "https://github.com/matrix-org/matrix-rust-sdk" +keywords = ["matrix", "chat", "messaging", "ruma"] +license = "Apache-2.0" +name = "matrix-sdk-ffi-macros" +readme = "README.md" +repository = "https://github.com/matrix-org/matrix-rust-sdk" +rust-version = { workspace = true } +version = "0.7.0" + +[lib] +proc-macro = true +test = false +doctest = false + +[dependencies] +proc-macro2 = "1.0.86" +quote = "1.0.18" +syn = { version = "2.0.43", features = ["full", "extra-traits"] } + +[lints] +workspace = true diff --git a/testing/matrix-sdk-ffi-macros/README.md b/testing/matrix-sdk-ffi-macros/README.md new file mode 100644 index 00000000000..17c29d67bbf --- /dev/null +++ b/testing/matrix-sdk-ffi-macros/README.md @@ -0,0 +1,14 @@ +[![Build Status](https://img.shields.io/travis/matrix-org/matrix-rust-sdk.svg?style=flat-square)](https://travis-ci.org/matrix-org/matrix-rust-sdk) +[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk) +[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0) +[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org) + +# matrix-sdk-ffi-macros + +Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK. + +**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably +interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate. + +[Matrix]: https://matrix.org/ +[Rust]: https://www.rust-lang.org/ diff --git a/testing/matrix-sdk-ffi-macros/src/lib.rs b/testing/matrix-sdk-ffi-macros/src/lib.rs new file mode 100644 index 00000000000..6c55aa0c797 --- /dev/null +++ b/testing/matrix-sdk-ffi-macros/src/lib.rs @@ -0,0 +1,78 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use proc_macro::TokenStream; +use quote::quote; +use syn::{spanned::Spanned as _, ImplItem, Item}; + +/// Attribute to always specify the async runtime parameter for the `uniffi` +/// export macros. +#[proc_macro_attribute] +pub fn export_async(_attr: TokenStream, item: TokenStream) -> TokenStream { + let item = proc_macro2::TokenStream::from(item); + + quote! { + #[uniffi::export(async_runtime = "tokio")] + #item + } + .into() +} + +/// Attribute to always specify the async runtime parameter for the `uniffi` +/// export macros. +#[proc_macro_attribute] +pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream { + let run_checks = || { + let item: Item = syn::parse(item.clone())?; + if let Item::Fn(fun) = &item { + // Fail compilation if the function is async. + if fun.sig.asyncness.is_some() { + let error = syn::Error::new( + fun.span(), + "async function must be exported with #[export_async]", + ); + return Err(error); + } + } else if let Item::Impl(blk) = &item { + // Fail compilation if at least one function in the impl block is async. + for item in &blk.items { + if let ImplItem::Fn(fun) = item { + if fun.sig.asyncness.is_some() { + let error = syn::Error::new( + blk.span(), + "impl block with async functions must be exported with #[export_async]", + ); + return Err(error); + } + } + } + } + + Ok(()) + }; + + let maybe_error = + if let Err(err) = run_checks() { Some(err.into_compile_error()) } else { None }; + + let item = proc_macro2::TokenStream::from(item); + let attr = proc_macro2::TokenStream::from(attr); + + quote! { + #maybe_error + + #[uniffi::export(#attr)] + #item + } + .into() +} From 752706c51dd514547da0a80905e611f3ebdeff2f Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Sat, 14 Sep 2024 00:03:03 +0200 Subject: [PATCH 241/979] Get back to Recovering syncing when we haven't sync for a while --- .../src/room_list_service/mod.rs | 31 +-- .../src/room_list_service/state.rs | 214 ++++++++++++++---- 2 files changed, 192 insertions(+), 53 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 7f3f27b5aa1..81ce260947d 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -60,7 +60,7 @@ mod state; use std::{sync::Arc, time::Duration}; use async_stream::stream; -use eyeball::{SharedObservable, Subscriber}; +use eyeball::Subscriber; use futures_util::{pin_mut, Stream, StreamExt}; use matrix_sdk::{ event_cache::EventCacheError, Client, Error as SlidingSyncError, SlidingSync, SlidingSyncList, @@ -89,7 +89,7 @@ pub struct RoomListService { /// The current state of the `RoomListService`. /// /// `RoomListService` is a simple state-machine. - state: SharedObservable, + state_machine: StateMachine, } impl RoomListService { @@ -172,7 +172,7 @@ impl RoomListService { // Eagerly subscribe the event cache to sync responses. client.event_cache().subscribe()?; - Ok(Self { client, sliding_sync, state: SharedObservable::new(State::Init) }) + Ok(Self { client, sliding_sync, state_machine: StateMachine::new() }) } /// Start to sync the room list. @@ -208,7 +208,7 @@ impl RoomListService { debug!("Run a sync iteration"); // Calculate the next state, and run the associated actions. - let next_state = self.state.get().next(&self.sliding_sync).await?; + let next_state = self.state_machine.next(&self.sliding_sync).await?; // Do the sync. match sync.next().await { @@ -217,7 +217,7 @@ impl RoomListService { debug!(state = ?next_state, "New state"); // Update the state. - self.state.set(next_state); + self.state_machine.set(next_state); yield Ok(()); } @@ -227,7 +227,7 @@ impl RoomListService { debug!(expected_state = ?next_state, "New state is an error"); let next_state = State::Error { from: Box::new(next_state) }; - self.state.set(next_state); + self.state_machine.set(next_state); yield Err(Error::SlidingSync(error)); @@ -239,7 +239,7 @@ impl RoomListService { debug!(expected_state = ?next_state, "New state is a termination"); let next_state = State::Terminated { from: Box::new(next_state) }; - self.state.set(next_state); + self.state_machine.set(next_state); break; } @@ -286,8 +286,8 @@ impl RoomListService { // when the session is forced to expire, the state remains `Terminated`, thus // the actions aren't executed as expected. Consequently, let's update the // state. - if let State::Terminated { from } = self.state.get() { - self.state.set(State::Error { from }); + if let State::Terminated { from } = self.state_machine.get() { + self.state_machine.set(State::Error { from }); } } @@ -341,7 +341,7 @@ impl RoomListService { // Update the `current_state`. current_state = next_state; } else { - // Something is broken with `self.state`. Let's stop this stream too. + // Something is broken with the state. Let's stop this stream too. break; } } @@ -355,7 +355,7 @@ impl RoomListService { /// Get a subscriber to the state. pub fn state(&self) -> Subscriber { - self.state.subscribe() + self.state_machine.subscribe() } async fn list_for(&self, sliding_sync_list_name: &str) -> Result { @@ -396,7 +396,7 @@ impl RoomListService { settings.required_state.push((StateEventType::RoomCreate, "".to_owned())); } - let cancel_in_flight_request = match self.state.get() { + let cancel_in_flight_request = match self.state_machine.get() { State::Init | State::Recovering | State::Error { .. } | State::Terminated { .. } => { false } @@ -617,13 +617,16 @@ mod tests { let _ = sync.next().await; // State is `Terminated`, as expected! - assert_eq!(room_list.state.get(), State::Terminated { from: Box::new(State::Running) }); + assert_eq!( + room_list.state_machine.get(), + State::Terminated { from: Box::new(State::Running) } + ); // Now, let's make the sliding sync session to expire. room_list.expire_sync_session().await; // State is `Error`, as a regular session expiration would generate! - assert_eq!(room_list.state.get(), State::Error { from: Box::new(State::Running) }); + assert_eq!(room_list.state_machine.get(), State::Error { from: Box::new(State::Running) }); Ok(()) } diff --git a/crates/matrix-sdk-ui/src/room_list_service/state.rs b/crates/matrix-sdk-ui/src/room_list_service/state.rs index edf189cbb64..31d6bc4f9fd 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/state.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/state.rs @@ -14,15 +14,20 @@ //! States and actions for the `RoomList` state machine. -use std::future::ready; +use std::{ + future::ready, + sync::Mutex, + time::{Duration, Instant}, +}; +use eyeball::{SharedObservable, Subscriber}; use matrix_sdk::{sliding_sync::Range, SlidingSync, SlidingSyncMode}; use super::Error; pub const ALL_ROOMS_LIST_NAME: &str = "all_rooms"; -/// The state of the [`super::RoomList`]' state machine. +/// The state of the [`super::RoomList`]. #[derive(Clone, Debug, PartialEq)] pub enum State { /// That's the first initial state. @@ -46,13 +51,71 @@ pub enum State { Terminated { from: Box }, } -impl State { +/// Default value for `StateMachine::state_lifespan`. +const DEFAULT_STATE_LIFESPAN: Duration = Duration::from_secs(1800); + +/// The state machine used to transition between the [`State`]s. +#[derive(Debug)] +pub struct StateMachine { + /// The current state of the `RoomListService`. + state: SharedObservable, + + /// Last time the state has been updated. + /// + /// When the state has not been updated since a long time, we want to enter + /// the [`State::Recovering`] state. Why do we need to do that? Because in + /// some cases, the user might have received many updates between two + /// distant syncs. If the sliding sync list range was too large, like + /// 0..=499, the next sync is likely to be heavy and potentially slow. + /// In this case, it's preferable to jump back onto `Recovering`, which will + /// reset the range, so that the next sync will be fast for the client. + /// + /// To be used in coordination with `Self::state_lifespan`. + /// + /// This mutex is only taken for short periods of time, so it's sync. + last_state_update_time: Mutex, + + /// The maximum time before considering the state as “too old”. + /// + /// To be used in coordination with `Self::last_state_update_time`. + state_lifespan: Duration, +} + +impl StateMachine { + pub(super) fn new() -> Self { + StateMachine { + state: SharedObservable::new(State::Init), + last_state_update_time: Mutex::new(Instant::now()), + state_lifespan: DEFAULT_STATE_LIFESPAN, + } + } + + /// Get the current state. + pub(super) fn get(&self) -> State { + self.state.get() + } + + /// Set the new state. + /// + /// Setting a new state will update `Self::last_state_update`. + pub(super) fn set(&self, state: State) { + let mut last_state_update_time = self.last_state_update_time.lock().unwrap(); + *last_state_update_time = Instant::now(); + + self.state.set(state); + } + + /// Subscribe to state updates. + pub fn subscribe(&self) -> Subscriber { + self.state.subscribe() + } + /// Transition to the next state, and execute the associated transition's /// [`Actions`]. - pub(super) async fn next(&self, sliding_sync: &SlidingSync) -> Result { + pub(super) async fn next(&self, sliding_sync: &SlidingSync) -> Result { use State::*; - let next_state = match self { + let next_state = match self.get() { Init => SettingUp, SettingUp | Recovering => { @@ -60,7 +123,18 @@ impl State { Running } - Running => Running, + Running => { + // We haven't changed the state for a while, we go back to `Recovering` to avoid + // requesting potentially large data. See `Self::last_state_update` to learn + // the details. + if self.last_state_update_time.lock().unwrap().elapsed() > self.state_lifespan { + set_all_rooms_to_selective_sync_mode(sliding_sync).await?; + + Recovering + } else { + Running + } + } Error { from: previous_state } | Terminated { from: previous_state } => { match previous_state.as_ref() { @@ -122,6 +196,7 @@ pub const ALL_ROOMS_DEFAULT_GROWING_BATCH_SIZE: u32 = 100; #[cfg(test)] mod tests { use matrix_sdk_test::async_test; + use tokio::time::sleep; use super::{super::tests::new_room_list, *}; @@ -130,94 +205,155 @@ mod tests { let room_list = new_room_list().await?; let sliding_sync = room_list.sliding_sync(); - // First state. - let state = State::Init; + let state_machine = StateMachine::new(); // Hypothetical error. { - let state = State::Error { from: Box::new(state.clone()) }.next(sliding_sync).await?; + state_machine.set(State::Error { from: Box::new(state_machine.get()) }); // Back to the previous state. - assert_eq!(state, State::Init); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Init); } // Hypothetical termination. { - let state = - State::Terminated { from: Box::new(state.clone()) }.next(sliding_sync).await?; + state_machine.set(State::Terminated { from: Box::new(state_machine.get()) }); // Back to the previous state. - assert_eq!(state, State::Init); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Init); } // Next state. - let state = state.next(sliding_sync).await?; - assert_eq!(state, State::SettingUp); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::SettingUp); // Hypothetical error. { - let state = State::Error { from: Box::new(state.clone()) }.next(sliding_sync).await?; + state_machine.set(State::Error { from: Box::new(state_machine.get()) }); // Back to the previous state. - assert_eq!(state, State::SettingUp); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::SettingUp); } // Hypothetical termination. { - let state = - State::Terminated { from: Box::new(state.clone()) }.next(sliding_sync).await?; + state_machine.set(State::Terminated { from: Box::new(state_machine.get()) }); // Back to the previous state. - assert_eq!(state, State::SettingUp); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::SettingUp); } // Next state. - let state = state.next(sliding_sync).await?; - assert_eq!(state, State::Running); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); // Hypothetical error. { - let state = State::Error { from: Box::new(state.clone()) }.next(sliding_sync).await?; + state_machine.set(State::Error { from: Box::new(state_machine.get()) }); // Jump to the **recovering** state! - assert_eq!(state, State::Recovering); - - let state = state.next(sliding_sync).await?; + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Recovering); // Now, back to the previous state. - assert_eq!(state, State::Running); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); } // Hypothetical termination. { - let state = - State::Terminated { from: Box::new(state.clone()) }.next(sliding_sync).await?; + state_machine.set(State::Terminated { from: Box::new(state_machine.get()) }); // Jump to the **recovering** state! - assert_eq!(state, State::Recovering); - - let state = state.next(sliding_sync).await?; + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Recovering); // Now, back to the previous state. - assert_eq!(state, State::Running); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); } // Hypothetical error when recovering. { - let state = - State::Error { from: Box::new(State::Recovering) }.next(sliding_sync).await?; + state_machine.set(State::Error { from: Box::new(State::Recovering) }); // Back to the previous state. - assert_eq!(state, State::Recovering); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Recovering); } // Hypothetical termination when recovering. { - let state = - State::Terminated { from: Box::new(State::Recovering) }.next(sliding_sync).await?; + state_machine.set(State::Terminated { from: Box::new(State::Recovering) }); // Back to the previous state. - assert_eq!(state, State::Recovering); + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Recovering); + } + + Ok(()) + } + + #[async_test] + async fn test_recover_state_after_delay() -> Result<(), Error> { + let room_list = new_room_list().await?; + let sliding_sync = room_list.sliding_sync(); + + let mut state_machine = StateMachine::new(); + state_machine.state_lifespan = Duration::from_millis(50); + + { + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::SettingUp); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + } + + // Time passes. + sleep(Duration::from_millis(100)).await; + + { + // Time has elapsed, time to recover. + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Recovering); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + } + + // Time passes, again. Just to test everything is going well. + sleep(Duration::from_millis(100)).await; + + { + // Time has elapsed, time to recover. + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Recovering); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); + + state_machine.set(state_machine.next(sliding_sync).await?); + assert_eq!(state_machine.get(), State::Running); } Ok(()) From b793acd2b155a0fbf36eca4990927f765322c98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 8 Oct 2024 15:42:01 +0200 Subject: [PATCH 242/979] sdk-ui: allow already sent local events to be redacted using `redact_by_id` Test this use case. --- crates/matrix-sdk-ui/src/timeline/error.rs | 3 - crates/matrix-sdk-ui/src/timeline/mod.rs | 37 +++-- .../tests/integration/timeline/mod.rs | 133 ++++++++++++++++++ 3 files changed, 151 insertions(+), 22 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index 2afb365e8b8..b1a89956474 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -86,9 +86,6 @@ pub enum RedactError { #[error("Local event to redact wasn't found for transaction {0}")] LocalEventNotFound(OwnedTransactionId), - #[error("Local event with transaction id {0} had a remote `TimelineItemHandle`. This should never happen.")] - InvalidTimelineItemHandle(OwnedTransactionId), - /// An error happened while attempting to redact an event. #[error(transparent)] HttpError(#[from] HttpError), diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index d4be98e8746..433611c44e4 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -601,32 +601,31 @@ impl Timeline { id: &TimelineEventItemId, reason: Option<&str>, ) -> Result<(), Error> { - match id { + let event_id = match id { TimelineEventItemId::TransactionId(transaction_id) => { - let item = self.item_by_transaction_id(transaction_id).await; + let Some(item) = self.item_by_transaction_id(transaction_id).await else { + return Err(Error::RedactError(RedactError::LocalEventNotFound( + transaction_id.to_owned(), + ))); + }; - match item.as_ref().map(|i| i.handle()) { - Some(TimelineItemHandle::Local(handle)) => { + match item.handle() { + TimelineItemHandle::Local(handle) => { + // If there is a local item that hasn't been sent yet, abort the upload handle.abort().await.map_err(RoomSendQueueError::StorageError)?; - Ok(()) + return Ok(()); } - Some(TimelineItemHandle::Remote(_)) => Err(Error::RedactError( - RedactError::InvalidTimelineItemHandle(transaction_id.to_owned()), - )), - None => Err(Error::RedactError(RedactError::LocalEventNotFound( - transaction_id.to_owned(), - ))), + TimelineItemHandle::Remote(event_id) => event_id.to_owned(), } } - TimelineEventItemId::EventId(event_id) => { - self.room() - .redact(event_id, reason, None) - .await - .map_err(|e| Error::RedactError(RedactError::HttpError(e)))?; + TimelineEventItemId::EventId(event_id) => event_id.to_owned(), + }; + self.room() + .redact(&event_id, reason, None) + .await + .map_err(|e| Error::RedactError(RedactError::HttpError(e)))?; - Ok(()) - } - } + Ok(()) } /// Redact an event. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index c05f83498ec..76fb281f9a8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -19,6 +19,7 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::{ + assert_let_timeout, config::SyncSettings, test_utils::{events::EventFactory, logged_in_client_with_server}, }; @@ -307,6 +308,72 @@ async fn test_redact_message() { assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); } +#[async_test] +async fn test_redact_local_sent_message() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + // Mock event sending. + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200) + .set_body_json(json!({ "event_id": "$wWgymRfo7ri1uQx0NXO40vLJ" })), + ) + .expect(1) + .mount(&server) + .await; + + // Send the event so it's added to the send queue as a local event. + timeline + .send(RoomMessageEventContent::text_plain("i will disappear soon").into()) + .await + .unwrap(); + + // Assert the local event is in the timeline now and is not sent yet. + assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + let event = item.as_event().unwrap(); + assert!(event.is_local_echo()); + assert_matches!(event.send_state(), Some(EventSendState::NotSentYet)); + + // As well as a day divider. + assert_let_timeout!( + Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next() + ); + assert!(day_divider.is_day_divider()); + + // We receive an update in the timeline from the send queue. + assert_let_timeout!(Some(VectorDiff::Set { index, value: item }) = timeline_stream.next()); + assert_eq!(index, 1); + + // Check the event is sent but still considered local. + let event = item.as_event().unwrap(); + assert!(event.is_local_echo()); + assert_matches!(event.send_state(), Some(EventSendState::Sent { .. })); + + // Mock the redaction response for the event we just sent. Ensure it's called + // once. + mock_redaction(event.event_id().unwrap()).expect(1).mount(&server).await; + + // Let's redact the local echo with the remote handle. + timeline.redact(event, None).await.unwrap(); +} + #[async_test] async fn test_redact_by_id_message() { let room_id = room_id!("!a98sd12bjh:example.org"); @@ -381,6 +448,72 @@ async fn test_redact_by_id_message() { assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); } +#[async_test] +async fn test_redact_by_local_sent_message() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client_with_server().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + mock_encryption_state(&server, false).await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline().await.unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + // Mock event sending. + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with( + ResponseTemplate::new(200) + .set_body_json(json!({ "event_id": "$wWgymRfo7ri1uQx0NXO40vLJ" })), + ) + .expect(1) + .mount(&server) + .await; + + // Send the event so it's added to the send queue as a local event. + timeline + .send(RoomMessageEventContent::text_plain("i will disappear soon").into()) + .await + .unwrap(); + + // Assert the local event is in the timeline now and is not sent yet. + assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + let event = item.as_event().unwrap(); + assert!(event.is_local_echo()); + assert_matches!(event.send_state(), Some(EventSendState::NotSentYet)); + + // As well as a day divider. + assert_let_timeout!( + Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next() + ); + assert!(day_divider.is_day_divider()); + + // We receive an update in the timeline from the send queue. + assert_let_timeout!(Some(VectorDiff::Set { index, value: item }) = timeline_stream.next()); + assert_eq!(index, 1); + + // Check the event is sent but still considered local. + let event = item.as_event().unwrap(); + assert!(event.is_local_echo()); + assert_matches!(event.send_state(), Some(EventSendState::Sent { .. })); + + // Mock the redaction response for the event we just sent. Ensure it's called + // once. + mock_redaction(event.event_id().unwrap()).expect(1).mount(&server).await; + + // Let's redact the local echo with the remote handle. + timeline.redact_by_id(&event.identifier(), None).await.unwrap(); +} + #[async_test] async fn test_redact_by_id_message_with_no_remote_message_present() { let room_id = room_id!("!a98sd12bjh:example.org"); From 17370a570223ebe4d4e5ae6111456a66f996f3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 8 Oct 2024 18:45:19 +0200 Subject: [PATCH 243/979] sdk: Upgrade aquamarine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finally get rid of syn 1! Signed-off-by: Kévin Commaille --- Cargo.lock | 134 +++++++++++++---------------------- crates/matrix-sdk/Cargo.toml | 2 +- 2 files changed, 51 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88a5555799d..f562bfe965a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -168,16 +168,16 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" [[package]] name = "aquamarine" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" dependencies = [ "include_dir", "itertools 0.10.5", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -194,7 +194,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -241,7 +241,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.79", + "syn", ] [[package]] @@ -359,7 +359,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -370,7 +370,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -897,7 +897,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1202,7 +1202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1239,7 +1239,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1263,7 +1263,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.79", + "syn", ] [[package]] @@ -1274,7 +1274,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1359,7 +1359,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1747,7 +1747,7 @@ checksum = "dd65f1b59dd22d680c7a626cc4a000c1e03d241c51c3e034d2bc9f1e90734f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1819,7 +1819,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -1995,7 +1995,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -2273,7 +2273,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -2619,7 +2619,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -2915,7 +2915,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -2926,7 +2926,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -2939,7 +2939,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -3096,7 +3096,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -3353,7 +3353,7 @@ version = "0.7.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -3506,7 +3506,7 @@ name = "matrix-sdk-test-macros" version = "0.7.0" dependencies = [ "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -3770,7 +3770,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -3964,7 +3964,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -4196,7 +4196,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -4234,7 +4234,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -4392,30 +4392,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -4462,7 +4438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -4505,7 +4481,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -5118,7 +5094,7 @@ dependencies = [ "quote", "ruma-identifiers-validation", "serde", - "syn 2.0.79", + "syn", "toml 0.8.15", ] @@ -5304,7 +5280,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.79", + "syn", ] [[package]] @@ -5336,7 +5312,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -5432,7 +5408,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -5443,7 +5419,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -5539,7 +5515,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -5711,7 +5687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -5792,7 +5768,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", + "syn", ] [[package]] @@ -5824,16 +5800,6 @@ dependencies = [ "symbolic-common", ] -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.79" @@ -5925,7 +5891,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -6031,7 +5997,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -6217,7 +6183,7 @@ source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -6449,7 +6415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcfa22f55829d3aaa7acfb1c5150224188fe0f27c59a8a3eddcaa24d1ffbe58" dependencies = [ "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -6481,7 +6447,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.79", + "syn", "toml 0.5.11", "uniffi_meta", ] @@ -6713,7 +6679,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn", "wasm-bindgen-shared", ] @@ -6747,7 +6713,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6780,7 +6746,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -7131,7 +7097,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] @@ -7151,7 +7117,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn", ] [[package]] diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index f2dcdf32933..2b2a5cd7062 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -68,7 +68,7 @@ docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode", "image [dependencies] anyhow = { workspace = true, optional = true } anymap2 = "0.13.0" -aquamarine = "0.5.0" +aquamarine = "0.6.0" assert_matches2 = { workspace = true, optional = true } as_variant = { workspace = true } async-channel = "2.2.1" From 9d976d0bcf9b73a9f9c7e479d3dc20502cee2f69 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 8 Oct 2024 11:54:27 +0100 Subject: [PATCH 244/979] sdk: Update get_media_file to take a filename instead of the body. --- crates/matrix-sdk/CHANGELOG.md | 1 + crates/matrix-sdk/src/media.rs | 92 ++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 915838e009d..1f2e7ab4b54 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -25,6 +25,7 @@ Breaking changes: - `Room::event` now takes an optional `RequestConfig` to allow for tweaking the network behavior. - The `instant` module was removed, use the `ruma::time` module instead. - Add `ClientBuilder::sqlite_store_with_cache_path` to build a client that stores caches in a different directory to state/crypto. +- The `body` parameter in `get_media_file` has been replaced with a `filename` parameter now that Ruma has a `filename()` method. Additions: diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index e8f62372ed3..29e9f7b4055 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -176,8 +176,13 @@ impl Media { /// /// * `request` - The `MediaRequest` of the content. /// + /// * `filename` - The filename specified in the event. It is suggested to + /// use the `filename()` method on the event's content instead of using + /// the `filename` field directly. If not provided, a random name will be + /// generated. + /// /// * `content_type` - The type of the media, this will be used to set the - /// temporary file's extension. + /// temporary file's extension when one isn't included in the filename. /// /// * `use_cache` - If we should use the media cache for this request. /// @@ -189,7 +194,7 @@ impl Media { pub async fn get_media_file( &self, request: &MediaRequest, - body: Option, + filename: Option, content_type: &Mime, use_cache: bool, temp_dir: Option, @@ -198,50 +203,51 @@ impl Media { let inferred_extension = mime2ext::mime2ext(content_type); - let body_path = body.as_ref().map(Path::new); - let filename = body_path.and_then(|f| f.file_name().and_then(|f| f.to_str())); - let filename_with_extension = body_path.and_then(|f| { - if f.extension().is_some() { - f.file_name().and_then(|f| f.to_str()) - } else { - None - } - }); + let filename_as_path = filename.as_ref().map(Path::new); - let (temp_file, temp_dir) = match (filename, filename_with_extension, inferred_extension) { - // If the body is a file name and has an extension use that - (Some(_), Some(filename_with_extension), Some(_)) => { - // Use an intermediary directory to avoid conflicts - let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?; - let temp_file = TempFileBuilder::new() - .prefix(filename_with_extension) - .rand_bytes(0) - .tempfile_in(&temp_dir)?; - (temp_file, Some(temp_dir)) - } - // If the body is a file name but doesn't have an extension try inferring one for it - (Some(filename), None, Some(inferred_extension)) => { - // Use an intermediary directory to avoid conflicts - let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?; - let temp_file = TempFileBuilder::new() - .prefix(filename) - .suffix(&(".".to_owned() + inferred_extension)) - .rand_bytes(0) - .tempfile_in(&temp_dir)?; - (temp_file, Some(temp_dir)) - } - // If the only thing we have is an inferred extension then use that together with a - // randomly generated file name - (None, None, Some(inferred_extension)) => ( - TempFileBuilder::new() - .suffix(&&(".".to_owned() + inferred_extension)) - .tempfile()?, - None, - ), - // Otherwise just use a completely random file name - _ => (TempFileBuilder::new().tempfile()?, None), + let (sanitized_filename, filename_has_extension) = if let Some(path) = filename_as_path { + let sanitized_filename = path.file_name().and_then(|f| f.to_str()); + let filename_has_extension = path.extension().is_some(); + (sanitized_filename, filename_has_extension) + } else { + (None, false) }; + let (temp_file, temp_dir) = + match (sanitized_filename, filename_has_extension, inferred_extension) { + // If the file name has an extension use that + (Some(filename_with_extension), true, _) => { + // Use an intermediary directory to avoid conflicts + let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?; + let temp_file = TempFileBuilder::new() + .prefix(filename_with_extension) + .rand_bytes(0) + .tempfile_in(&temp_dir)?; + (temp_file, Some(temp_dir)) + } + // If the file name doesn't have an extension try inferring one for it + (Some(filename), false, Some(inferred_extension)) => { + // Use an intermediary directory to avoid conflicts + let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?; + let temp_file = TempFileBuilder::new() + .prefix(filename) + .suffix(&(".".to_owned() + inferred_extension)) + .rand_bytes(0) + .tempfile_in(&temp_dir)?; + (temp_file, Some(temp_dir)) + } + // If the only thing we have is an inferred extension then use that together with a + // randomly generated file name + (None, _, Some(inferred_extension)) => ( + TempFileBuilder::new() + .suffix(&&(".".to_owned() + inferred_extension)) + .tempfile()?, + None, + ), + // Otherwise just use a completely random file name + _ => (TempFileBuilder::new().tempfile()?, None), + }; + let mut file = TokioFile::from_std(temp_file.reopen()?); file.write_all(&data).await?; // Make sure the file metadata is flushed to disk. From 95ae5d193876cf5701b7b44df450ab094d017b1c Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 8 Oct 2024 11:55:01 +0100 Subject: [PATCH 245/979] ffi: Rename get_media_file body parameter to filename. --- bindings/matrix-sdk-ffi/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 314b19eab1e..f6646b2b176 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -414,7 +414,7 @@ impl Client { pub async fn get_media_file( &self, media_source: Arc, - body: Option, + filename: Option, mime_type: String, use_cache: bool, temp_dir: Option, @@ -427,7 +427,7 @@ impl Client { .media() .get_media_file( &MediaRequest { source, format: MediaFormat::File }, - body, + filename, &mime_type, use_cache, temp_dir, From b36a9ad78162e5a33c124f58f4f4e2dd74e0a86f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 2 Oct 2024 09:18:29 +0100 Subject: [PATCH 246/979] timeline: Add documentation to `[Sync]TimelineEvent` I found it hard to understand what these two structs were for, so let's start by giving them some documentation. --- .../src/deserialized_responses.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index e01ea298323..394f13bb55f 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -297,8 +297,11 @@ pub struct EncryptionInfo { pub verification_state: VerificationState, } -/// A customized version of a room event coming from a sync that holds optional -/// encryption info. +/// Represents a matrix room event that has been returned from `/sync`, +/// after initial processing. +/// +/// This is almost identical to [`TimelineEvent`], but wraps an +/// [`AnySyncTimelineEvent`] instead of [`AnyTimelineEvent`]. #[derive(Clone, Deserialize, Serialize)] pub struct SyncTimelineEvent { /// The actual event. @@ -388,6 +391,16 @@ impl From for SyncTimelineEvent { } } +/// Represents a matrix room event that has been returned from a Matrix +/// client-server API endpoint such as `/messages`, after initial processing. +/// +/// The "initial processing" includes an attempt to decrypt encrypted events, so +/// the main thing this adds over [`AnyTimelineEvent`] is information on +/// encryption. +/// +/// See also [`SyncTimelineEvent`] which is almost identical, but is used for +/// results from the `/sync` endpoint (which lack a `room_id` property) and +/// hence wraps an [`AnySyncTimelineEvent`] instead of [`AnyTimelineEvent`]. #[derive(Clone)] pub struct TimelineEvent { /// The actual event. From ce231e6c2b41cf8ff759012bc51dbe551b3398a2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 3 Oct 2024 09:51:35 +0100 Subject: [PATCH 247/979] timeline: test for `SyncTimelineEvent` serialization I'm going to change the internal structure of `SyncTimelineEvent`, and since it implements `Deserialize`, we need to not break it. Let's add a test for the current format. --- .../src/deserialized_responses.rs | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 394f13bb55f..891e22df8c3 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -537,14 +537,19 @@ pub struct UnableToDecryptInfo { #[cfg(test)] mod tests { + use assert_matches::assert_matches; use ruma::{ + event_id, events::{room::message::RoomMessageEventContent, AnySyncTimelineEvent}, serde::Raw, + user_id, }; use serde::Deserialize; use serde_json::json; - use super::{SyncTimelineEvent, TimelineEvent, VerificationState}; + use super::{ + AlgorithmInfo, EncryptionInfo, SyncTimelineEvent, TimelineEvent, VerificationState, + }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; fn example_event() -> serde_json::Value { @@ -619,4 +624,58 @@ mod tests { VerificationState::Unverified(VerificationLevel::UnsignedDevice) ); } + + #[test] + fn sync_timeline_event_serialisation() { + let room_event = SyncTimelineEvent { + event: Raw::new(&example_event()).unwrap().cast(), + encryption_info: Some(EncryptionInfo { + sender: user_id!("@sender:example.com").to_owned(), + sender_device: None, + algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "xxx".to_owned(), + sender_claimed_keys: Default::default(), + }, + verification_state: VerificationState::Verified, + }), + push_actions: Default::default(), + unsigned_encryption_info: None, + }; + + let serialized = serde_json::to_value(&room_event).unwrap(); + + // Test that the serialization is as expected + assert_eq!( + serialized, + json!({ + "event": { + "content": {"body": "secret", "msgtype": "m.text"}, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message", + }, + "encryption_info": { + "sender": "@sender:example.com", + "sender_device": null, + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified", + }, + }) + ); + + // And it can be properly deserialized from the new format. + let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); + assert_matches!( + event.encryption_info.unwrap().algorithm_info, + AlgorithmInfo::MegolmV1AesSha2 { .. } + ) + } } From 4d472f6aedba7c2df330e0efebc5b109c4970bab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 9 Oct 2024 14:24:25 +0100 Subject: [PATCH 248/979] timeline: make `SyncTimelineEvent` fields private ... and add accessors instead. I'm going to change the inner structure of `SyncTimelineEvent`, meaning that access will have to be via an accessor in future. Let's start by making the fields private, and use accessors where we previously used direct access. --- crates/matrix-sdk-base/src/client.rs | 18 ++--- crates/matrix-sdk-base/src/read_receipts.rs | 4 +- crates/matrix-sdk-base/src/rooms/normal.rs | 4 +- .../matrix-sdk-base/src/sliding_sync/mod.rs | 6 +- .../src/deserialized_responses.rs | 71 +++++++++++++++---- .../src/timeline/controller/state.rs | 6 +- .../src/timeline/event_item/mod.rs | 6 +- .../src/timeline/pinned_events_loader.rs | 2 +- .../matrix-sdk-ui/src/timeline/tests/edit.rs | 27 +++++-- crates/matrix-sdk/src/client/mod.rs | 2 +- crates/matrix-sdk/src/event_cache/mod.rs | 2 +- crates/matrix-sdk/src/event_handler/mod.rs | 6 +- crates/matrix-sdk/src/room/edit.rs | 4 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 12 ++-- crates/matrix-sdk/src/sliding_sync/room.rs | 6 +- crates/matrix-sdk/src/test_utils.rs | 2 +- labs/multiverse/src/main.rs | 2 +- .../tests/sliding_sync/notification_client.rs | 2 +- 18 files changed, 119 insertions(+), 63 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 4ec70c10f5c..7cb4fb0c82c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -358,7 +358,7 @@ impl BaseClient { let event: SyncTimelineEvent = olm.decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await?.into(); - if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.event.deserialize() { + if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.raw().deserialize() { match &e { AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original( original_event, @@ -398,7 +398,7 @@ impl BaseClient { for event in events { let mut event: SyncTimelineEvent = event.into(); - match event.event.deserialize() { + match event.raw().deserialize() { Ok(e) => { #[allow(clippy::single_match)] match &e { @@ -432,7 +432,7 @@ impl BaseClient { } } - let raw_event: Raw = event.event.clone().cast(); + let raw_event: Raw = event.raw().clone().cast(); changes.add_state_event(room.room_id(), s.clone(), raw_event); } @@ -443,8 +443,8 @@ impl BaseClient { room_info.room_version().unwrap_or(&RoomVersionId::V1); if let Some(redacts) = r.redacts(room_version) { - room_info.handle_redaction(r, event.event.cast_ref()); - let raw_event = event.event.clone().cast(); + room_info.handle_redaction(r, event.raw().cast_ref()); + let raw_event = event.raw().clone().cast(); changes.add_redaction(room.room_id(), redacts, raw_event); } @@ -456,7 +456,7 @@ impl BaseClient { SyncMessageLikeEvent::Original(_), ) => { if let Ok(Some(e)) = Box::pin( - self.decrypt_sync_room_event(&event.event, room.room_id()), + self.decrypt_sync_room_event(event.raw(), room.room_id()), ) .await { @@ -494,14 +494,14 @@ impl BaseClient { } if let Some(context) = &push_context { - let actions = push_rules.get_actions(&event.event, context); + let actions = push_rules.get_actions(event.raw(), context); if actions.iter().any(Action::should_notify) { notifications.entry(room.room_id().to_owned()).or_default().push( Notification { actions: actions.to_owned(), event: RawAnySyncOrStrippedTimelineEvent::Sync( - event.event.clone(), + event.raw().clone(), ), }, ); @@ -773,7 +773,7 @@ impl BaseClient { if let Ok(Some(decrypted)) = decrypt_sync_room_event.await { // We found an event we can decrypt - if let Ok(any_sync_event) = decrypted.event.deserialize() { + if let Ok(any_sync_event) = decrypted.raw().deserialize() { // We can deserialize it to find its type match is_suitable_for_latest_event(&any_sync_event) { PossibleLatestEvent::YesRoomMessage(_) diff --git a/crates/matrix-sdk-base/src/read_receipts.rs b/crates/matrix-sdk-base/src/read_receipts.rs index c96f7af8198..f582bdd5ade 100644 --- a/crates/matrix-sdk-base/src/read_receipts.rs +++ b/crates/matrix-sdk-base/src/read_receipts.rs @@ -203,7 +203,7 @@ impl RoomReadReceipts { /// Returns whether a new event triggered a new unread/notification/mention. #[inline(always)] fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) { - if marks_as_unread(&event.event, user_id) { + if marks_as_unread(event.raw(), user_id) { self.num_unread += 1; } @@ -408,7 +408,7 @@ impl ReceiptSelector { fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) { for ev in new_events { // Get the `sender` field, if any, or skip this event. - let Ok(Some(sender)) = ev.event.get_field::("sender") else { continue }; + let Ok(Some(sender)) = ev.raw().get_field::("sender") else { continue }; if sender == user_id { // Get the event id, if any, or skip this event. let Some(event_id) = ev.event_id() else { continue }; diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index b5918c15e31..cb3f4bdbd92 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1283,9 +1283,9 @@ impl RoomInfo { if let Some(latest_event) = &mut self.latest_event { tracing::trace!("Checking if redaction applies to latest event"); if latest_event.event_id().as_deref() == Some(redacts) { - match apply_redaction(&latest_event.event().event, _raw, room_version) { + match apply_redaction(latest_event.event().raw(), _raw, room_version) { Some(redacted) => { - latest_event.event_mut().event = redacted; + latest_event.event_mut().set_raw(redacted); debug!("Redacted latest event"); } None => { diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index a2a5dbb00d9..4783777a06b 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -680,7 +680,7 @@ async fn cache_latest_events( Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); for event in events.iter().rev() { - if let Ok(timeline_event) = event.event.deserialize() { + if let Ok(timeline_event) = event.raw().deserialize() { match is_suitable_for_latest_event(&timeline_event) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) @@ -760,7 +760,7 @@ async fn cache_latest_events( // 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(event.event.clone()); + encrypted_events.push(event.raw().clone()); } } _ => { @@ -1686,7 +1686,7 @@ mod tests { // But it's now redacted assert_matches!( - latest_event.event().event.deserialize().unwrap(), + latest_event.event().raw().deserialize().unwrap(), AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( SyncRoomMessageEvent::Redacted(_) )) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 891e22df8c3..7bf6be8f2f3 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -305,10 +305,14 @@ pub struct EncryptionInfo { #[derive(Clone, Deserialize, Serialize)] pub struct SyncTimelineEvent { /// The actual event. - pub event: Raw, + #[serde(rename = "event")] + inner_event: Raw, + /// The encryption info about the event. Will be `None` if the event was not /// encrypted. - pub encryption_info: Option, + #[serde(rename = "encryption_info")] + inner_encryption_info: Option, + /// The push actions associated with this event. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub push_actions: Vec, @@ -325,7 +329,12 @@ impl SyncTimelineEvent { /// This is a convenience constructor for when you don't need to set /// `encryption_info` or `push_action`, for example inside a test. pub fn new(event: Raw) -> Self { - Self { event, encryption_info: None, push_actions: vec![], unsigned_encryption_info: None } + Self { + inner_event: event, + inner_encryption_info: None, + push_actions: vec![], + unsigned_encryption_info: None, + } } /// Create a new `SyncTimelineEvent` from the given raw event and push @@ -337,24 +346,56 @@ impl SyncTimelineEvent { event: Raw, push_actions: Vec, ) -> Self { - Self { event, encryption_info: None, push_actions, unsigned_encryption_info: None } + Self { + inner_event: event, + inner_encryption_info: None, + push_actions, + unsigned_encryption_info: None, + } } /// Get the event id of this `SyncTimelineEvent` if the event has any valid /// id. pub fn event_id(&self) -> Option { - self.event.get_field::("event_id").ok().flatten() + self.inner_event.get_field::("event_id").ok().flatten() + } + + /// Returns a reference to the (potentially decrypted) Matrix event inside + /// this `TimelineEvent`. + pub fn raw(&self) -> &Raw { + &self.inner_event + } + + /// If the event was a decrypted event that was successfully decrypted, get + /// its encryption info. Otherwise, `None`. + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + self.inner_encryption_info.as_ref() + } + + /// Takes ownership of this `TimelineEvent`, returning the (potentially + /// decrypted) Matrix event within. + pub fn into_raw(self) -> Raw { + self.inner_event + } + + /// Replace the Matrix event within this event. Used to handle redaction. + pub fn set_raw(&mut self, event: Raw) { + self.inner_event = event; } } #[cfg(not(tarpaulin_include))] impl fmt::Debug for SyncTimelineEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let SyncTimelineEvent { event, encryption_info, push_actions, unsigned_encryption_info } = - self; + let SyncTimelineEvent { + inner_event, + inner_encryption_info, + push_actions, + unsigned_encryption_info, + } = self; let mut s = f.debug_struct("SyncTimelineEvent"); - s.field("event", &DebugRawEvent(event)); - s.maybe_field("encryption_info", encryption_info); + s.field("event", &DebugRawEvent(inner_event)); + s.maybe_field("encryption_info", inner_encryption_info); if !push_actions.is_empty() { s.field("push_actions", push_actions); } @@ -376,8 +417,8 @@ impl From for SyncTimelineEvent { // this way, we simply cause the `room_id` field in the json to be // ignored by a subsequent deserialization. Self { - event: o.event.cast(), - encryption_info: o.encryption_info, + inner_event: o.event.cast(), + inner_encryption_info: o.encryption_info, push_actions: o.push_actions.unwrap_or_default(), unsigned_encryption_info: o.unsigned_encryption_info, } @@ -579,7 +620,7 @@ mod tests { let converted_room_event: SyncTimelineEvent = room_event.into(); let converted_event: AnySyncTimelineEvent = - converted_room_event.event.deserialize().unwrap(); + converted_room_event.raw().deserialize().unwrap(); assert_eq!(converted_event.event_id(), "$xxxxx:example.org"); assert_eq!(converted_event.sender(), "@carl:example.com"); @@ -628,8 +669,8 @@ mod tests { #[test] fn sync_timeline_event_serialisation() { let room_event = SyncTimelineEvent { - event: Raw::new(&example_event()).unwrap().cast(), - encryption_info: Some(EncryptionInfo { + inner_event: Raw::new(&example_event()).unwrap().cast(), + inner_encryption_info: Some(EncryptionInfo { sender: user_id!("@sender:example.com").to_owned(), sender_device: None, algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { @@ -674,7 +715,7 @@ mod tests { let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); assert_matches!( - event.encryption_info.unwrap().algorithm_info, + event.encryption_info().unwrap().algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { .. } ) } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4cd8095a130..e13b7aec076 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -421,7 +421,7 @@ impl TimelineStateTransaction<'_> { settings: &TimelineSettings, day_divider_adjuster: &mut DayDividerAdjuster, ) -> HandleEventResult { - let raw = event.event; + let raw = event.raw(); let (event_id, sender, timestamp, txn_id, event_kind, should_add) = match raw.deserialize() { Ok(event) => { @@ -580,8 +580,8 @@ impl TimelineStateTransaction<'_> { is_highlighted: event.push_actions.iter().any(Action::is_highlight), flow: Flow::Remote { event_id: event_id.clone(), - raw_event: raw, - encryption_info: event.encryption_info, + raw_event: raw.clone(), + encryption_info: event.encryption_info().cloned(), txn_id, position, }, 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 41f835f1b66..3eb331025a8 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -25,7 +25,7 @@ use matrix_sdk::{ Client, Error, }; use matrix_sdk_base::{ - deserialized_responses::{ShieldStateCode, SyncTimelineEvent, SENT_IN_CLEAR}, + deserialized_responses::{ShieldStateCode, SENT_IN_CLEAR}, latest_event::LatestEvent, }; use once_cell::sync::Lazy; @@ -161,8 +161,8 @@ impl EventTimelineItem { // potential footgun which could one day turn into a security issue. use super::traits::RoomDataProvider; - let SyncTimelineEvent { event: raw_sync_event, encryption_info, .. } = - latest_event.event().clone(); + let raw_sync_event = latest_event.event().raw().clone(); + let encryption_info = latest_event.event().encryption_info().cloned(); let Ok(event) = raw_sync_event.deserialize_as::() else { warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!"); diff --git a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs index 88bc56d5c53..450ccae0e94 100644 --- a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs +++ b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs @@ -109,7 +109,7 @@ impl PinnedEventsLoader { // Sort using chronological ordering (oldest -> newest) loaded_events.sort_by_key(|item| { - item.event + item.raw() .deserialize() .map(|e| e.origin_server_ts()) .unwrap_or_else(|_| MilliSecondsSinceUnixEpoch::now()) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs index 3d102408c89..e6c59889891 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs @@ -19,6 +19,7 @@ use eyeball_im::VectorDiff; use matrix_sdk::deserialized_responses::{ AlgorithmInfo, EncryptionInfo, VerificationLevel, VerificationState, }; +use matrix_sdk_base::deserialized_responses::{DecryptedRoomEvent, SyncTimelineEvent}; use matrix_sdk_test::{async_test, ALICE}; use ruma::{ event_id, @@ -26,6 +27,7 @@ use ruma::{ room::message::{MessageType, RedactedRoomMessageEventContent}, BundledMessageLikeRelations, }, + room_id, }; use stream_assert::{assert_next_matches, assert_pending}; @@ -158,13 +160,15 @@ async fn test_edit_updates_encryption_info() { let timeline = TestTimeline::new(); let event_factory = &timeline.factory; + let room_id = room_id!("!room:id"); let original_event_id = event_id!("$original_event"); - let mut original_event = event_factory + let original_event = event_factory .text_msg("**original** message") .sender(*ALICE) .event_id(original_event_id) - .into_sync(); + .room(room_id) + .into_raw_timeline(); let mut encryption_info = EncryptionInfo { sender: (*ALICE).into(), @@ -176,7 +180,12 @@ async fn test_edit_updates_encryption_info() { verification_state: VerificationState::Verified, }; - original_event.encryption_info = Some(encryption_info.clone()); + let original_event: SyncTimelineEvent = DecryptedRoomEvent { + event: original_event.cast(), + encryption_info: encryption_info.clone(), + unsigned_encryption_info: None, + } + .into(); timeline.handle_live_event(original_event).await; @@ -192,14 +201,20 @@ async fn test_edit_updates_encryption_info() { assert_let!(MessageType::Text(text) = message.msgtype()); assert_eq!(text.body, "**original** message"); - let mut edit_event = event_factory + let edit_event = event_factory .text_msg(" * !!edited!! **better** message") .sender(*ALICE) + .room(room_id) .edit(original_event_id, MessageType::text_plain("!!edited!! **better** message").into()) - .into_sync(); + .into_raw_timeline(); encryption_info.verification_state = VerificationState::Unverified(VerificationLevel::UnverifiedIdentity); - edit_event.encryption_info = Some(encryption_info); + let edit_event: SyncTimelineEvent = DecryptedRoomEvent { + event: edit_event.cast(), + encryption_info: encryption_info.clone(), + unsigned_encryption_info: None, + } + .into(); timeline.handle_live_event(edit_event).await; diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 4b28daf873b..92619b03089 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2111,7 +2111,7 @@ impl Client { /// while let Some(Ok(response)) = sync_stream.next().await { /// for room in response.rooms.join.values() { /// for e in &room.timeline.events { - /// if let Ok(event) = e.event.deserialize() { + /// if let Ok(event) = e.raw().deserialize() { /// println!("Received event {:?}", event); /// } /// } diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 1de623c4b86..b7ff8f321f6 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -820,7 +820,7 @@ impl RoomEventCacheInner { event: &SyncTimelineEvent, ) { // Handle and cache events and relations. - if let Ok(AnySyncTimelineEvent::MessageLike(ev)) = event.event.deserialize() { + if let Ok(AnySyncTimelineEvent::MessageLike(ev)) = event.raw().deserialize() { // Handle redactions separately, as their logic is slightly different. if let AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(ev)) = &ev diff --git a/crates/matrix-sdk/src/event_handler/mod.rs b/crates/matrix-sdk/src/event_handler/mod.rs index c9513849947..c375619811d 100644 --- a/crates/matrix-sdk/src/event_handler/mod.rs +++ b/crates/matrix-sdk/src/event_handler/mod.rs @@ -388,7 +388,7 @@ impl Client { for item in timeline_events { let TimelineEventDetails { event_type, state_key, unsigned } = - item.event.deserialize_as()?; + item.raw().deserialize_as()?; let redacted = unsigned.and_then(|u| u.redacted_because).is_some(); let (handler_kind_g, handler_kind_r) = match state_key { @@ -396,8 +396,8 @@ impl Client { None => (HandlerKind::MessageLike, HandlerKind::message_like_redacted(redacted)), }; - let raw_event = item.event.json(); - let encryption_info = item.encryption_info.as_ref(); + let raw_event = item.raw().json(); + let encryption_info = item.encryption_info(); let push_actions = &item.push_actions; // Event handlers for possibly-redacted timeline events diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index 04b22b1c473..0ed178ff62d 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -142,7 +142,7 @@ async fn make_edit_event( ) -> Result { let target = source.get_event(event_id).await?; - let event = target.event.deserialize().map_err(EditError::Deserialize)?; + let event = target.raw().deserialize().map_err(EditError::Deserialize)?; // The event must be message-like. let AnySyncTimelineEvent::MessageLike(message_like_event) = event else { @@ -186,7 +186,7 @@ async fn make_edit_event( let replied_to_original_room_msg = replied_to_sync_timeline_event .and_then(|sync_timeline_event| { sync_timeline_event - .event + .raw() .deserialize() .map_err(|err| warn!("unable to deserialize replied-to event: {err}")) .ok() diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index fff27f60405..9de9a21afb7 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -2303,7 +2303,7 @@ mod tests { let no_local_events = room_id!("!crepe:example.org"); let already_limited = room_id!("!paris:example.org"); - let response_timeline = vec![event_c.event.clone(), event_d.event.clone()]; + let response_timeline = vec![event_c.raw().clone(), event_d.raw().clone()]; let local_rooms = BTreeMap::from_iter([ ( @@ -2386,21 +2386,21 @@ mod tests { no_overlap.to_owned(), assign!(http::response::Room::default(), { initial: Some(true), - timeline: vec![event_c.event.clone(), event_d.event.clone()], + timeline: vec![event_c.raw().clone(), event_d.raw().clone()], }), ), ( partial_overlap.to_owned(), assign!(http::response::Room::default(), { initial: Some(true), - timeline: vec![event_c.event.clone(), event_d.event.clone()], + timeline: vec![event_c.raw().clone(), event_d.raw().clone()], }), ), ( complete_overlap.to_owned(), assign!(http::response::Room::default(), { initial: Some(true), - timeline: vec![event_c.event.clone(), event_d.event.clone()], + timeline: vec![event_c.raw().clone(), event_d.raw().clone()], }), ), ( @@ -2414,7 +2414,7 @@ mod tests { no_local_events.to_owned(), assign!(http::response::Room::default(), { initial: Some(true), - timeline: vec![event_c.event.clone(), event_d.event.clone()], + timeline: vec![event_c.raw().clone(), event_d.raw().clone()], }), ), ( @@ -2422,7 +2422,7 @@ mod tests { assign!(http::response::Room::default(), { initial: Some(true), limited: true, - timeline: vec![event_c.event, event_d.event], + timeline: vec![event_c.into_raw(), event_d.into_raw()], }), ), ]); diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 93e0e4b17fb..f610911f359 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -363,7 +363,7 @@ mod tests { let timeline = & $( $timeline_queue ).*; $( - assert_eq!(timeline[ $nth ].event.deserialize().unwrap().event_id(), $event_id); + assert_eq!(timeline[ $nth ].raw().deserialize().unwrap().event_id(), $event_id); )* }; } @@ -714,7 +714,7 @@ mod tests { // Check that the last event is the last event of the timeline, i.e. we only // keep the _latest_ events, not the _first_ events. assert_eq!( - frozen_room.timeline_queue.last().unwrap().event.deserialize().unwrap().event_id(), + frozen_room.timeline_queue.last().unwrap().raw().deserialize().unwrap().event_id(), &format!("$x{max}:baz.org") ); } @@ -755,7 +755,7 @@ mod tests { // Check that the last event is the last event of the timeline, i.e. we only // keep the _latest_ events, not the _first_ events. assert_eq!( - frozen_room.timeline_queue.last().unwrap().event.deserialize().unwrap().event_id(), + frozen_room.timeline_queue.last().unwrap().raw().deserialize().unwrap().event_id(), &format!("$x{max}:baz.org") ); } diff --git a/crates/matrix-sdk/src/test_utils.rs b/crates/matrix-sdk/src/test_utils.rs index d73d6976ce1..f016503e85a 100644 --- a/crates/matrix-sdk/src/test_utils.rs +++ b/crates/matrix-sdk/src/test_utils.rs @@ -24,7 +24,7 @@ use crate::{ #[track_caller] pub fn assert_event_matches_msg>(event: &E, expected: &str) { let event: SyncTimelineEvent = event.clone().into(); - let event = event.event.deserialize().unwrap(); + let event = event.raw().deserialize().unwrap(); assert_let!( AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) = event ); diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 4a8f7938f7d..d370bc4b928 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -729,7 +729,7 @@ impl App { let rendered_events = events .into_iter() - .map(|sync_timeline_item| sync_timeline_item.event.json().to_string()) + .map(|sync_timeline_item| sync_timeline_item.raw().json().to_string()) .collect::>() .join("\n\n"); diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs index 6a178cd71d6..5712fa5dc3a 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/notification_client.rs @@ -154,7 +154,7 @@ async fn test_notification() -> Result<()> { .events .iter() .find_map(|event| { - let event = event.event.deserialize().ok()?; + let event = event.raw().deserialize().ok()?; (event.event_type() == TimelineEventType::RoomMessage) .then(|| event.event_id().to_owned()) }) From 07cfe3da949584a1b6aa9e8b415c1588ad2f46ff Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 2 Oct 2024 14:28:04 +0100 Subject: [PATCH 249/979] timeline: make `TimelineEvent` fields private ... and add accessors instead. Give `TimelineEvent` the same treatment we just gave `SyncTimelineEvent`: make the fields private, and use accessors where we previously used direct access. --- .../src/deserialized_responses.rs | 48 +++++++++++++++---- .../matrix-sdk-ui/src/notification_client.rs | 16 ++++--- .../src/timeline/controller/state.rs | 2 +- .../timeline/event_item/content/message.rs | 2 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 2 +- .../matrix-sdk-ui/tests/integration/main.rs | 10 ++-- .../integration/timeline/pinned_event.rs | 4 +- .../matrix-sdk/src/event_cache/paginator.rs | 8 ++-- crates/matrix-sdk/src/room/mod.rs | 6 +-- crates/matrix-sdk/src/widget/matrix.rs | 2 +- crates/matrix-sdk/tests/integration/client.rs | 2 +- .../tests/integration/encryption/backups.rs | 18 +++---- .../tests/integration/room/common.rs | 8 ++-- .../src/tests/e2ee.rs | 4 +- .../src/tests/nse.rs | 2 +- .../src/tests/room.rs | 6 +-- 16 files changed, 86 insertions(+), 54 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 7bf6be8f2f3..953659a3fb8 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -417,8 +417,8 @@ impl From for SyncTimelineEvent { // this way, we simply cause the `room_id` field in the json to be // ignored by a subsequent deserialization. Self { - inner_event: o.event.cast(), - inner_encryption_info: o.encryption_info, + inner_event: o.inner_event.cast(), + inner_encryption_info: o.inner_encryption_info, push_actions: o.push_actions.unwrap_or_default(), unsigned_encryption_info: o.unsigned_encryption_info, } @@ -445,10 +445,10 @@ impl From for SyncTimelineEvent { #[derive(Clone)] pub struct TimelineEvent { /// The actual event. - pub event: Raw, + inner_event: Raw, /// The encryption info about the event. Will be `None` if the event was not /// encrypted. - pub encryption_info: Option, + inner_encryption_info: Option, /// The push actions associated with this event, if we had sufficient /// context to compute them. pub push_actions: Option>, @@ -464,7 +464,30 @@ impl TimelineEvent { /// This is a convenience constructor for when you don't need to set /// `encryption_info` or `push_action`, for example inside a test. pub fn new(event: Raw) -> Self { - Self { event, encryption_info: None, push_actions: None, unsigned_encryption_info: None } + Self { + inner_event: event, + inner_encryption_info: None, + push_actions: None, + unsigned_encryption_info: None, + } + } + + /// Returns a reference to the (potentially decrypted) Matrix event inside + /// this `TimelineEvent`. + pub fn raw(&self) -> &Raw { + &self.inner_event + } + + /// If the event was a decrypted event that was successfully decrypted, get + /// its encryption info. Otherwise, `None`. + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + self.inner_encryption_info.as_ref() + } + + /// Takes ownership of this `TimelineEvent`, returning the (potentially + /// decrypted) Matrix event within. + pub fn into_raw(self) -> Raw { + self.inner_event } } @@ -474,8 +497,8 @@ impl From for TimelineEvent { // Casting from the more specific `AnyMessageLikeEvent` (i.e. an event without a // `state_key`) to a more generic `AnyTimelineEvent` (i.e. one that may contain // a `state_key`) is safe. - event: decrypted.event.cast(), - encryption_info: Some(decrypted.encryption_info), + inner_event: decrypted.event.cast(), + inner_encryption_info: Some(decrypted.encryption_info), push_actions: None, unsigned_encryption_info: decrypted.unsigned_encryption_info, } @@ -485,10 +508,15 @@ impl From for TimelineEvent { #[cfg(not(tarpaulin_include))] impl fmt::Debug for TimelineEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let TimelineEvent { event, encryption_info, push_actions, unsigned_encryption_info } = self; + let TimelineEvent { + inner_event, + inner_encryption_info, + push_actions, + unsigned_encryption_info, + } = self; let mut s = f.debug_struct("TimelineEvent"); - s.field("event", &DebugRawEvent(event)); - s.maybe_field("encryption_info", encryption_info); + s.field("event", &DebugRawEvent(inner_event)); + s.maybe_field("encryption_info", inner_encryption_info); if let Some(push_actions) = &push_actions { if !push_actions.is_empty() { s.field("push_actions", push_actions); diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index f4774ec23f5..df15519a77b 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -496,9 +496,12 @@ impl NotificationClient { let push_actions = match &raw_event { RawNotificationEvent::Timeline(timeline_event) => { // Timeline events may be encrypted, so make sure they get decrypted first. - if let Some(timeline_event) = self.retry_decryption(&room, timeline_event).await? { - raw_event = RawNotificationEvent::Timeline(timeline_event.event.cast()); - timeline_event.push_actions + if let Some(mut timeline_event) = + self.retry_decryption(&room, timeline_event).await? + { + let push_actions = timeline_event.push_actions.take(); + raw_event = RawNotificationEvent::Timeline(timeline_event.into_raw().cast()); + push_actions } else { room.event_push_actions(timeline_event).await? } @@ -550,7 +553,7 @@ impl NotificationClient { let state_events = response.state; if let Some(decrypted_event) = - self.retry_decryption(&room, timeline_event.event.cast_ref()).await? + self.retry_decryption(&room, timeline_event.raw().cast_ref()).await? { timeline_event = decrypted_event; } @@ -561,11 +564,12 @@ impl NotificationClient { } } + let push_actions = timeline_event.push_actions.take(); Ok(Some( NotificationItem::new( &room, - RawNotificationEvent::Timeline(timeline_event.event.cast()), - timeline_event.push_actions.as_deref(), + RawNotificationEvent::Timeline(timeline_event.into_raw().cast()), + push_actions.as_deref(), state_events, ) .await?, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index e13b7aec076..41f4f163b16 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -234,7 +234,7 @@ impl TimelineState { }; event.push_actions = push_rules_context.as_ref().map(|(push_rules, push_context)| { - push_rules.get_actions(&event.event, push_context).to_owned() + push_rules.get_actions(event.raw(), push_context).to_owned() }); let handle_one_res = txn diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index df1d3d160dd..ce9758a833e 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -351,7 +351,7 @@ impl RepliedToEvent { timeline_event: TimelineEvent, room_data_provider: &P, ) -> Result { - let event = match timeline_event.event.deserialize() { + let event = match timeline_event.raw().deserialize() { Ok(AnyTimelineEvent::MessageLike(event)) => event, _ => { return Err(TimelineError::UnsupportedEvent); diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 433611c44e4..4cadaf69ccb 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -417,7 +417,7 @@ impl Timeline { // `AnySyncTimelineEvent` which is the same as a `AnyTimelineEvent`, but without // the `room_id` field. The cast is valid because we are just losing // track of such field. - let raw_sync_event: Raw = event.event.cast(); + let raw_sync_event: Raw = event.into_raw().cast(); let sync_event = raw_sync_event.deserialize().map_err(|error| { error!("Failed to deserialize event with ID {event_id} with error: {error}"); UnsupportedReplyItem::FailedToDeserializeEvent diff --git a/crates/matrix-sdk-ui/tests/integration/main.rs b/crates/matrix-sdk-ui/tests/integration/main.rs index 5ea771c88c4..da8bc38c3ee 100644 --- a/crates/matrix-sdk-ui/tests/integration/main.rs +++ b/crates/matrix-sdk-ui/tests/integration/main.rs @@ -71,9 +71,9 @@ async fn mock_context( .and(path(format!("/_matrix/client/r0/rooms/{room_id}/context/{event_id}"))) .and(header("authorization", "Bearer 1234")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "events_before": events_before.into_iter().rev().map(|ev| ev.event).collect_vec(), - "event": event.event, - "events_after": events_after.into_iter().map(|ev| ev.event).collect_vec(), + "events_before": events_before.into_iter().rev().map(|ev| ev.into_raw()).collect_vec(), + "event": event.into_raw(), + "events_after": events_after.into_iter().map(|ev| ev.into_raw()).collect_vec(), "state": state, "start": prev_batch_token, "end": next_batch_token @@ -93,7 +93,7 @@ async fn mock_event( Mock::given(method("GET")) .and(path(format!("/_matrix/client/r0/rooms/{room_id}/event/{event_id}"))) .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(event.event.json())) + .respond_with(ResponseTemplate::new(200).set_body_json(event.into_raw().json())) .mount(server) .await; } @@ -116,7 +116,7 @@ async fn mock_messages( .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "start": start, "end": end, - "chunk": chunk.into_iter().map(|ev| ev.event).collect_vec(), + "chunk": chunk.into_iter().map(|ev| ev.into_raw()).collect_vec(), "state": state, }))) .expect(1) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index 269e975b2ce..594263454c9 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -720,7 +720,7 @@ impl TestHelper { ) -> Result { let mut joined_room_builder = JoinedRoomBuilder::new(&self.room_id); for (timeline_event, add_to_timeline) in text_messages { - let deserialized_event = timeline_event.event.deserialize()?; + let deserialized_event = timeline_event.raw().deserialize()?; mock_event( &self.server, &self.room_id, @@ -731,7 +731,7 @@ impl TestHelper { if add_to_timeline { joined_room_builder = - joined_room_builder.add_timeline_event(timeline_event.event.cast()); + joined_room_builder.add_timeline_event(timeline_event.into_raw().cast()); } } diff --git a/crates/matrix-sdk/src/event_cache/paginator.rs b/crates/matrix-sdk/src/event_cache/paginator.rs index df240325a5d..cbeb21268f0 100644 --- a/crates/matrix-sdk/src/event_cache/paginator.rs +++ b/crates/matrix-sdk/src/event_cache/paginator.rs @@ -736,7 +736,7 @@ mod tests { } assert_event_matches_msg(&context.events[10], "fetch_from"); - assert_eq!(context.events[10].event.deserialize().unwrap().event_id(), event_id); + assert_eq!(context.events[10].raw().deserialize().unwrap().event_id(), event_id); for i in 0..10 { assert_event_matches_msg(&context.events[i + 11], &format!("after-{i}")); @@ -800,7 +800,7 @@ mod tests { // And I get the events I expected. assert_eq!(context.events.len(), 1); assert_event_matches_msg(&context.events[0], "initial"); - assert_eq!(context.events[0].event.deserialize().unwrap().event_id(), event_id); + assert_eq!(context.events[0].raw().deserialize().unwrap().event_id(), event_id); // There's a previous batch, but no next batch. assert!(context.has_prev); @@ -865,7 +865,7 @@ mod tests { // And I get the events I expected. assert_eq!(context.events.len(), 1); assert_event_matches_msg(&context.events[0], "initial"); - assert_eq!(context.events[0].event.deserialize().unwrap().event_id(), event_id); + assert_eq!(context.events[0].raw().deserialize().unwrap().event_id(), event_id); // There's a previous batch. assert!(context.has_prev); @@ -915,7 +915,7 @@ mod tests { // And I get the events I expected. assert_eq!(context.events.len(), 1); assert_event_matches_msg(&context.events[0], "initial"); - assert_eq!(context.events[0].event.deserialize().unwrap().event_id(), event_id); + assert_eq!(context.events[0].raw().deserialize().unwrap().event_id(), event_id); // There's a next batch, but no previous batch (i.e. we've hit the start of the // timeline). diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 09479921ad6..1a5287abd98 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -315,7 +315,7 @@ impl Room { for event in &mut response.chunk { event.push_actions = - Some(push_rules.get_actions(&event.event, &push_context).to_owned()); + Some(push_rules.get_actions(event.raw(), &push_context).to_owned()); } } @@ -420,7 +420,7 @@ impl Room { } let mut event = TimelineEvent::new(event); - event.push_actions = self.event_push_actions(&event.event).await?; + event.push_actions = self.event_push_actions(event.raw()).await?; Ok(event) } @@ -1238,7 +1238,7 @@ impl Room { }; let mut event: TimelineEvent = decrypted.into(); - event.push_actions = self.event_push_actions(&event.event).await?; + event.push_actions = self.event_push_actions(event.raw()).await?; Ok(event) } diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index b0d09b18ab7..96a40d423aa 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -73,7 +73,7 @@ impl MatrixDriver { }); let messages = self.room.messages(options).await?; - Ok(messages.chunk.into_iter().map(|ev| ev.event.cast()).collect()) + Ok(messages.chunk.into_iter().map(|ev| ev.into_raw().cast()).collect()) } pub(crate) async fn read_state_events( diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index ac8158c1db0..d8eb2f850f4 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -750,7 +750,7 @@ async fn test_encrypt_room_event() { .expect("We should be able to decrypt an event that we ourselves have encrypted"); let event = timeline_event - .event + .raw() .deserialize() .expect("We should be able to deserialize the decrypted event"); diff --git a/crates/matrix-sdk/tests/integration/encryption/backups.rs b/crates/matrix-sdk/tests/integration/encryption/backups.rs index f510d40b706..26244204a7c 100644 --- a/crates/matrix-sdk/tests/integration/encryption/backups.rs +++ b/crates/matrix-sdk/tests/integration/encryption/backups.rs @@ -972,7 +972,7 @@ async fn test_enable_from_secret_storage() { room.event(event_id, None).await.expect("We should be able to fetch our encrypted event"); assert_matches!( - event.encryption_info, + event.encryption_info(), None, "We should not be able to decrypt our encrypted event before we import the room keys from \ the backup" @@ -1042,9 +1042,9 @@ async fn test_enable_from_secret_storage() { let event = room.event(event_id, None).await.expect("We should be able to fetch our encrypted event"); - assert_matches!(event.encryption_info, Some(..), "The event should now be decrypted"); + assert_matches!(event.encryption_info(), Some(..), "The event should now be decrypted"); let event: RoomMessageEvent = - event.event.deserialize_as().expect("We should be able to deserialize the event"); + event.raw().deserialize_as().expect("We should be able to deserialize the event"); let event = event.as_original().unwrap(); assert_eq!(event.content.body(), "tt"); @@ -1392,7 +1392,7 @@ async fn test_enable_from_secret_storage_and_download_after_utd() { room.event(event_id, None).await.expect("We should be able to fetch our encrypted event"); assert_matches!( - event.encryption_info, + event.encryption_info(), None, "We should not be able to decrypt the event right away" ); @@ -1412,9 +1412,9 @@ async fn test_enable_from_secret_storage_and_download_after_utd() { let event = room.event(event_id, None).await.expect("We should be able to fetch our encrypted event"); - assert_matches!(event.encryption_info, Some(..), "The event should now be decrypted"); + assert_matches!(event.encryption_info(), Some(..), "The event should now be decrypted"); let event: RoomMessageEvent = - event.event.deserialize_as().expect("We should be able to deserialize the event"); + event.raw().deserialize_as().expect("We should be able to deserialize the event"); let event = event.as_original().unwrap(); assert_eq!(event.content.body(), "tt"); @@ -1522,7 +1522,7 @@ async fn test_enable_from_secret_storage_and_download_after_utd_from_old_message room.event(event_id, None).await.expect("We should be able to fetch our encrypted event"); assert_matches!( - event.encryption_info, + event.encryption_info(), None, "We should not be able to decrypt the event right away" ); @@ -1542,9 +1542,9 @@ async fn test_enable_from_secret_storage_and_download_after_utd_from_old_message let event = room.event(event_id, None).await.expect("We should be able to fetch our encrypted event"); - assert_matches!(event.encryption_info, Some(..), "The event should now be decrypted"); + assert_matches!(event.encryption_info(), Some(..), "The event should now be decrypted"); let event: RoomMessageEvent = - event.event.deserialize_as().expect("We should be able to deserialize the event"); + event.raw().deserialize_as().expect("We should be able to deserialize the event"); let event = event.as_original().unwrap(); assert_eq!(event.content.body(), "tt"); diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index c8096c1099e..1403b96f3ad 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -672,7 +672,7 @@ async fn test_event() { let timeline_event = room.event(event_id, None).await.unwrap(); assert_let!( AnyTimelineEvent::State(AnyStateEvent::RoomTombstone(event)) = - timeline_event.event.deserialize().unwrap() + timeline_event.raw().deserialize().unwrap() ); assert_eq!(event.event_id(), event_id); @@ -734,15 +734,15 @@ async fn test_event_with_context() { let context_ret = room.event_with_context(event_id, false, uint!(1), None).await.unwrap(); assert_let!(Some(timeline_event) = context_ret.event); - assert_let!(Ok(event) = timeline_event.event.deserialize()); + assert_let!(Ok(event) = timeline_event.raw().deserialize()); assert_eq!(event.event_id(), event_id); assert_eq!(1, context_ret.events_before.len()); - assert_let!(Ok(event) = context_ret.events_before[0].event.deserialize()); + assert_let!(Ok(event) = context_ret.events_before[0].raw().deserialize()); assert_eq!(event.event_id(), prev_event_id); assert_eq!(1, context_ret.events_after.len()); - assert_let!(Ok(event) = context_ret.events_after[0].event.deserialize()); + assert_let!(Ok(event) = context_ret.events_after[0].raw().deserialize()); assert_eq!(event.event_id(), next_event_id); // Requested event and their context ones were saved to the cache diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs index 746bbb203c7..233dbc49e7d 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs @@ -950,11 +950,11 @@ async fn test_secret_gossip_after_interactive_verification() -> Result<()> { let timeline_event = room.event(&event_id, None).await?; timeline_event - .encryption_info + .encryption_info() .expect("The event should have been encrypted and successfully decrypted."); let event: OriginalSyncMessageLikeEvent = - timeline_event.event.deserialize_as()?; + timeline_event.raw().deserialize_as()?; let message = event.content.msgtype; assert_let!(MessageType::Text(message) = message); diff --git a/testing/matrix-sdk-integration-testing/src/tests/nse.rs b/testing/matrix-sdk-integration-testing/src/tests/nse.rs index 4487814c297..0bdac9841ae 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/nse.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/nse.rs @@ -423,7 +423,7 @@ async fn decrypt_event( return None; }; - let Ok(deserialized) = decrypted.event.deserialize() else { return None }; + let Ok(deserialized) = decrypted.raw().deserialize() else { return None }; let AnyTimelineEvent::MessageLike(message) = &deserialized else { return None }; diff --git a/testing/matrix-sdk-integration-testing/src/tests/room.rs b/testing/matrix-sdk-integration-testing/src/tests/room.rs index 811e1729820..b661198e1e5 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/room.rs @@ -99,7 +99,7 @@ async fn test_event_with_context() -> Result<()> { let target = response .event .expect("there should be an event") - .event + .raw() .deserialize() .expect("it should be deserializable"); assert_eq!(target.event_id(), &event_id); @@ -129,7 +129,7 @@ async fn test_event_with_context() -> Result<()> { let target = response .event .expect("there should be an event") - .event + .raw() .deserialize() .expect("it should be deserializable"); assert_eq!(target.event_id(), &event_id); @@ -179,7 +179,7 @@ async fn test_event_with_context() -> Result<()> { assert_event_matches_msg(&prev_events[8], "0"); // Last event is the m.room.encryption event. - let event = prev_events[9].event.deserialize().unwrap(); + let event = prev_events[9].raw().deserialize().unwrap(); assert_matches!(event, AnyTimelineEvent::State(AnyStateEvent::RoomEncryption(_))); // There are other events before that (room creation, alice joining). From 8fe61e1fb3069d194f9f56b0fdbc1c7894f44ddf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 9 Oct 2024 14:47:18 +0100 Subject: [PATCH 250/979] timeline: make `TimelineEvent::raw` return a `Raw` I'm going to be replacing the inner structure of `TimelineEvent` with an implementation that holds a `Raw`, rather than a `Raw`. Prepare for that by changing the accessors to return `Raw`. --- crates/matrix-sdk-common/src/deserialized_responses.rs | 5 +++-- crates/matrix-sdk-ui/src/notification_client.rs | 4 +--- .../src/timeline/event_item/content/message.rs | 4 ++-- crates/matrix-sdk/tests/integration/client.rs | 4 ++-- crates/matrix-sdk/tests/integration/room/common.rs | 4 ++-- testing/matrix-sdk-integration-testing/src/tests/nse.rs | 5 ++--- testing/matrix-sdk-integration-testing/src/tests/room.rs | 6 +++--- 7 files changed, 15 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 953659a3fb8..aa7d3f2145a 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -474,8 +474,9 @@ impl TimelineEvent { /// Returns a reference to the (potentially decrypted) Matrix event inside /// this `TimelineEvent`. - pub fn raw(&self) -> &Raw { - &self.inner_event + pub fn raw(&self) -> &Raw { + // TODO: make `inner_event` an AnySyncTimelineEvent instead. + self.inner_event.cast_ref() } /// If the event was a decrypted event that was successfully decrypted, get diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index df15519a77b..64e43606f58 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -552,9 +552,7 @@ impl NotificationClient { let mut timeline_event = response.event.ok_or(Error::ContextMissingEvent)?; let state_events = response.state; - if let Some(decrypted_event) = - self.retry_decryption(&room, timeline_event.raw().cast_ref()).await? - { + if let Some(decrypted_event) = self.retry_decryption(&room, timeline_event.raw()).await? { timeline_event = decrypted_event; } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index ce9758a833e..1c232e9a52d 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -31,7 +31,7 @@ use ruma::{ SyncRoomMessageEvent, }, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, - AnyTimelineEvent, BundledMessageLikeRelations, Mentions, + BundledMessageLikeRelations, Mentions, }, html::RemoveReplyFallback, serde::Raw, @@ -352,7 +352,7 @@ impl RepliedToEvent { room_data_provider: &P, ) -> Result { let event = match timeline_event.raw().deserialize() { - Ok(AnyTimelineEvent::MessageLike(event)) => event, + Ok(AnySyncTimelineEvent::MessageLike(event)) => event, _ => { return Err(TimelineError::UnsupportedEvent); } diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index d8eb2f850f4..3dd739d751f 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -755,8 +755,8 @@ async fn test_encrypt_room_event() { .expect("We should be able to deserialize the decrypted event"); assert_let!( - ruma::events::AnyTimelineEvent::MessageLike( - ruma::events::AnyMessageLikeEvent::RoomMessage(message_event) + ruma::events::AnySyncTimelineEvent::MessageLike( + ruma::events::AnySyncMessageLikeEvent::RoomMessage(message_event) ) = event ); diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index 1403b96f3ad..f742ee3bfe4 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -19,7 +19,7 @@ use ruma::{ member::MembershipState, message::RoomMessageEventContent, }, - AnyStateEvent, AnySyncStateEvent, AnyTimelineEvent, StateEventType, + AnySyncStateEvent, AnySyncTimelineEvent, StateEventType, }, mxc_uri, room_id, }; @@ -671,7 +671,7 @@ async fn test_event() { let timeline_event = room.event(event_id, None).await.unwrap(); assert_let!( - AnyTimelineEvent::State(AnyStateEvent::RoomTombstone(event)) = + AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTombstone(event)) = timeline_event.raw().deserialize().unwrap() ); assert_eq!(event.event_id(), event_id); diff --git a/testing/matrix-sdk-integration-testing/src/tests/nse.rs b/testing/matrix-sdk-integration-testing/src/tests/nse.rs index 0bdac9841ae..2bbb1bd1201 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/nse.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/nse.rs @@ -18,8 +18,7 @@ use matrix_sdk::{ message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent}, }, room_key::ToDeviceRoomKeyEvent, - AnyMessageLikeEventContent, AnySyncTimelineEvent, AnyTimelineEvent, - OriginalSyncMessageLikeEvent, + AnyMessageLikeEventContent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent, }, serde::Raw, EventEncryptionAlgorithm, OwnedEventId, OwnedRoomId, RoomId, @@ -425,7 +424,7 @@ async fn decrypt_event( let Ok(deserialized) = decrypted.raw().deserialize() else { return None }; - let AnyTimelineEvent::MessageLike(message) = &deserialized else { return None }; + let AnySyncTimelineEvent::MessageLike(message) = &deserialized else { return None }; let Some(AnyMessageLikeEventContent::RoomMessage(content)) = message.original_content() else { return None; diff --git a/testing/matrix-sdk-integration-testing/src/tests/room.rs b/testing/matrix-sdk-integration-testing/src/tests/room.rs index b661198e1e5..d9f9575bde5 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/room.rs @@ -8,8 +8,8 @@ use matrix_sdk::{ api::client::room::create_room::v3::Request as CreateRoomRequest, assign, event_id, events, events::{ - room::message::RoomMessageEventContent, AnyRoomAccountDataEventContent, AnyStateEvent, - AnyTimelineEvent, EventContent, + room::message::RoomMessageEventContent, AnyRoomAccountDataEventContent, + AnySyncStateEvent, AnySyncTimelineEvent, EventContent, }, serde::Raw, uint, @@ -180,7 +180,7 @@ async fn test_event_with_context() -> Result<()> { // Last event is the m.room.encryption event. let event = prev_events[9].raw().deserialize().unwrap(); - assert_matches!(event, AnyTimelineEvent::State(AnyStateEvent::RoomEncryption(_))); + assert_matches!(event, AnySyncTimelineEvent::State(AnySyncStateEvent::RoomEncryption(_))); // There are other events before that (room creation, alice joining). assert!(prev_messages.end.is_some()); From 7f0a3f0e471ac6af402d424890cebec2ac8bff9e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 9 Oct 2024 14:56:21 +0100 Subject: [PATCH 251/979] timeline: make `TimelineEvent::into_raw` return a `Raw` Give `Timeline::into_raw()` the same treatmeant we just gave `Timeline::ra()`. --- crates/matrix-sdk-common/src/deserialized_responses.rs | 5 +++-- crates/matrix-sdk-ui/src/notification_client.rs | 4 ++-- crates/matrix-sdk-ui/src/timeline/mod.rs | 6 +----- .../tests/integration/timeline/pinned_event.rs | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index aa7d3f2145a..8c46ef014f0 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -487,8 +487,9 @@ impl TimelineEvent { /// Takes ownership of this `TimelineEvent`, returning the (potentially /// decrypted) Matrix event within. - pub fn into_raw(self) -> Raw { - self.inner_event + pub fn into_raw(self) -> Raw { + // TODO: make `inner_event` an AnySyncTimelineEvent instead. + self.inner_event.cast() } } diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 64e43606f58..cc306440f2d 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -500,7 +500,7 @@ impl NotificationClient { self.retry_decryption(&room, timeline_event).await? { let push_actions = timeline_event.push_actions.take(); - raw_event = RawNotificationEvent::Timeline(timeline_event.into_raw().cast()); + raw_event = RawNotificationEvent::Timeline(timeline_event.into_raw()); push_actions } else { room.event_push_actions(timeline_event).await? @@ -566,7 +566,7 @@ impl NotificationClient { Ok(Some( NotificationItem::new( &room, - RawNotificationEvent::Timeline(timeline_event.into_raw().cast()), + RawNotificationEvent::Timeline(timeline_event.into_raw()), push_actions.as_deref(), state_events, ) diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 4cadaf69ccb..0dc57b1e936 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -413,11 +413,7 @@ impl Timeline { UnsupportedReplyItem::MissingEvent })?; - // We need to get the content and we can do that by casting the event as a - // `AnySyncTimelineEvent` which is the same as a `AnyTimelineEvent`, but without - // the `room_id` field. The cast is valid because we are just losing - // track of such field. - let raw_sync_event: Raw = event.into_raw().cast(); + let raw_sync_event = event.into_raw(); let sync_event = raw_sync_event.deserialize().map_err(|error| { error!("Failed to deserialize event with ID {event_id} with error: {error}"); UnsupportedReplyItem::FailedToDeserializeEvent diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index 594263454c9..b67661a73d0 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -731,7 +731,7 @@ impl TestHelper { if add_to_timeline { joined_room_builder = - joined_room_builder.add_timeline_event(timeline_event.into_raw().cast()); + joined_room_builder.add_timeline_event(timeline_event.into_raw()); } } From d9167f208a62ba460d85eba165e6384ff03cc2e0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 2 Oct 2024 15:13:21 +0100 Subject: [PATCH 252/979] timeline: Extract inner parts of `[Sync]TimelineEvent` Pull out the bits of these classes which are dependent on success or otherwise of decrypting an event to a new enum. --- crates/matrix-sdk-base/src/latest_event.rs | 26 +- crates/matrix-sdk-base/src/rooms/normal.rs | 5 +- .../src/deserialized_responses.rs | 420 ++++++++++++------ crates/matrix-sdk/src/sliding_sync/room.rs | 5 +- 4 files changed, 319 insertions(+), 137 deletions(-) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 0799a296bc8..ecdd490dccf 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -561,9 +561,12 @@ mod tests { json!({ "latest_event": { "event": { - "encryption_info": null, - "event": { - "event_id": "$1" + "kind": { + "PlainText": { + "event": { + "event_id": "$1" + } + } } }, } @@ -577,6 +580,23 @@ mod tests { assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none()); // The previous format can also be deserialized. + let serialized = json!({ + "latest_event": { + "event": { + "encryption_info": null, + "event": { + "event_id": "$1" + } + }, + } + }); + + let deserialized: TestStruct = serde_json::from_value(serialized).unwrap(); + assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1"); + assert!(deserialized.latest_event.sender_profile.is_none()); + assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none()); + + // The even older format can also be deserialized. let serialized = json!({ "latest_event": event }); diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index cb3f4bdbd92..95eead8bc7e 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1908,10 +1908,7 @@ mod tests { "encryption_state_synced": true, "latest_event": { "event": { - "encryption_info": null, - "event": { - "sender": "@u:i.uk", - }, + "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}}, }, }, "base_info": { diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 8c46ef014f0..5cd859cab67 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -300,106 +300,79 @@ pub struct EncryptionInfo { /// Represents a matrix room event that has been returned from `/sync`, /// after initial processing. /// -/// This is almost identical to [`TimelineEvent`], but wraps an -/// [`AnySyncTimelineEvent`] instead of [`AnyTimelineEvent`]. -#[derive(Clone, Deserialize, Serialize)] +/// Previously, this differed from [`TimelineEvent`] by wrapping an +/// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays +/// they are essentially identical, and one of them should probably be removed. +#[derive(Clone, Serialize)] pub struct SyncTimelineEvent { - /// The actual event. - #[serde(rename = "event")] - inner_event: Raw, - - /// The encryption info about the event. Will be `None` if the event was not - /// encrypted. - #[serde(rename = "encryption_info")] - inner_encryption_info: Option, + /// The event itself, together with any information on decryption. + pub kind: TimelineEventKind, /// The push actions associated with this event. - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(skip_serializing_if = "Vec::is_empty")] pub push_actions: Vec, - /// The encryption info about the events bundled in the `unsigned` object. - /// - /// Will be `None` if no bundled event was encrypted. - #[serde(skip_serializing_if = "Option::is_none")] - pub unsigned_encryption_info: Option>, } impl SyncTimelineEvent { /// Create a new `SyncTimelineEvent` from the given raw event. /// - /// This is a convenience constructor for when you don't need to set - /// `encryption_info` or `push_action`, for example inside a test. + /// This is a convenience constructor for a plaintext event when you don't + /// need to set `push_action`, for example inside a test. pub fn new(event: Raw) -> Self { - Self { - inner_event: event, - inner_encryption_info: None, - push_actions: vec![], - unsigned_encryption_info: None, - } + Self { kind: TimelineEventKind::PlainText { event }, push_actions: vec![] } } /// Create a new `SyncTimelineEvent` from the given raw event and push /// actions. /// - /// This is a convenience constructor for when you don't need to set - /// `encryption_info`, for example inside a test. + /// This is a convenience constructor for a plaintext event, for example + /// inside a test. pub fn new_with_push_actions( event: Raw, push_actions: Vec, ) -> Self { - Self { - inner_event: event, - inner_encryption_info: None, - push_actions, - unsigned_encryption_info: None, - } + Self { kind: TimelineEventKind::PlainText { event }, push_actions } } /// Get the event id of this `SyncTimelineEvent` if the event has any valid /// id. pub fn event_id(&self) -> Option { - self.inner_event.get_field::("event_id").ok().flatten() + self.kind.raw().get_field::("event_id").ok().flatten() } /// Returns a reference to the (potentially decrypted) Matrix event inside /// this `TimelineEvent`. pub fn raw(&self) -> &Raw { - &self.inner_event + self.kind.raw() } /// If the event was a decrypted event that was successfully decrypted, get /// its encryption info. Otherwise, `None`. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { - self.inner_encryption_info.as_ref() + self.kind.encryption_info() } /// Takes ownership of this `TimelineEvent`, returning the (potentially /// decrypted) Matrix event within. pub fn into_raw(self) -> Raw { - self.inner_event + self.kind.into_raw() } /// Replace the Matrix event within this event. Used to handle redaction. pub fn set_raw(&mut self, event: Raw) { - self.inner_event = event; + self.kind = TimelineEventKind::PlainText { event }; } } #[cfg(not(tarpaulin_include))] impl fmt::Debug for SyncTimelineEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let SyncTimelineEvent { - inner_event, - inner_encryption_info, - push_actions, - unsigned_encryption_info, - } = self; + let SyncTimelineEvent { kind, push_actions } = self; let mut s = f.debug_struct("SyncTimelineEvent"); - s.field("event", &DebugRawEvent(inner_event)); - s.maybe_field("encryption_info", inner_encryption_info); + s.field("kind", &kind); if !push_actions.is_empty() { s.field("push_actions", push_actions); } - s.maybe_field("unsigned_encryption_info", unsigned_encryption_info); s.finish() } } @@ -412,16 +385,7 @@ impl From> for SyncTimelineEvent { impl From for SyncTimelineEvent { fn from(o: TimelineEvent) -> Self { - // This conversion is unproblematic since a `SyncTimelineEvent` is just a - // `TimelineEvent` without the `room_id`. By converting the raw value in - // this way, we simply cause the `room_id` field in the json to be - // ignored by a subsequent deserialization. - Self { - inner_event: o.inner_event.cast(), - inner_encryption_info: o.inner_encryption_info, - push_actions: o.push_actions.unwrap_or_default(), - unsigned_encryption_info: o.unsigned_encryption_info, - } + Self { kind: o.kind, push_actions: o.push_actions.unwrap_or_default() } } } @@ -432,6 +396,48 @@ impl From for SyncTimelineEvent { } } +impl<'de> Deserialize<'de> for SyncTimelineEvent { + /// Custom deserializer for [`SyncTimelineEvent`], to support older formats. + /// + /// Ideally we might use an untagged enum and then convert from that; + /// however, that doesn't work due to a [serde bug](https://github.com/serde-rs/json/issues/497). + /// + /// Instead, we first deserialize into an unstructured JSON map, and then + /// inspect the json to figure out which format we have. + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde_json::{Map, Value}; + + // First, deserialize to an unstructured JSON map + let value = Map::::deserialize(deserializer)?; + + // If we have a top-level `event`, it's V0 + if value.contains_key("event") { + let v0: SyncTimelineEventDeserializationHelperV0 = + serde_json::from_value(Value::Object(value)).map_err(|e| { + serde::de::Error::custom(format!( + "Unable to deserialize V0-format SyncTimelineEvent: {}", + e + )) + })?; + Ok(v0.into()) + } + // Otherwise, it's V1 + else { + let v1: SyncTimelineEventDeserializationHelperV1 = + serde_json::from_value(Value::Object(value)).map_err(|e| { + serde::de::Error::custom(format!( + "Unable to deserialize V1-format SyncTimelineEvent: {}", + e + )) + })?; + Ok(v1.into()) + } + } +} + /// Represents a matrix room event that has been returned from a Matrix /// client-server API endpoint such as `/messages`, after initial processing. /// @@ -439,96 +445,146 @@ impl From for SyncTimelineEvent { /// the main thing this adds over [`AnyTimelineEvent`] is information on /// encryption. /// -/// See also [`SyncTimelineEvent`] which is almost identical, but is used for -/// results from the `/sync` endpoint (which lack a `room_id` property) and -/// hence wraps an [`AnySyncTimelineEvent`] instead of [`AnyTimelineEvent`]. +/// Previously, this differed from [`SyncTimelineEvent`] by wrapping an +/// [`AnyTimelineEvent`] instead of an [`AnySyncTimelineEvent`], but nowadays +/// they are essentially identical, and one of them should probably be removed. #[derive(Clone)] pub struct TimelineEvent { - /// The actual event. - inner_event: Raw, - /// The encryption info about the event. Will be `None` if the event was not - /// encrypted. - inner_encryption_info: Option, + /// The event itself, together with any information on decryption. + pub kind: TimelineEventKind, + /// The push actions associated with this event, if we had sufficient /// context to compute them. pub push_actions: Option>, - /// The encryption info about the events bundled in the `unsigned` object. - /// - /// Will be `None` if no bundled event was encrypted. - pub unsigned_encryption_info: Option>, } impl TimelineEvent { /// Create a new `TimelineEvent` from the given raw event. /// - /// This is a convenience constructor for when you don't need to set - /// `encryption_info` or `push_action`, for example inside a test. + /// This is a convenience constructor for a plaintext event when you don't + /// need to set `push_action`, for example inside a test. pub fn new(event: Raw) -> Self { Self { - inner_event: event, - inner_encryption_info: None, + // This conversion is unproblematic since a `SyncTimelineEvent` is just a + // `TimelineEvent` without the `room_id`. By converting the raw value in + // this way, we simply cause the `room_id` field in the json to be + // ignored by a subsequent deserialization. + kind: TimelineEventKind::PlainText { event: event.cast() }, push_actions: None, - unsigned_encryption_info: None, } } /// Returns a reference to the (potentially decrypted) Matrix event inside /// this `TimelineEvent`. pub fn raw(&self) -> &Raw { - // TODO: make `inner_event` an AnySyncTimelineEvent instead. - self.inner_event.cast_ref() + self.kind.raw() } /// If the event was a decrypted event that was successfully decrypted, get /// its encryption info. Otherwise, `None`. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { - self.inner_encryption_info.as_ref() + self.kind.encryption_info() } /// Takes ownership of this `TimelineEvent`, returning the (potentially /// decrypted) Matrix event within. pub fn into_raw(self) -> Raw { - // TODO: make `inner_event` an AnySyncTimelineEvent instead. - self.inner_event.cast() + self.kind.into_raw() } } impl From for TimelineEvent { fn from(decrypted: DecryptedRoomEvent) -> Self { - Self { - // Casting from the more specific `AnyMessageLikeEvent` (i.e. an event without a - // `state_key`) to a more generic `AnyTimelineEvent` (i.e. one that may contain - // a `state_key`) is safe. - inner_event: decrypted.event.cast(), - inner_encryption_info: Some(decrypted.encryption_info), - push_actions: None, - unsigned_encryption_info: decrypted.unsigned_encryption_info, - } + Self { kind: TimelineEventKind::Decrypted(decrypted), push_actions: None } } } #[cfg(not(tarpaulin_include))] impl fmt::Debug for TimelineEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let TimelineEvent { - inner_event, - inner_encryption_info, - push_actions, - unsigned_encryption_info, - } = self; + let TimelineEvent { kind, push_actions } = self; let mut s = f.debug_struct("TimelineEvent"); - s.field("event", &DebugRawEvent(inner_event)); - s.maybe_field("encryption_info", inner_encryption_info); + s.field("kind", &kind); if let Some(push_actions) = &push_actions { if !push_actions.is_empty() { s.field("push_actions", push_actions); } } - s.maybe_field("unsigned_encryption_info", unsigned_encryption_info); s.finish() } } +/// The event within a [`TimelineEvent`] or [`SyncTimelineEvent`], together with +/// encryption data. +#[derive(Clone, Serialize, Deserialize)] +pub enum TimelineEventKind { + /// A successfully-decrypted encrypted event. + Decrypted(DecryptedRoomEvent), + + /// An unencrypted event. + PlainText { + /// The actual event. Depending on the source of the event, it could + /// actually be a [`AnyTimelineEvent`] (which differs from + /// [`AnySyncTimelineEvent`] by the addition of a `room_id` property). + event: Raw, + }, +} + +impl TimelineEventKind { + /// Returns a reference to the (potentially decrypted) Matrix event inside + /// this `TimelineEvent`. + pub fn raw(&self) -> &Raw { + match self { + // It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does + // *not* contain a `state_key` and *does* contain a `room_id`) into an + // `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not* + // expected to contain a `room_id`). It just means that the `room_id` will be ignored + // in a future deserialization. + TimelineEventKind::Decrypted(d) => d.event.cast_ref(), + TimelineEventKind::PlainText { event } => event, + } + } + + /// If the event was a decrypted event that was successfully decrypted, get + /// its encryption info. Otherwise, `None`. + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + match self { + TimelineEventKind::Decrypted(d) => Some(&d.encryption_info), + TimelineEventKind::PlainText { .. } => None, + } + } + + /// Takes ownership of this `TimelineEvent`, returning the (potentially + /// decrypted) Matrix event within. + pub fn into_raw(self) -> Raw { + match self { + // It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does + // *not* contain a `state_key` and *does* contain a `room_id`) into an + // `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not* + // expected to contain a `room_id`). It just means that the `room_id` will be ignored + // in a future deserialization. + TimelineEventKind::Decrypted(d) => d.event.cast(), + TimelineEventKind::PlainText { event } => event, + } + } +} + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for TimelineEventKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + Self::PlainText { event } => f + .debug_struct("TimelineEventDecryptionResult::PlainText") + .field("event", &DebugRawEvent(event)) + .finish(), + + Self::Decrypted(decrypted) => { + f.debug_tuple("TimelineEventDecryptionResult::Decrypted").field(decrypted).finish() + } + } + } +} + #[derive(Clone, Serialize, Deserialize)] /// A successfully-decrypted encrypted event. pub struct DecryptedRoomEvent { @@ -542,6 +598,7 @@ pub struct DecryptedRoomEvent { /// object. /// /// Will be `None` if no bundled event was encrypted. + #[serde(skip_serializing_if = "Option::is_none")] pub unsigned_encryption_info: Option>, } @@ -606,6 +663,79 @@ pub struct UnableToDecryptInfo { pub session_id: Option, } +/// Deserialization helper for [`SyncTimelineEvent`], for the modern format. +/// +/// This has the exact same fields as [`SyncTimelineEvent`] itself, but has a +/// regular `Deserialize` implementation. +#[derive(Debug, Deserialize)] +struct SyncTimelineEventDeserializationHelperV1 { + /// The event itself, together with any information on decryption. + kind: TimelineEventKind, + + /// The push actions associated with this event. + #[serde(default)] + push_actions: Vec, +} + +impl From for SyncTimelineEvent { + fn from(value: SyncTimelineEventDeserializationHelperV1) -> Self { + let SyncTimelineEventDeserializationHelperV1 { kind, push_actions } = value; + SyncTimelineEvent { kind, push_actions } + } +} + +/// Deserialization helper for [`SyncTimelineEvent`], for an older format. +#[derive(Deserialize)] +struct SyncTimelineEventDeserializationHelperV0 { + /// The actual event. + event: Raw, + + /// The encryption info about the event. Will be `None` if the event + /// was not encrypted. + encryption_info: Option, + + /// The push actions associated with this event. + #[serde(default)] + push_actions: Vec, + + /// The encryption info about the events bundled in the `unsigned` + /// object. + /// + /// Will be `None` if no bundled event was encrypted. + unsigned_encryption_info: Option>, +} + +impl From for SyncTimelineEvent { + fn from(value: SyncTimelineEventDeserializationHelperV0) -> Self { + let SyncTimelineEventDeserializationHelperV0 { + event, + encryption_info, + push_actions, + unsigned_encryption_info, + } = value; + + let kind = match encryption_info { + Some(encryption_info) => { + TimelineEventKind::Decrypted(DecryptedRoomEvent { + // We cast from `Raw` to + // `Raw`, which means + // we are asserting that it contains a room_id. + // That *should* be ok, because if this is genuinely a decrypted + // room event (as the encryption_info indicates), then it will have + // a room_id. + event: event.cast(), + encryption_info, + unsigned_encryption_info, + }) + } + + None => TimelineEventKind::PlainText { event }, + }; + + SyncTimelineEvent { kind, push_actions } + } +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; @@ -619,7 +749,8 @@ mod tests { use serde_json::json; use super::{ - AlgorithmInfo, EncryptionInfo, SyncTimelineEvent, TimelineEvent, VerificationState, + AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, + TimelineEventKind, VerificationState, }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; @@ -699,18 +830,20 @@ mod tests { #[test] fn sync_timeline_event_serialisation() { let room_event = SyncTimelineEvent { - inner_event: Raw::new(&example_event()).unwrap().cast(), - inner_encryption_info: Some(EncryptionInfo { - sender: user_id!("@sender:example.com").to_owned(), - sender_device: None, - algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { - curve25519_key: "xxx".to_owned(), - sender_claimed_keys: Default::default(), + kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { + event: Raw::new(&example_event()).unwrap().cast(), + encryption_info: EncryptionInfo { + sender: user_id!("@sender:example.com").to_owned(), + sender_device: None, + algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "xxx".to_owned(), + sender_claimed_keys: Default::default(), + }, + verification_state: VerificationState::Verified, }, - verification_state: VerificationState::Verified, + unsigned_encryption_info: None, }), push_actions: Default::default(), - unsigned_encryption_info: None, }; let serialized = serde_json::to_value(&room_event).unwrap(); @@ -719,25 +852,29 @@ mod tests { assert_eq!( serialized, json!({ - "event": { - "content": {"body": "secret", "msgtype": "m.text"}, - "event_id": "$xxxxx:example.org", - "origin_server_ts": 2189, - "room_id": "!someroom:example.com", - "sender": "@carl:example.com", - "type": "m.room.message", - }, - "encryption_info": { - "sender": "@sender:example.com", - "sender_device": null, - "algorithm_info": { - "MegolmV1AesSha2": { - "curve25519_key": "xxx", - "sender_claimed_keys": {} - } - }, - "verification_state": "Verified", - }, + "kind": { + "Decrypted": { + "event": { + "content": {"body": "secret", "msgtype": "m.text"}, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message", + }, + "encryption_info": { + "sender": "@sender:example.com", + "sender_device": null, + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified", + }, + } + } }) ); @@ -747,6 +884,35 @@ mod tests { assert_matches!( event.encryption_info().unwrap().algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { .. } - ) + ); + + // Test that the previous format can also be deserialized. + let serialized = json!({ + "event": { + "content": {"body": "secret", "msgtype": "m.text"}, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message", + }, + "encryption_info": { + "sender": "@sender:example.com", + "sender_device": null, + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified", + }, + }); + let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); + assert_matches!( + event.encryption_info().unwrap().algorithm_info, + AlgorithmInfo::MegolmV1AesSha2 { .. } + ); } } diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index f610911f359..1f4b4dbec1f 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -657,7 +657,7 @@ mod tests { "prev_batch": "foo", "timeline": [ { - "event": { + "kind": { "PlainText": { "event": { "content": { "body": "let it gooo!", "msgtype": "m.text" @@ -667,8 +667,7 @@ mod tests { "room_id": "!someroom:example.com", "sender": "@bob:example.com", "type": "m.room.message" - }, - "encryption_info": null + }}} } ] }) From 42f0d83b5396fbca07414b76110ec6b1dbebbe11 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 9 Oct 2024 15:01:59 +0100 Subject: [PATCH 253/979] timeline: remove redundant `Debug` implementations These are no longer required now that the event itself lives in an inner class. --- .../src/deserialized_responses.rs | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 5cd859cab67..c0717b28c96 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -303,7 +303,7 @@ pub struct EncryptionInfo { /// Previously, this differed from [`TimelineEvent`] by wrapping an /// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays /// they are essentially identical, and one of them should probably be removed. -#[derive(Clone, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct SyncTimelineEvent { /// The event itself, together with any information on decryption. pub kind: TimelineEventKind, @@ -364,19 +364,6 @@ impl SyncTimelineEvent { } } -#[cfg(not(tarpaulin_include))] -impl fmt::Debug for SyncTimelineEvent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let SyncTimelineEvent { kind, push_actions } = self; - let mut s = f.debug_struct("SyncTimelineEvent"); - s.field("kind", &kind); - if !push_actions.is_empty() { - s.field("push_actions", push_actions); - } - s.finish() - } -} - impl From> for SyncTimelineEvent { fn from(inner: Raw) -> Self { Self::new(inner) @@ -448,7 +435,7 @@ impl<'de> Deserialize<'de> for SyncTimelineEvent { /// Previously, this differed from [`SyncTimelineEvent`] by wrapping an /// [`AnyTimelineEvent`] instead of an [`AnySyncTimelineEvent`], but nowadays /// they are essentially identical, and one of them should probably be removed. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct TimelineEvent { /// The event itself, together with any information on decryption. pub kind: TimelineEventKind, @@ -499,21 +486,6 @@ impl From for TimelineEvent { } } -#[cfg(not(tarpaulin_include))] -impl fmt::Debug for TimelineEvent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let TimelineEvent { kind, push_actions } = self; - let mut s = f.debug_struct("TimelineEvent"); - s.field("kind", &kind); - if let Some(push_actions) = &push_actions { - if !push_actions.is_empty() { - s.field("push_actions", push_actions); - } - } - s.finish() - } -} - /// The event within a [`TimelineEvent`] or [`SyncTimelineEvent`], together with /// encryption data. #[derive(Clone, Serialize, Deserialize)] From 9c6413551cd6e15f01453e57178fe98e0d12200b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 8 Oct 2024 14:16:39 +0100 Subject: [PATCH 254/979] Inline `SyncTimelineEvent::set_raw` This is only used in one place, and is much better inlined anyway. --- crates/matrix-sdk-base/src/rooms/normal.rs | 7 ++++++- crates/matrix-sdk-common/src/deserialized_responses.rs | 5 ----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 95eead8bc7e..9f790ba33f4 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -23,6 +23,8 @@ use std::{ use bitflags::bitflags; use eyeball::{SharedObservable, Subscriber}; use futures_util::{Stream, StreamExt}; +#[cfg(feature = "experimental-sliding-sync")] +use matrix_sdk_common::deserialized_responses::TimelineEventKind; #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] use matrix_sdk_common::ring_buffer::RingBuffer; #[cfg(feature = "experimental-sliding-sync")] @@ -1285,7 +1287,10 @@ impl RoomInfo { if latest_event.event_id().as_deref() == Some(redacts) { match apply_redaction(latest_event.event().raw(), _raw, room_version) { Some(redacted) => { - latest_event.event_mut().set_raw(redacted); + // Even if the original event was encrypted, redaction removes all its + // fields so it cannot possibly be successfully decrypted after redaction. + latest_event.event_mut().kind = + TimelineEventKind::PlainText { event: redacted }; debug!("Redacted latest event"); } None => { diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index c0717b28c96..6ef33dbfd20 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -357,11 +357,6 @@ impl SyncTimelineEvent { pub fn into_raw(self) -> Raw { self.kind.into_raw() } - - /// Replace the Matrix event within this event. Used to handle redaction. - pub fn set_raw(&mut self, event: Raw) { - self.kind = TimelineEventKind::PlainText { event }; - } } impl From> for SyncTimelineEvent { From 81119a66d87bec10f87e6b6fdd12e3f9541aef87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Oct 2024 13:43:58 +0200 Subject: [PATCH 255/979] ci: Install libsqlite, it does not seem to be part of the latest ubuntu image (#4108) --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ .github/workflows/coverage.yml | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6de235b408c..d4fac449bf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,11 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Install libsqlite + run: | + sudo apt-get update + sudo apt-get install libsqlite3-dev + - name: Load cache uses: Swatinem/rust-cache@v2 with: @@ -114,6 +119,11 @@ jobs: - name: Checkout the repo uses: actions/checkout@v4.2.0 + - name: Install libsqlite + run: | + sudo apt-get update + sudo apt-get install libsqlite3-dev + - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -165,6 +175,12 @@ jobs: with: tool: protoc@3.20.3 + - name: Install libsqlite + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install libsqlite3-dev + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: @@ -367,6 +383,11 @@ jobs: - name: Checkout the repo uses: actions/checkout@v4.2.0 + - name: Install libsqlite + run: | + sudo apt-get update + sudo apt-get install libsqlite3-dev + - name: Install Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5e1b198951b..296bbdeaf4c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -62,6 +62,11 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} + - name: Install libsqlite + run: | + sudo apt-get update + sudo apt-get install libsqlite3-dev + - name: Install Rust uses: dtolnay/rust-toolchain@stable From cb51a3155a0228015189fa61477c29607fc97447 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 12:23:16 +0200 Subject: [PATCH 256/979] chore: get rid of unused dependencies --- Cargo.lock | 38 ------------------- bindings/matrix-sdk-ffi/Cargo.toml | 1 - crates/matrix-sdk-crypto/Cargo.toml | 1 - crates/matrix-sdk-store-encryption/Cargo.toml | 3 ++ examples/qr-login/Cargo.toml | 1 - 5 files changed, 3 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f562bfe965a..0fac945b493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -995,26 +995,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const_format" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "const_panic" version = "0.2.8" @@ -1691,7 +1671,6 @@ dependencies = [ "clap", "futures-util", "matrix-sdk", - "qrcode", "tokio", "tracing-subscriber", "url", @@ -3243,7 +3222,6 @@ dependencies = [ "async-trait", "bs58", "byteorder", - "cbc", "cfg-if", "ctr", "eyeball", @@ -3331,7 +3309,6 @@ dependencies = [ "once_cell", "paranoid-android", "ruma", - "sanitize-filename-reader-friendly", "serde", "serde_json", "thiserror", @@ -5238,15 +5215,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "sanitize-filename-reader-friendly" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750e71aac86f4b238844ac9416e7339a8de1225eb1ebe5fba89890f634c46bf" -dependencies = [ - "const_format", -] - [[package]] name = "schannel" version = "0.1.23" @@ -6345,12 +6313,6 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "uniffi" version = "0.28.0" diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 02400423b17..388375135c9 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -33,7 +33,6 @@ matrix-sdk-ui = { workspace = true, features = ["uniffi"] } mime = "0.3.16" once_cell = { workspace = true } ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] } -sanitize-filename-reader-friendly = "2.2.1" serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 13923b1c95f..76714b39542 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -35,7 +35,6 @@ as_variant = { workspace = true } async-trait = { workspace = true } bs58 = { version = "0.5.0" } byteorder = { workspace = true } -cbc = { version = "0.1.2", features = ["std"] } cfg-if = "1.0" ctr = "0.9.1" eyeball = { workspace = true } diff --git a/crates/matrix-sdk-store-encryption/Cargo.toml b/crates/matrix-sdk-store-encryption/Cargo.toml index e8a805f5d9c..178d4f355e5 100644 --- a/crates/matrix-sdk-store-encryption/Cargo.toml +++ b/crates/matrix-sdk-store-encryption/Cargo.toml @@ -33,3 +33,6 @@ anyhow = { workspace = true } [lints] workspace = true + +[package.metadata.cargo-machete] +ignored = ["getrandom"] # We do manually enable a feature for it. diff --git a/examples/qr-login/Cargo.toml b/examples/qr-login/Cargo.toml index 445e5384418..6af3a8d03ae 100644 --- a/examples/qr-login/Cargo.toml +++ b/examples/qr-login/Cargo.toml @@ -13,7 +13,6 @@ test = false anyhow = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } clap = { version = "4.0.15", features = ["derive"] } -qrcode = { version = "0.14.1" } futures-util = { workspace = true } tracing-subscriber = { workspace = true } url = "2.3.1" From 711f4cb868696a8c1337f01a9ba4dd570f13e255 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 12:25:11 +0200 Subject: [PATCH 257/979] ci: detect unused dependencies with `cargo-machete` --- .github/workflows/detect-unused-dependencies.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/detect-unused-dependencies.yml diff --git a/.github/workflows/detect-unused-dependencies.yml b/.github/workflows/detect-unused-dependencies.yml new file mode 100644 index 00000000000..d7a7f520300 --- /dev/null +++ b/.github/workflows/detect-unused-dependencies.yml @@ -0,0 +1,12 @@ +name: Detects unused dependencies +on: + pull_request: { branches: "*" } + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Machete + uses: bnjbvr/cargo-machete@main From ca7f2ad3d08fdbfa2098569aa6b5f0585770c5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Oct 2024 12:13:49 +0200 Subject: [PATCH 258/979] Add a cargo-release config --- benchmarks/Cargo.toml | 3 +++ bindings/matrix-sdk-crypto-ffi/Cargo.toml | 3 +++ bindings/matrix-sdk-ffi/Cargo.toml | 3 +++ crates/matrix-sdk-sqlite/README.md | 0 crates/matrix-sdk-ui/README.md | 0 examples/autojoin/Cargo.toml | 3 +++ examples/backups/Cargo.toml | 3 +++ examples/command_bot/Cargo.toml | 3 +++ examples/cross_signing_bootstrap/Cargo.toml | 3 +++ examples/custom_events/Cargo.toml | 3 +++ examples/emoji_verification/Cargo.toml | 3 +++ examples/get_profiles/Cargo.toml | 3 +++ examples/getting_started/Cargo.toml | 3 +++ examples/image_bot/Cargo.toml | 3 +++ examples/login/Cargo.toml | 3 +++ examples/oidc_cli/Cargo.toml | 3 +++ examples/persist_session/Cargo.toml | 3 +++ examples/qr-login/Cargo.toml | 3 +++ examples/secret_storage/Cargo.toml | 3 +++ examples/timeline/Cargo.toml | 3 +++ labs/multiverse/Cargo.toml | 3 +++ release.toml | 10 ++++++++++ testing/matrix-sdk-integration-testing/Cargo.toml | 3 +++ testing/matrix-sdk-test-macros/Cargo.toml | 3 +++ testing/matrix-sdk-test/Cargo.toml | 3 +++ uniffi-bindgen/Cargo.toml | 3 +++ xtask/Cargo.toml | 3 +++ 27 files changed, 82 insertions(+) create mode 100644 crates/matrix-sdk-sqlite/README.md create mode 100644 crates/matrix-sdk-ui/README.md create mode 100644 release.toml diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index ea0ba7370f5..0fa7d70cb0d 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -36,3 +36,6 @@ harness = false [[bench]] name = "room_bench" harness = false + +[package.metadata.release] +release = false diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index 3432c965ef8..1f6bdd4db28 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -67,3 +67,6 @@ assert_matches2 = { workspace = true } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 388375135c9..4268a0220f0 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -82,3 +82,6 @@ features = [ [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/crates/matrix-sdk-sqlite/README.md b/crates/matrix-sdk-sqlite/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/crates/matrix-sdk-ui/README.md b/crates/matrix-sdk-ui/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/autojoin/Cargo.toml b/examples/autojoin/Cargo.toml index 491914db83e..599f7db8232 100644 --- a/examples/autojoin/Cargo.toml +++ b/examples/autojoin/Cargo.toml @@ -19,3 +19,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/backups/Cargo.toml b/examples/backups/Cargo.toml index c6660a70004..cf8f3f6616e 100644 --- a/examples/backups/Cargo.toml +++ b/examples/backups/Cargo.toml @@ -22,3 +22,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/command_bot/Cargo.toml b/examples/command_bot/Cargo.toml index 5b15be635f3..401b03c6473 100644 --- a/examples/command_bot/Cargo.toml +++ b/examples/command_bot/Cargo.toml @@ -19,3 +19,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/cross_signing_bootstrap/Cargo.toml b/examples/cross_signing_bootstrap/Cargo.toml index 1592ec9415b..3910bad611f 100644 --- a/examples/cross_signing_bootstrap/Cargo.toml +++ b/examples/cross_signing_bootstrap/Cargo.toml @@ -20,3 +20,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/custom_events/Cargo.toml b/examples/custom_events/Cargo.toml index 27ed2f7ebec..7ca797dfb51 100644 --- a/examples/custom_events/Cargo.toml +++ b/examples/custom_events/Cargo.toml @@ -20,3 +20,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/emoji_verification/Cargo.toml b/examples/emoji_verification/Cargo.toml index d38c62b74c0..c3dd959f272 100644 --- a/examples/emoji_verification/Cargo.toml +++ b/examples/emoji_verification/Cargo.toml @@ -22,3 +22,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/get_profiles/Cargo.toml b/examples/get_profiles/Cargo.toml index c06cda8361f..bd0a9ff3c47 100644 --- a/examples/get_profiles/Cargo.toml +++ b/examples/get_profiles/Cargo.toml @@ -20,3 +20,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml index 1b913633080..0bbde23cfa5 100644 --- a/examples/getting_started/Cargo.toml +++ b/examples/getting_started/Cargo.toml @@ -19,3 +19,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/image_bot/Cargo.toml b/examples/image_bot/Cargo.toml index 1ae3bf43227..e9f89800d04 100644 --- a/examples/image_bot/Cargo.toml +++ b/examples/image_bot/Cargo.toml @@ -21,3 +21,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/login/Cargo.toml b/examples/login/Cargo.toml index a5cd1623dfe..7de8db9d4a5 100644 --- a/examples/login/Cargo.toml +++ b/examples/login/Cargo.toml @@ -20,3 +20,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk", features = ["sso-login"] } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/oidc_cli/Cargo.toml b/examples/oidc_cli/Cargo.toml index 30e475ca62e..a5c8617dd02 100644 --- a/examples/oidc_cli/Cargo.toml +++ b/examples/oidc_cli/Cargo.toml @@ -29,3 +29,6 @@ features = ["experimental-oidc"] [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/persist_session/Cargo.toml b/examples/persist_session/Cargo.toml index 8478f4e7420..cec117820bc 100644 --- a/examples/persist_session/Cargo.toml +++ b/examples/persist_session/Cargo.toml @@ -23,3 +23,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/qr-login/Cargo.toml b/examples/qr-login/Cargo.toml index 6af3a8d03ae..fddab3c59ef 100644 --- a/examples/qr-login/Cargo.toml +++ b/examples/qr-login/Cargo.toml @@ -22,3 +22,6 @@ url = "2.3.1" # have copied the example as it was at the time of the release you use. path = "../../crates/matrix-sdk" features = ["experimental-oidc"] + +[package.metadata.release] +release = false diff --git a/examples/secret_storage/Cargo.toml b/examples/secret_storage/Cargo.toml index 0b0466721a1..a5805b35b89 100644 --- a/examples/secret_storage/Cargo.toml +++ b/examples/secret_storage/Cargo.toml @@ -21,3 +21,6 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/examples/timeline/Cargo.toml b/examples/timeline/Cargo.toml index 0150f4c7718..4db0b16e538 100644 --- a/examples/timeline/Cargo.toml +++ b/examples/timeline/Cargo.toml @@ -23,3 +23,6 @@ matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui" } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/labs/multiverse/Cargo.toml b/labs/multiverse/Cargo.toml index 29098ea9193..8b367cb8f5b 100644 --- a/labs/multiverse/Cargo.toml +++ b/labs/multiverse/Cargo.toml @@ -27,3 +27,6 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/release.toml b/release.toml new file mode 100644 index 00000000000..8d55f8b0dca --- /dev/null +++ b/release.toml @@ -0,0 +1,10 @@ +owners = ["poljar", "github:matrix-org:rust"] + +pre-release-commit-message = "chore: Release matrix-sdk version {{version}}" +pre-release-replacements = [] +pre-release-hook = [] + +sign-tag = true +tag-message = "Release {{crate_name}} version {{version}}" +tag-name = "{{prefix}}{{version}}" +shared-version = true diff --git a/testing/matrix-sdk-integration-testing/Cargo.toml b/testing/matrix-sdk-integration-testing/Cargo.toml index 12d7fa3ce95..6620320bb3e 100644 --- a/testing/matrix-sdk-integration-testing/Cargo.toml +++ b/testing/matrix-sdk-integration-testing/Cargo.toml @@ -35,3 +35,6 @@ json-structural-diff = "0.1.0" [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/testing/matrix-sdk-test-macros/Cargo.toml b/testing/matrix-sdk-test-macros/Cargo.toml index 770ffac4cb6..efe8c261387 100644 --- a/testing/matrix-sdk-test-macros/Cargo.toml +++ b/testing/matrix-sdk-test-macros/Cargo.toml @@ -22,3 +22,6 @@ syn = { version = "2.0.43", features = ["full", "extra-traits"] } [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index 5d4bb62f017..7c1aa7391dd 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -35,3 +35,6 @@ wasm-bindgen-test = "0.3.33" [lints] workspace = true + +[package.metadata.release] +release = false diff --git a/uniffi-bindgen/Cargo.toml b/uniffi-bindgen/Cargo.toml index a7a51a7e19d..fb92ab76d9f 100644 --- a/uniffi-bindgen/Cargo.toml +++ b/uniffi-bindgen/Cargo.toml @@ -7,3 +7,6 @@ license = "Apache-2.0" [dependencies] uniffi = { workspace = true, features = ["cli"] } + +[package.metadata.release] +release = false diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 94f1bae3055..d6c00f0e071 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -17,3 +17,6 @@ serde_json = { workspace = true } fs_extra = "1" uniffi_bindgen = { workspace = true } xshell = "0.1.17" + +[package.metadata.release] +release = false From 4c7461357c66260e261a4d74f676c3b63542b759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Oct 2024 12:15:30 +0200 Subject: [PATCH 259/979] Add a git-cliff configuration file --- cliff-weekly-report.toml | 47 +++++++++++++++++++++ cliff.toml | 89 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 cliff-weekly-report.toml create mode 100644 cliff.toml diff --git a/cliff-weekly-report.toml b/cliff-weekly-report.toml new file mode 100644 index 00000000000..bfd0732c68f --- /dev/null +++ b/cliff-weekly-report.toml @@ -0,0 +1,47 @@ +# This git-cliff configuration file is used to generate weekly reports for This +# Week in Matrix amongst others. + +[changelog] +header = """ +# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }}) +""" +body = """ +{% for commit in commits %} + {% set_global commit_message = commit.message -%} + {% for footer in commit.footers -%} + {% if footer.token | lower == "changelog" -%} + {% set_global commit_message = footer.value -%} + {% elif footer.token | lower == "breaking-change" -%} + {% set_global commit_message = footer.value -%} + {% endif -%} + {% endfor -%} + - {{ commit_message | upper_first }} +{% endfor %} +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"}, +] +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor", skip = true }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore", skip = true }, + { message = "^style", group = "Styling", skip = true }, + { message = "^test", skip = true }, + { message = "^ci", skip = true }, +] +filter_commits = true +tag_pattern = "[0-9]*" +skip_tags = "" +ignore_tags = "" +date_order = false +sort_commits = "newest" diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000000..b81994cffeb --- /dev/null +++ b/cliff.toml @@ -0,0 +1,89 @@ +# This git-cliff configuration file is used to generate release reports. + +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/ +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + {% set_global commit_message = commit.message -%} + {% set_global breaking = commit.breaking -%} + {% for footer in commit.footers -%} + {% if footer.token | lower == "changelog" -%} + {% set_global commit_message = footer.value -%} + {% elif footer.token | lower == "breaking-change" -%} + {% set_global commit_message = footer.value -%} + {% elif footer.token | lower == "security-impact" -%} + {% set_global security_impact = footer.value -%} + {% elif footer.token | lower == "cve" -%} + {% set_global cve = footer.value -%} + {% elif footer.token | lower == "github-advisory" -%} + {% set_global github_advisory = footer.value -%} + {% endif -%} + {% endfor -%} + - {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }} + {% if security_impact -%} + (\ + *{{ security_impact | upper_first }}*\ + {% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\ + {% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%} + ) + {% endif -%} + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ + +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# regex for preprocessing the commit messages +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"}, +] +# regex for parsing and grouping commits +commit_parsers = [ + { footer = "Security-Impact:", group = "Security" }, + { footer = "CVE:", group = "Security" }, + { footer = "GitHub-Advisory:", group = "Security" }, + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore", skip = true }, + { message = "^style", group = "Styling", skip = true }, + { message = "^test", skip = true }, + { message = "^ci", skip = true }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = true +# glob pattern for matching git tags +tag_pattern = "[0-9]*" +# regex for skipping tags +skip_tags = "" +# regex for ignoring tags +ignore_tags = "" +# sort the tags chronologically +date_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" From 1945b508c38cc0f7c80b946a42ea521ecfe17a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Oct 2024 12:16:26 +0200 Subject: [PATCH 260/979] Create some missing changelog files --- crates/matrix-sdk-base/CHANGELOG.md | 4 ++++ crates/matrix-sdk-common/CHANGELOG.md | 4 ++++ crates/matrix-sdk-indexeddb/CHANGELOG.md | 4 ++++ crates/matrix-sdk-qrcode/CHANGELOG.md | 4 ++++ crates/matrix-sdk-sqlite/CHANGELOG.md | 4 ++++ crates/matrix-sdk-store-encryption/CHANGELOG.md | 4 ++++ crates/matrix-sdk-ui/CHANGELOG.md | 4 ++++ 7 files changed, 28 insertions(+) create mode 100644 crates/matrix-sdk-common/CHANGELOG.md create mode 100644 crates/matrix-sdk-qrcode/CHANGELOG.md create mode 100644 crates/matrix-sdk-sqlite/CHANGELOG.md create mode 100644 crates/matrix-sdk-store-encryption/CHANGELOG.md diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index dab46a947fc..aebdc82fe75 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -1,3 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + # unreleased - Add `BaseClient::room_key_recipient_strategy` field diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md new file mode 100644 index 00000000000..ba8118892a2 --- /dev/null +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +All notable changes to this project will be documented in this file. + diff --git a/crates/matrix-sdk-indexeddb/CHANGELOG.md b/crates/matrix-sdk-indexeddb/CHANGELOG.md index 7e5e0be9498..993b6152e47 100644 --- a/crates/matrix-sdk-indexeddb/CHANGELOG.md +++ b/crates/matrix-sdk-indexeddb/CHANGELOG.md @@ -1,3 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + # UNRELEASED - Improve the efficiency of objects stored in the crypto store. diff --git a/crates/matrix-sdk-qrcode/CHANGELOG.md b/crates/matrix-sdk-qrcode/CHANGELOG.md new file mode 100644 index 00000000000..ba8118892a2 --- /dev/null +++ b/crates/matrix-sdk-qrcode/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +All notable changes to this project will be documented in this file. + diff --git a/crates/matrix-sdk-sqlite/CHANGELOG.md b/crates/matrix-sdk-sqlite/CHANGELOG.md new file mode 100644 index 00000000000..ba8118892a2 --- /dev/null +++ b/crates/matrix-sdk-sqlite/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +All notable changes to this project will be documented in this file. + diff --git a/crates/matrix-sdk-store-encryption/CHANGELOG.md b/crates/matrix-sdk-store-encryption/CHANGELOG.md new file mode 100644 index 00000000000..ba8118892a2 --- /dev/null +++ b/crates/matrix-sdk-store-encryption/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +All notable changes to this project will be documented in this file. + diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 55f9cb79440..1068434a5a6 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -1,3 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + # unreleased Breaking changes: From 86d9fe59d234e421057b4ea937e6847a3483e3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Oct 2024 12:17:30 +0200 Subject: [PATCH 261/979] Create an xtask for the release handling --- xtask/src/main.rs | 5 ++ xtask/src/release.rs | 187 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 xtask/src/release.rs diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 73da0704f10..132335cc57a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,6 +1,7 @@ mod ci; mod fixup; mod kotlin; +mod release; mod swift; mod workspace; @@ -8,6 +9,7 @@ use ci::CiArgs; use clap::{Parser, Subcommand}; use fixup::FixupArgs; use kotlin::KotlinArgs; +use release::ReleaseArgs; use swift::SwiftArgs; use xshell::cmd; @@ -33,6 +35,8 @@ enum Command { #[clap(long)] open: bool, }, + /// Prepare and publish a release of the matrix-sdk crates + Release(ReleaseArgs), Swift(SwiftArgs), Kotlin(KotlinArgs), } @@ -44,6 +48,7 @@ fn main() -> Result<()> { Command::Doc { open } => build_docs(open.then_some("--open"), DenyWarnings::No), Command::Swift(cfg) => cfg.run(), Command::Kotlin(cfg) => cfg.run(), + Command::Release(cfg) => cfg.run(), } } diff --git a/xtask/src/release.rs b/xtask/src/release.rs new file mode 100644 index 00000000000..d31b49b8509 --- /dev/null +++ b/xtask/src/release.rs @@ -0,0 +1,187 @@ +use std::env; + +use clap::{Args, Subcommand, ValueEnum}; +use xshell::{cmd, pushd}; + +use crate::{workspace, Result}; + +#[derive(Args)] +pub struct ReleaseArgs { + #[clap(subcommand)] + cmd: ReleaseCommand, +} + +#[derive(PartialEq, Subcommand)] +enum ReleaseCommand { + /// Prepare the release of the matrix-sdk workspace. + /// + /// This command will update the `README.md`, prepend the `CHANGELOG.md` + /// file using `git cliff`, and bump the versions in the `Cargo.toml` + /// files. + Prepare { + /// What type of version bump we should perform. + version: ReleaseVersion, + /// Actually prepare a release. Dry-run mode is the default. + #[clap(long)] + execute: bool, + }, + /// Publish the release. + /// + /// This command will create signed tags, publish the release on crates.io, + /// and finally push the tags to the repo. + Publish { + /// Actually publish a release. Dry-run mode is the default + #[clap(long)] + execute: bool, + }, + /// Get a list of interesting changes that happened in the last week. + WeeklyReport, + /// Generate the changelog for a specific crate, this shouldn't be run + /// manually, cargo-release will call this. + #[clap(hide = true)] + Changelog, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)] +enum ReleaseVersion { + // TODO: Add a Major variant here once we're stable. + /// Create a new minor release. + #[default] + Minor, + /// Create a new patch release. + Patch, + /// Create a new release candidate. + Rc, +} + +impl ReleaseVersion { + fn as_str(&self) -> &str { + match self { + ReleaseVersion::Minor => "minor", + ReleaseVersion::Patch => "patch", + ReleaseVersion::Rc => "rc", + } + } +} + +impl ReleaseArgs { + pub fn run(self) -> Result<()> { + check_prerequisites(); + + // The changelog needs to be generated from the directory of the crate, + // `cargo-release` changes the directory for us but we need to + // make sure to not switch back to the workspace dir. + // + // More info: https://git-cliff.org/docs/usage/monorepos + if self.cmd != ReleaseCommand::Changelog { + let _p = pushd(workspace::root_path()?)?; + } + + match self.cmd { + ReleaseCommand::Prepare { version, execute } => prepare(version, execute), + ReleaseCommand::Publish { execute } => publish(execute), + ReleaseCommand::WeeklyReport => weekly_report(), + ReleaseCommand::Changelog => changelog(), + } + } +} + +fn check_prerequisites() { + if cmd!("cargo release --version").echo_cmd(false).ignore_stdout().run().is_err() { + eprintln!("This command requires cargo-release, please install it."); + eprintln!("More info can be found at: https://github.com/crate-ci/cargo-release?tab=readme-ov-file#install"); + + std::process::exit(1); + } + + if cmd!("git cliff --version").echo_cmd(false).ignore_stdout().run().is_err() { + eprintln!("This command requires git-cliff, please install it."); + eprintln!("More info can be found at: https://git-cliff.org/docs/installation/"); + + std::process::exit(1); + } +} + +fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> { + let cmd = cmd!("cargo release --no-publish --no-tag --no-push"); + + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + let cmd = cmd.arg(version.as_str()); + + cmd.run()?; + + if execute { + eprintln!( + "Please double check the changelogs and edit them if necessary, \ + publish the PR, and once it's merged, switch to `main` and pull the PR \ + and run `cargo xtask release publish`" + ); + } + + Ok(()) +} + +fn publish(execute: bool) -> Result<()> { + let cmd = cmd!("cargo release tag"); + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + cmd.run()?; + + let cmd = cmd!("cargo release publish"); + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + cmd.run()?; + + let cmd = cmd!("cargo release push"); + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + cmd.run()?; + + Ok(()) +} + +fn weekly_report() -> Result<()> { + let lines = cmd!("git log --pretty=format:%H --since='1 week ago'").read()?; + + let Some(start) = lines.split_whitespace().last() else { + panic!("Could not find a start range for the git commit range.") + }; + + cmd!("git cliff --config cliff-weekly-report.toml {start}..HEAD").run()?; + + Ok(()) +} + +/// Generate the changelog for a given crate. +/// +/// This will be called by `cargo-release` and it will set the correct +/// environment and call it from within the correct directory. +fn changelog() -> Result<()> { + let dry_run = env::var("DRY_RUN").map(|dry| str::parse::(&dry)).unwrap_or(Ok(true))?; + let crate_name = env::var("CRATE_NAME").expect("CRATE_NAME must be set"); + let new_version = env::var("NEW_VERSION").expect("NEW_VERSION must be set"); + + if dry_run { + println!( + "\nGenerating a changelog for {} (dry run), the following output will be prepended to the CHANGELOG.md file:\n", + crate_name + ); + } else { + println!("Generating a changelog for {}.", crate_name); + } + + let command = cmd!("git cliff") + .arg("cliff") + .arg("--config") + .arg("../../cliff.toml") + .arg("--include-path") + .arg(format!("crates/{}/**/*", crate_name)) + .arg("--repository") + .arg("../../") + .arg("--unreleased") + .arg("--tag") + .arg(&new_version); + + let command = if dry_run { command } else { command.arg("--prepend").arg("CHANGELOG.md") }; + + command.run()?; + + Ok(()) +} From ab0871f299eea9c8e51454ca4a09ab8fb0e4412d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Oct 2024 12:26:19 +0200 Subject: [PATCH 262/979] Call git-cliff as a pre-release hook --- release.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.toml b/release.toml index 8d55f8b0dca..9de2b7e4cfc 100644 --- a/release.toml +++ b/release.toml @@ -2,7 +2,7 @@ owners = ["poljar", "github:matrix-org:rust"] pre-release-commit-message = "chore: Release matrix-sdk version {{version}}" pre-release-replacements = [] -pre-release-hook = [] +pre-release-hook = ["cargo", "xtask", "release", "changelog"] sign-tag = true tag-message = "Release {{crate_name}} version {{version}}" From 9a4a67d488ec4070af509d1b955ffb5b55495342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Oct 2024 12:23:23 +0200 Subject: [PATCH 263/979] Document the new release process --- RELEASE.md | 21 --------------------- RELEASING.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 21 deletions(-) delete mode 100644 RELEASE.md create mode 100644 RELEASING.md diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index 25a486e9e29..00000000000 --- a/RELEASE.md +++ /dev/null @@ -1,21 +0,0 @@ -# Releasing `matrix-rust-sdk` - -- Make sure to bump all the crates to *the same version number*, and commit that (along with the - changes to the `Cargo.lock` file). -- Create a `git tag` for the current version, following the format `major.minor.patch`, e.g. `0.7.0`. -- Push the tag: `git push origin 0.7.0` -- Publish all the crates, in topological order of the dependency tree: - -``` -cargo publish -p matrix-sdk-test-macros -cargo publish -p matrix-sdk-test -cargo publish -p matrix-sdk-common -cargo publish -p matrix-sdk-qrcode -cargo publish -p matrix-sdk-store-encryption -cargo publish -p matrix-sdk-crypto -cargo publish -p matrix-sdk-base -cargo publish -p matrix-sdk-sqlite -cargo publish -p matrix-sdk-indexeddb -cargo publish -p matrix-sdk -cargo publish -p matrix-sdk-ui -``` diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000000..fadd5b76aae --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,47 @@ +# Releasing and publishing the SDK + +While the release process can be handled manually, `cargo-release` has been +configured to make it more convenient. + +By default, [`cargo-release`](https://github.com/crate-ci/cargo-release) assumes +that no pull request is required to cut a release. However, since the SDK +repo is set up so that each push requires a pull request, we need to slightly +deviate from the default workflow. A `cargo-xtask` has been created to make the +process as smooth as possible. + +The procedure is as follows: + +1. Switch to a release branch: + + ```bash + git switch -c release-x.y.z +  ``` + +2. Prepare the release. This will update the `README.md`, prepend the `CHANGELOG.md` + file using `git cliff`, and bump the version in the `Cargo.toml` file. + +```bash +cargo xtask release prepare --execute minor|patch|rc +``` + +3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are + satisfied, push the branch and open a PR. + +```bash +git push --set-upstream origin/release-x.y.z +``` + +4. Pass the review and merge the branch as you would with any other branch. + +5. Create tags for your new release, publish the release on crates.io and push + the tags: + +```bash +# Switch to main first. +git switch main +# Pull in the now-merged release commit(s). +git pull +# Create tags, publish the release on crates.io, and push the tags. +cargo xtask release publish --execute +``` +For more information on cargo-release: https://github.com/crate-ci/cargo-release From 1260e740ba7cadbadf49c6b829a4a3260216064b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 3 Oct 2024 14:15:05 +0200 Subject: [PATCH 264/979] Update the contributing guide with our new git-cliff setup Co-authored-by: Ivan Enderlin --- CONTRIBUTING.md | 119 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e09da0c6f1..9e3313cd9b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,43 +29,110 @@ integration tests that need a running synapse instance. These tests reside in [README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a synapse for testing purposes. -## Commit messages and PR title guidelines -Ideally, a PR should have a *proper title*, with *atomic logical commits*, and each commit -should have a *good commit message*. +## Pull requests -An *atomic logical commit* is one that is ideally small, can be compiled in isolation, and passes -tests. This is useful to make the review process easier (help your reviewer), but also when running -bisections, helping identifying which commit introduced a regression. +Ideally, a PR should have a *proper title*, with *atomic logical commits*, and +each commit should have a *good commit message*. -A *good commit message* should be composed of: +A *proper PR title* would be a one-liner summary of the changes in the PR, +following the same guidelines of a good commit message, including the +area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would +be a good PR title. -- a prefix to indicate which area/feature is related by the commit -- a short description that would give sufficient context for a reviewer to guess what the commit is - about. +(An additional bad example of a bad PR title would be `mynickname/branch name`, +that is, just the branch name.) -Examples of commit messages that aren't so useful: +# Writing changelog entries -- “add new method“ -- “enhance performance“ -- “fix receipts“ +We aim to maintain clear and informative changelogs that accurately reflect the +changes in our project. This guide will help you write useful changelog entries +using git-cliff, which fetches changelog entries from commit messages. -Examples of good commit messages: +## Commit message format -- “ffi: Add new method `frobnicate_the_foos`” -- “indexeddb: Break up the request inside `get_inbound_group_sessions`” -- “read_receipts: Store receipts locally, fixing #12345” +Commit messages should be formatted as Conventional Commits. In addition, some +git trailers are supported and have special meaning (see below). -A *proper PR title* would be a one-liner summary of the changes in the PR, following the -same guidelines of a good commit message, including the area/feature prefix. Something like -`FFI: Allow logs files to be pruned.` would be a good PR title. +### Conventional commits -(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the -branch name.) +Conventional Commits are structured as follows: -Having good commit messages and PR titles also helps with reviews, scanning the `git log` of -the project, and writing the [*This week in -Matrix*](https://matrix.org/category/this-week-in-matrix/) updates for the SDK. +``` +(): +``` + +The type of changes which will be included in changelogs is one of the following: + +* `feat`: A new feature +* `fix`: A bug fix +* `doc`: Documentation changes +* `refactor`: Code refactoring +* `perf`: Performance improvements +* `ci`: Changes to CI configuration files and scripts + +The scope is optional and can specify the area of the codebase affected (e.g., +olm, cipher). + +### Changelog trailer + +In addition to the Conventional Commit format, you can use the `Changelog` git +trailer to specify the changelog message explicitly. When that trailer is +present, its value will be used as the changelog entry instead of the commit's +leading line. The `Breaking-Change` git trailer can be used in a similar manner +if the changelog entry should be marked as a breaking change. + + +#### Example commit message + +``` +feat: Add a method to encode Ed25519 public keys to Base64 + +This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to +stringify Ed25519 and thus present them to users. It's also commonly used when +Ed25519 keys need to be inserted into JSON. + +Changelog: Added the `Ed25519PublicKey::to_base64()` method which can be used to +stringify the Ed25519 public key. +``` + +In this commit message, the content specified in the `Changelog` trailer will be +used for the changelog entry. + +### Security fixes + +Commits addressing security vulnerabilities must include specific trailers for +vulnerability metadata. These commits are required to include at least the +`Security-Impact` trailer to indicate that the commit is a security fix. + +Security issues have some additional git-trailers: + +* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical. +* `CVE`: The CVE that was assigned to this issue. +* `GitHub-Advisory`: The GitHub advisory identifier. + +Example: + +``` +fix(crypto): Use a constant-time Base64 encoder for secret key material + +This patch fixes a security issue around a side-channel vulnerability[1] +when decoding secret key material using Base64. + +In some circumstances an attacker can obtain information about secret +secret key material via a controlled-channel and side-channel attack. + +This patch avoids the side-channel by switching to the base64ct crate +for the encoding, and more importantly, the decoding of secret key +material. + +Security-Impact: Low +CVE: CVE-2024-40640 +GitHub-Advisory: GHSA-j8cm-g7r6-hfpq + +Changelog: Use a constant-time Base64 encoder for secret key material +to mitigate side-channel attacks leaking secret key material. +``` ## Review process From 248cf55272872175e3c6aa37b488b5a398cc6313 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 8 Oct 2024 16:25:29 +0200 Subject: [PATCH 265/979] fix(base): Don't use state events from `timeline` with sliding sync. With sliding sync, we must handle state events from `required_state` only, not from `timeline`, this is a mistake as they might be incomplete or _staled_. --- .../matrix-sdk-base/src/sliding_sync/mod.rs | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 4783777a06b..7d227b4408c 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -355,13 +355,12 @@ impl BaseClient { let mut room_data = Cow::Borrowed(room_data); let (raw_state_events, state_events): (Vec<_>, Vec<_>) = { - let mut state_events = Vec::new(); - // Read state events from the `required_state` field. - state_events.extend(Self::deserialize_state_events(&room_data.required_state)); + let state_events = Self::deserialize_state_events(&room_data.required_state); - // Read state events from the `timeline` field. - state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline)); + // Don't read state events from the `timeline` field, because they might be + // incomplete or staled already. We must only read state events from + // `required_state`. state_events.into_iter().unzip() }; @@ -632,33 +631,6 @@ impl BaseClient { } } } - - pub(crate) fn deserialize_state_events_from_timeline( - raw_events: &[Raw], - ) -> Vec<(Raw, AnySyncStateEvent)> { - raw_events - .iter() - .filter_map(|raw_event| { - // If it contains `state_key`, we assume it's a state event. - if raw_event.get_field::("state_key").transpose().is_some() { - match raw_event.deserialize_as::() { - Ok(event) => { - // SAFETY: Casting `AnySyncTimelineEvent` to `AnySyncStateEvent` is safe - // because we checked that there is a `state_key`. - Some((raw_event.clone().cast(), event)) - } - - Err(error) => { - warn!("Couldn't deserialize state event from timeline: {error}"); - None - } - } - } else { - None - } - }) - .collect() - } } /// Find the most recent decrypted event and cache it in the supplied RoomInfo. From 72dc3074007db8b82cf26377c5d9f7930fdfffe5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 8 Oct 2024 17:06:20 +0200 Subject: [PATCH 266/979] fix(base): Add a way for `handle_timeline` to ignore state events. Sliding sync expects all state events to be in `required_state`. State events in `timeline` **must be ignored**. However, in sync v2, state events in `timeline` **must be handled**. In the sync response flow, both sliding sync and sync v2 uses the same `handle_timeline` method. This patch adds an argument to ignore state events. This is not ideal, but it's a temporary solution as a first step. The next step is to refactor this code, but let's start easy. The rest of the patch updates the tests accordingly. --- crates/matrix-sdk-base/src/client.rs | 7 +++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 7 ++-- .../src/timeline/event_item/mod.rs | 37 +++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 7cb4fb0c82c..35ec906b855 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -384,6 +384,7 @@ impl BaseClient { room: &Room, limited: bool, events: Vec>, + ignore_state_events: bool, prev_batch: Option, push_rules: &Ruleset, user_ids: &mut BTreeSet, @@ -402,7 +403,7 @@ impl BaseClient { Ok(e) => { #[allow(clippy::single_match)] match &e { - AnySyncTimelineEvent::State(s) => { + AnySyncTimelineEvent::State(s) if !ignore_state_events => { match s { AnySyncStateEvent::RoomMember(member) => { Box::pin(ambiguity_cache.handle_event( @@ -436,6 +437,8 @@ impl BaseClient { changes.add_state_event(room.room_id(), s.clone(), raw_event); } + AnySyncTimelineEvent::State(_) => { /* do nothing */ } + AnySyncTimelineEvent::MessageLike( AnySyncMessageLikeEvent::RoomRedaction(r), ) => { @@ -981,6 +984,7 @@ impl BaseClient { &room, new_info.timeline.limited, new_info.timeline.events, + false, new_info.timeline.prev_batch, &push_rules, &mut user_ids, @@ -1075,6 +1079,7 @@ impl BaseClient { &room, new_info.timeline.limited, new_info.timeline.events, + false, new_info.timeline.prev_batch, &push_rules, &mut user_ids, diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 7d227b4408c..334336e3ba7 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -28,7 +28,7 @@ use ruma::api::client::sync::sync_events::v5; use ruma::events::AnyToDeviceEvent; use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom}, - events::{AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent}, + events::{AnyRoomAccountDataEvent, AnySyncStateEvent}, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, }; @@ -453,6 +453,7 @@ impl BaseClient { &room, room_data.limited, room_data.timeline.clone(), + true, room_data.prev_batch.clone(), &push_rules, &mut user_ids, @@ -1135,8 +1136,8 @@ mod tests { let response = response_with_room(room_id, room); client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); - // The room is left. - assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); + // The room is NOT left because state events from `timeline` must be IGNORED! + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined); } #[async_test] 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 3eb331025a8..723f482d49e 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -724,7 +724,7 @@ mod tests { deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http, MinimalStateEvent, OriginalMinimalStateEvent, }; - use matrix_sdk_test::{async_test, sync_timeline_event}; + use matrix_sdk_test::{async_test, sync_state_event, sync_timeline_event}; use ruma::{ event_id, events::{ @@ -732,7 +732,7 @@ mod tests { member::RoomMemberEventContent, message::{MessageFormat, MessageType}, }, - AnySyncTimelineEvent, BundledMessageLikeRelations, + AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations, }, room_id, serde::Raw, @@ -889,7 +889,12 @@ mod tests { let event = message_event(room_id, user_id, "**My M**", "My M", 122344); let client = logged_in_client(None).await; let mut room = http::response::Room::new(); - room.timeline.push(member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")); + room.required_state.push(member_event_as_state_event( + room_id, + user_id, + "Alice Margatroid", + "mxc://e.org/SEs", + )); // And the room is stored in the client so it can be extracted when needed let response = response_with_room(room_id, room); @@ -1000,6 +1005,32 @@ mod tests { }) } + fn member_event_as_state_event( + room_id: &RoomId, + user_id: &UserId, + display_name: &str, + avatar_url: &str, + ) -> Raw { + sync_state_event!({ + "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 + } + }) + } + fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response { let mut response = http::Response::new("6".to_owned()); response.rooms.insert(room_id.to_owned(), room); From a4782939b3ab2ce0edc22fd63e5dff9862f6e915 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 9 Oct 2024 12:07:01 +0200 Subject: [PATCH 267/979] test(integration): Fix one test by adding `required_state`. To fix the `test_left_room`, we need to ask for the `m.room.member` state event from `required_state`. The rest of the patch rewrites the test a little bit to make it more Rust idiomatic. --- .../src/tests/sliding_sync/room.rs | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 012a9b0bb2f..cde45683a3f 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -69,25 +69,35 @@ async fn test_left_room() -> Result<()> { .network_timeout(Duration::from_secs(3)) .add_list( SlidingSyncList::builder("all") - .sync_mode(SlidingSyncMode::new_selective().add_range(0..=20)), + .sync_mode(SlidingSyncMode::new_selective().add_range(0..=20)) + .required_state(vec![(StateEventType::RoomMember, "$ME".to_owned())]), ) .build() .await?; - let s = sliding_peter.clone(); - spawn(async move { - let stream = s.sync(); - pin_mut!(stream); - while let Some(up) = stream.next().await { - warn!("received update: {up:?}"); + spawn({ + let peter_sliding = sliding_peter.clone(); + + async move { + let stream = peter_sliding.sync(); + pin_mut!(stream); + + while let Some(up) = stream.next().await { + let up = up.expect("sync should not fail"); + + warn!("received update: {up:?}"); + } } }); // Set up regular sync for Steven. - let steven2 = steven.clone(); - spawn(async move { - if let Err(err) = steven2.sync(SyncSettings::default()).await { - error!("steven couldn't sync: {err}"); + spawn({ + let steven = steven.clone(); + + async move { + if let Err(err) = steven.sync(SyncSettings::default()).await { + error!("steven couldn't sync: {err}"); + } } }); @@ -103,20 +113,33 @@ async fn test_left_room() -> Result<()> { let mut joined = false; for _ in 0..10 { sleep(Duration::from_secs(1)).await; + if let Some(room) = steven.get_room(peter_room.room_id()) { room.join().await?; joined = true; break; } } - anyhow::ensure!(joined, "steven couldn't join after 10 seconds"); + + assert!(joined, "steven couldn't join after 10 seconds"); // Now Peter is just being rude. peter_room.leave().await?; - sleep(Duration::from_secs(1)).await; - let peter_room = peter.get_room(peter_room.room_id()).unwrap(); - assert_eq!(peter_room.state(), RoomState::Left); + let mut left = false; + + for _ in 0..10 { + sleep(Duration::from_secs(1)).await; + + let peter_room = peter.get_room(peter_room.room_id()).unwrap(); + + if peter_room.state() == RoomState::Left { + left = true; + break; + } + } + + assert!(left, "peter couldn't leave after 10 seconds"); Ok(()) } From 3ad8f1d607b779a1b473f4ef21dd6180fb4b44c8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 9 Oct 2024 13:41:55 +0200 Subject: [PATCH 268/979] test(integration): Fix one test by adding `required_state`. To fix the `test_room_avatar_group_conversation`, we need to ask for the `m.room.avatar` state event from `required_state`. The rest of the patch rewrites the test a little bit to make it more Rust idiomatic. The `response.rooms.*.avatar` field from sliding sync should contain the new avatar, but for the moment, it doesn't. It seems to be a bug. --- .../src/tests/sliding_sync/room.rs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index cde45683a3f..9dd0958f5ec 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -155,24 +155,31 @@ async fn test_room_avatar_group_conversation() -> Result<()> { celine.account().set_avatar_url(Some(mxc_uri!("mxc://localhost/celine"))).await?; // Set up sliding sync for alice. - let sliding_alice = alice + let alice_sliding = alice .sliding_sync("main")? .with_all_extensions() .poll_timeout(Duration::from_secs(2)) .network_timeout(Duration::from_secs(2)) .add_list( SlidingSyncList::builder("all") - .sync_mode(SlidingSyncMode::new_selective().add_range(0..=20)), + .sync_mode(SlidingSyncMode::new_selective().add_range(0..=20)) + .required_state(vec![(StateEventType::RoomAvatar, "".to_owned())]), ) .build() .await?; - let s = sliding_alice.clone(); - spawn(async move { - let stream = s.sync(); - pin_mut!(stream); - while let Some(up) = stream.next().await { - warn!("received update: {up:?}"); + spawn({ + let alice_sliding = alice_sliding.clone(); + + async move { + let stream = alice_sliding.sync(); + pin_mut!(stream); + + while let Some(up) = stream.next().await { + let up = up.expect("update must not fail"); + + warn!("received update: {up:?}"); + } } }); From 22c765b9abda9fc1117bc8f4ccce81346f1f9bd4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 9 Oct 2024 14:15:14 +0200 Subject: [PATCH 269/979] fix(ui): `all_rooms` in `RoomListService` requires the `m.room.avatar` state. This patch updates the `required_state` of `all_rooms` inside the `RoomListService` to add `m.room.name`. Apparently, Synapse doesn't always update the `response.rooms.*.avatar` field when the avatar is updated. It's being investigated, but it doesn't hurt to ensure we get it from the state events. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 1 + crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 81ce260947d..932d0e9e019 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -151,6 +151,7 @@ impl RoomListService { (StateEventType::RoomMember, "$ME".to_owned()), (StateEventType::RoomName, "".to_owned()), (StateEventType::RoomCanonicalAlias, "".to_owned()), + (StateEventType::RoomAvatar, "".to_owned()), (StateEventType::RoomPowerLevels, "".to_owned()), ]) .include_heroes(Some(true)) 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 674cbddbd59..db7291209e6 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -332,6 +332,7 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.member", "$ME"], ["m.room.name", ""], ["m.room.canonical_alias", ""], + ["m.room.avatar", ""], ["m.room.power_levels", ""], ], "include_heroes": true, From 85682ac37f4b8b56b99fc23980ec2175af6636b9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 14:43:48 +0200 Subject: [PATCH 270/979] chore(timeline): add extra logs to investigate edit issues --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 2 +- crates/matrix-sdk-ui/src/timeline/event_handler.rs | 3 +++ .../src/timeline/event_item/content/message.rs | 13 ++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 9095a94937b..ba23ab6e5bb 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -812,7 +812,7 @@ impl TimelineController

{ } } - warn!("Timeline item not found, can't add event ID"); + warn!("Timeline item not found, can't update send state"); return; }; diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 0550ea94158..1161030522f 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -523,6 +523,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let Some(message) = event_item.content.as_message() else { return }; let Some(in_reply_to) = message.in_reply_to() else { return }; if replacement.event_id == in_reply_to.event_id { + trace!(reply_event_id = ?event_item.identifier(), "Updating response to edited event"); let in_reply_to = InReplyToDetails { event_id: in_reply_to.event_id.clone(), event: TimelineDetails::Ready(Box::new( @@ -554,6 +555,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } /// Try to stash a pending edit, if it makes sense to do so. + #[instrument(skip(self, replacement))] fn stash_pending_edit( &mut self, position: TimelineItemPosition, @@ -604,6 +606,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { event_id: &EventId, ) -> Option { let pos = edits.iter().position(|edit| edit.edited_event() == event_id)?; + trace!(edited_event = %event_id, "unstashed pending edit"); Some(edits.remove(pos).unwrap()) } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 1c232e9a52d..4e361e3862b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -37,7 +37,7 @@ use ruma::{ serde::Raw, OwnedEventId, OwnedUserId, RoomVersionId, UserId, }; -use tracing::error; +use tracing::{error, trace}; use super::TimelineItemContent; use crate::{ @@ -99,6 +99,7 @@ impl Message { /// Apply an edit to the current message. pub(crate) fn apply_edit(&mut self, mut new_content: RoomMessageEventContentWithoutRelation) { + trace!("applying edit to a Message"); // Edit's content is never supposed to contain the reply fallback. new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No); self.msgtype = new_content.msgtype; @@ -191,7 +192,10 @@ pub(crate) fn extract_room_msg_edit_content( .content .relates_to { - Some(Relation::Replacement(re)) => Some(re.new_content), + Some(Relation::Replacement(re)) => { + trace!("found a bundled edit event in a room message"); + Some(re.new_content) + } _ => { error!("got m.room.message event with an edit without a valid m.replace relation"); None @@ -215,7 +219,10 @@ pub(crate) fn extract_poll_edit_content( match *relations.replace? { AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Original(ev)) => { match ev.content { - UnstablePollStartEventContent::Replacement(re) => Some(re.relates_to.new_content), + UnstablePollStartEventContent::Replacement(re) => { + trace!("found a bundled edit event in a poll"); + Some(re.relates_to.new_content) + } _ => { error!("got new poll start event in a bundled edit"); None From b002a8da52a6ee1a3879b4c8e72f3a888a1fafbc Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 11:31:38 +0200 Subject: [PATCH 271/979] refactor(ffi): Don't repeat information in `EventTimelineItem` about local vs remote echoes --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 3 +-- crates/matrix-sdk-ui/src/timeline/event_item/mod.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 9182fcf55fb..82ba36c82ef 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1032,7 +1032,7 @@ impl From for ShieldState { #[derive(Clone, uniffi::Record)] pub struct EventTimelineItem { - is_local: bool, + /// Indicates that an event is remote. is_remote: bool, event_or_transaction_id: EventOrTransactionId, sender: String, @@ -1072,7 +1072,6 @@ impl From for EventTimelineItem { let read_receipts = value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect(); Self { - is_local: value.is_local_echo(), is_remote: !value.is_local_echo(), event_or_transaction_id: value.identifier().into(), sender: value.sender().to_string(), 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 723f482d49e..62984c09d8d 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -237,11 +237,20 @@ impl EventTimelineItem { /// /// This returns `true` for events created locally, until the server echoes /// back the full event as part of a sync response. + /// + /// This is the opposite of [`Self::is_remote_event`]. pub fn is_local_echo(&self) -> bool { matches!(self.kind, EventTimelineItemKind::Local(_)) } - pub(super) fn is_remote_event(&self) -> bool { + /// Check whether this item is a remote event. + /// + /// This returns `true` only for events that have been echoed back from the + /// homeserver. A local echo sent but not echoed back yet will return + /// `false` here. + /// + /// This is the opposite of [`Self::is_local_echo`]. + pub fn is_remote_event(&self) -> bool { matches!(self.kind, EventTimelineItemKind::Remote(_)) } From 32919405d6d87f7255c4f5aff6243183a30f65bd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 11:34:50 +0200 Subject: [PATCH 272/979] refactor(ffi): use a single provider for lazily computed info --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 71 +++++++++------------ 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 82ba36c82ef..a9dd3597698 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1042,17 +1042,16 @@ pub struct EventTimelineItem { content: TimelineItemContent, timestamp: u64, reactions: Vec, - debug_info_provider: Arc, local_send_state: Option, read_receipts: HashMap, origin: Option, can_be_replied_to: bool, - shields_provider: Arc, + lazy_provider: Arc, } impl From for EventTimelineItem { - fn from(value: matrix_sdk_ui::timeline::EventTimelineItem) -> Self { - let reactions = value + fn from(item: matrix_sdk_ui::timeline::EventTimelineItem) -> Self { + let reactions = item .reactions() .iter() .map(|(k, v)| Reaction { @@ -1066,27 +1065,25 @@ impl From for EventTimelineItem { .collect(), }) .collect(); - let value = Arc::new(value); - let debug_info_provider = Arc::new(EventTimelineItemDebugInfoProvider(value.clone())); - let shields_provider = Arc::new(EventShieldsProvider(value.clone())); + let item = Arc::new(item); + let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone())); let read_receipts = - value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect(); + item.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect(); Self { - is_remote: !value.is_local_echo(), - event_or_transaction_id: value.identifier().into(), - sender: value.sender().to_string(), - sender_profile: value.sender_profile().into(), - is_own: value.is_own(), - is_editable: value.is_editable(), - content: value.content().clone().into(), - timestamp: value.timestamp().0.into(), + is_remote: !item.is_local_echo(), + event_or_transaction_id: item.identifier().into(), + sender: item.sender().to_string(), + sender_profile: item.sender_profile().into(), + is_own: item.is_own(), + is_editable: item.is_editable(), + content: item.content().clone().into(), + timestamp: item.timestamp().0.into(), reactions, - debug_info_provider, - local_send_state: value.send_state().map(|s| s.into()), + local_send_state: item.send_state().map(|s| s.into()), read_receipts, - origin: value.origin(), - can_be_replied_to: value.can_be_replied_to(), - shields_provider, + origin: item.origin(), + can_be_replied_to: item.can_be_replied_to(), + lazy_provider, } } } @@ -1102,22 +1099,6 @@ impl From for Receipt { } } -/// Wrapper to retrieve the debug info lazily instead of immediately -/// transforming it for each timeline event. -#[derive(uniffi::Object)] -pub struct EventTimelineItemDebugInfoProvider(Arc); - -#[matrix_sdk_ffi_macros::export] -impl EventTimelineItemDebugInfoProvider { - fn get(&self) -> EventTimelineItemDebugInfo { - EventTimelineItemDebugInfo { - model: format!("{:#?}", self.0), - original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()), - latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()), - } - } -} - #[derive(Clone, uniffi::Record)] pub struct EventTimelineItemDebugInfo { model: String, @@ -1269,13 +1250,23 @@ impl TryFrom for SdkEditedContent { } } -/// Wrapper to retrieve the shields info lazily. +/// Wrapper to retrieve some timeline item info lazily. #[derive(Clone, uniffi::Object)] -pub struct EventShieldsProvider(Arc); +pub struct LazyTimelineItemProvider(Arc); #[matrix_sdk_ffi_macros::export] -impl EventShieldsProvider { +impl LazyTimelineItemProvider { + /// Returns the shields for this event timeline item. fn get_shields(&self, strict: bool) -> Option { self.0.get_shield(strict).map(Into::into) } + + /// Returns some debug information for this event timeline item. + fn debug_info(&self) -> EventTimelineItemDebugInfo { + EventTimelineItemDebugInfo { + model: format!("{:#?}", self.0), + original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()), + latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()), + } + } } From 41a2ad09cf53f4195033d48ef356e7cbf39552a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Oct 2024 19:44:05 +0200 Subject: [PATCH 273/979] chore: Move the ffi macros into the bindings folder --- Cargo.toml | 2 +- {testing => bindings}/matrix-sdk-ffi-macros/Cargo.toml | 0 {testing => bindings}/matrix-sdk-ffi-macros/README.md | 0 {testing => bindings}/matrix-sdk-ffi-macros/src/lib.rs | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename {testing => bindings}/matrix-sdk-ffi-macros/Cargo.toml (100%) rename {testing => bindings}/matrix-sdk-ffi-macros/README.md (100%) rename {testing => bindings}/matrix-sdk-ffi-macros/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 448a450312f..da337457d4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" } matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" } matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" } -matrix-sdk-ffi-macros = { path = "testing/matrix-sdk-ffi-macros", version = "0.7.0" } +matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" } matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false } matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" } matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false } diff --git a/testing/matrix-sdk-ffi-macros/Cargo.toml b/bindings/matrix-sdk-ffi-macros/Cargo.toml similarity index 100% rename from testing/matrix-sdk-ffi-macros/Cargo.toml rename to bindings/matrix-sdk-ffi-macros/Cargo.toml diff --git a/testing/matrix-sdk-ffi-macros/README.md b/bindings/matrix-sdk-ffi-macros/README.md similarity index 100% rename from testing/matrix-sdk-ffi-macros/README.md rename to bindings/matrix-sdk-ffi-macros/README.md diff --git a/testing/matrix-sdk-ffi-macros/src/lib.rs b/bindings/matrix-sdk-ffi-macros/src/lib.rs similarity index 100% rename from testing/matrix-sdk-ffi-macros/src/lib.rs rename to bindings/matrix-sdk-ffi-macros/src/lib.rs From e46e63771bf0a3f8bd2f56a6c61d21774f933a27 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 10 Oct 2024 21:15:58 +0200 Subject: [PATCH 274/979] chore(ffi): Merge export and export_async attribute macros --- bindings/matrix-sdk-ffi-macros/src/lib.rs | 65 ++++++++----------- bindings/matrix-sdk-ffi/src/authentication.rs | 2 +- bindings/matrix-sdk-ffi/src/client.rs | 4 +- bindings/matrix-sdk-ffi/src/client_builder.rs | 2 +- bindings/matrix-sdk-ffi/src/encryption.rs | 6 +- bindings/matrix-sdk-ffi/src/notification.rs | 2 +- .../src/notification_settings.rs | 2 +- bindings/matrix-sdk-ffi/src/room.rs | 2 +- .../src/room_directory_search.rs | 2 +- bindings/matrix-sdk-ffi/src/room_list.rs | 4 +- .../src/session_verification.rs | 2 +- bindings/matrix-sdk-ffi/src/sync_service.rs | 4 +- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 6 +- bindings/matrix-sdk-ffi/src/widget.rs | 6 +- 14 files changed, 48 insertions(+), 61 deletions(-) diff --git a/bindings/matrix-sdk-ffi-macros/src/lib.rs b/bindings/matrix-sdk-ffi-macros/src/lib.rs index 6c55aa0c797..5a0bca7b3ee 100644 --- a/bindings/matrix-sdk-ffi-macros/src/lib.rs +++ b/bindings/matrix-sdk-ffi-macros/src/lib.rs @@ -14,65 +14,52 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{spanned::Spanned as _, ImplItem, Item}; +use syn::{ImplItem, Item, TraitItem}; -/// Attribute to always specify the async runtime parameter for the `uniffi` -/// export macros. -#[proc_macro_attribute] -pub fn export_async(_attr: TokenStream, item: TokenStream) -> TokenStream { - let item = proc_macro2::TokenStream::from(item); - - quote! { - #[uniffi::export(async_runtime = "tokio")] - #item - } - .into() -} - -/// Attribute to always specify the async runtime parameter for the `uniffi` -/// export macros. +/// Attribute to specify the async runtime parameter for the `uniffi` +/// export macros if there any `async fn`s in the input. #[proc_macro_attribute] pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream { - let run_checks = || { - let item: Item = syn::parse(item.clone())?; + let has_async_fn = |item| { if let Item::Fn(fun) = &item { - // Fail compilation if the function is async. if fun.sig.asyncness.is_some() { - let error = syn::Error::new( - fun.span(), - "async function must be exported with #[export_async]", - ); - return Err(error); + return true; } } else if let Item::Impl(blk) = &item { - // Fail compilation if at least one function in the impl block is async. for item in &blk.items { if let ImplItem::Fn(fun) = item { if fun.sig.asyncness.is_some() { - let error = syn::Error::new( - blk.span(), - "impl block with async functions must be exported with #[export_async]", - ); - return Err(error); + return true; + } + } + } + } else if let Item::Trait(blk) = &item { + for item in &blk.items { + if let TraitItem::Fn(fun) = item { + if fun.sig.asyncness.is_some() { + return true; } } } } - Ok(()) + false }; - let maybe_error = - if let Err(err) = run_checks() { Some(err.into_compile_error()) } else { None }; + let attr2 = proc_macro2::TokenStream::from(attr); + let item2 = proc_macro2::TokenStream::from(item.clone()); - let item = proc_macro2::TokenStream::from(item); - let attr = proc_macro2::TokenStream::from(attr); + let res = match syn::parse(item) { + Ok(item) => match has_async_fn(item) { + true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] }, + false => quote! { #[uniffi::export(#attr2)] }, + }, + Err(e) => e.into_compile_error(), + }; quote! { - #maybe_error - - #[uniffi::export(#attr)] - #item + #res + #item2 } .into() } diff --git a/bindings/matrix-sdk-ffi/src/authentication.rs b/bindings/matrix-sdk-ffi/src/authentication.rs index 3a22244c519..8540ca3b644 100644 --- a/bindings/matrix-sdk-ffi/src/authentication.rs +++ b/bindings/matrix-sdk-ffi/src/authentication.rs @@ -62,7 +62,7 @@ pub struct SsoHandler { pub(crate) url: String, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl SsoHandler { /// Returns the URL for starting SSO authentication. The URL should be /// opened in a web view. Once the web view succeeds, call `finish` with diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index f6646b2b176..8e451b65b6e 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -260,7 +260,7 @@ impl Client { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl Client { /// Information about login options for the client's homeserver. pub async fn homeserver_login_details(&self) -> Arc { @@ -526,7 +526,7 @@ impl Client { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl Client { /// The sliding sync version. pub fn sliding_sync_version(&self) -> SlidingSyncVersion { diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 9b817dd4f66..f05b211dafc 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -270,7 +270,7 @@ pub struct ClientBuilder { request_config: Option, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl ClientBuilder { #[uniffi::constructor] pub fn new() -> Arc { diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index cd7c6e94fc1..25849e60e42 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -212,7 +212,7 @@ impl From for VerificationState { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl Encryption { /// Get the public ed25519 key of our own device. This is usually what is /// called the fingerprint of the device. @@ -432,7 +432,7 @@ pub struct UserIdentity { inner: matrix_sdk::encryption::identities::UserIdentity, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl UserIdentity { /// Remember this identity, ensuring it does not result in a pin violation. /// @@ -468,7 +468,7 @@ pub struct IdentityResetHandle { pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl IdentityResetHandle { /// Get the underlying [`CrossSigningResetAuthType`] this identity reset /// process is using. diff --git a/bindings/matrix-sdk-ffi/src/notification.rs b/bindings/matrix-sdk-ffi/src/notification.rs index 35d047b94a2..5c51a695150 100644 --- a/bindings/matrix-sdk-ffi/src/notification.rs +++ b/bindings/matrix-sdk-ffi/src/notification.rs @@ -88,7 +88,7 @@ pub struct NotificationClient { pub(crate) _client: Arc, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl NotificationClient { /// See also documentation of /// `MatrixNotificationClient::get_notification`. diff --git a/bindings/matrix-sdk-ffi/src/notification_settings.rs b/bindings/matrix-sdk-ffi/src/notification_settings.rs index 1a01011a889..fc1b7fbb8a4 100644 --- a/bindings/matrix-sdk-ffi/src/notification_settings.rs +++ b/bindings/matrix-sdk-ffi/src/notification_settings.rs @@ -98,7 +98,7 @@ impl Drop for NotificationSettings { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl NotificationSettings { pub fn set_delegate(&self, delegate: Option>) { if let Some(delegate) = delegate { diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index d5d36e57a19..8ecbe787623 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -82,7 +82,7 @@ impl Room { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl Room { pub fn id(&self) -> String { self.inner.room_id().to_string() diff --git a/bindings/matrix-sdk-ffi/src/room_directory_search.rs b/bindings/matrix-sdk-ffi/src/room_directory_search.rs index 0666b50e3b3..9d37b60d45e 100644 --- a/bindings/matrix-sdk-ffi/src/room_directory_search.rs +++ b/bindings/matrix-sdk-ffi/src/room_directory_search.rs @@ -79,7 +79,7 @@ impl RoomDirectorySearch { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl RoomDirectorySearch { pub async fn next_page(&self) -> Result<(), ClientError> { let mut inner = self.inner.write().await; diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 9f70e9e71f3..d8700571580 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -85,7 +85,7 @@ pub struct RoomListService { pub(crate) utd_hook: Option>, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl RoomListService { fn state(&self, listener: Box) -> Arc { let state_stream = self.inner.state(); @@ -549,7 +549,7 @@ impl RoomListItem { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl RoomListItem { fn id(&self) -> String { self.inner.id().to_string() diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index f0cf5437425..655e92d7e5f 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -58,7 +58,7 @@ pub struct SessionVerificationController { sas_verification: Arc>>, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl SessionVerificationController { pub async fn is_verified(&self) -> Result { let device = diff --git a/bindings/matrix-sdk-ffi/src/sync_service.rs b/bindings/matrix-sdk-ffi/src/sync_service.rs index 3d3d30b0a66..dd1e6b89d27 100644 --- a/bindings/matrix-sdk-ffi/src/sync_service.rs +++ b/bindings/matrix-sdk-ffi/src/sync_service.rs @@ -62,7 +62,7 @@ pub struct SyncService { utd_hook: Option>, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl SyncService { pub fn room_list_service(&self) -> Arc { Arc::new(RoomListService { @@ -110,7 +110,7 @@ impl SyncServiceBuilder { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl SyncServiceBuilder { pub fn with_cross_process_lock(self: Arc, app_identifier: Option) -> Arc { let this = unwrap_or_clone_arc(self); diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index a9dd3597698..7d3117c3b8c 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -142,7 +142,7 @@ impl Timeline { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl Timeline { pub async fn add_listener(&self, listener: Box) -> Arc { let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await; @@ -688,7 +688,7 @@ pub struct SendHandle { inner: Mutex>, } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl SendHandle { /// Try to abort the sending of the current event. /// @@ -1182,7 +1182,7 @@ impl SendAttachmentJoinHandle { } } -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl SendAttachmentJoinHandle { pub async fn join(&self) -> Result<(), RoomError> { let join_hdl = self.join_hdl.clone(); diff --git a/bindings/matrix-sdk-ffi/src/widget.rs b/bindings/matrix-sdk-ffi/src/widget.rs index 2e5ca65c8a1..956670f4bb0 100644 --- a/bindings/matrix-sdk-ffi/src/widget.rs +++ b/bindings/matrix-sdk-ffi/src/widget.rs @@ -29,7 +29,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result>); -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl WidgetDriver { pub async fn run( &self, @@ -96,7 +96,7 @@ impl From for WidgetSettings { /// * `room` - A matrix room which is used to query the logged in username /// * `props` - Properties from the client that can be used by a widget to adapt /// to the client. e.g. language, font-scale... -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] pub async fn generate_webview_url( widget_settings: WidgetSettings, room: Arc, @@ -354,7 +354,7 @@ impl From for matrix_sdk::widget::ClientProperties { #[derive(uniffi::Object)] pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle); -#[matrix_sdk_ffi_macros::export_async] +#[matrix_sdk_ffi_macros::export] impl WidgetDriverHandle { /// Receive a message from the widget driver. /// From a4bda1ac6636610f6d13cecd6f56fb9e76a67556 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 10 Oct 2024 21:42:19 +0200 Subject: [PATCH 275/979] chore: Move lint configuration out of .cargo/config.toml This allows removing a lot of hacks to avoid spurious rebuilds. --- .cargo/config.toml | 48 ----------------------------- .github/workflows/documentation.yml | 2 -- Cargo.toml | 18 +++++++++++ crates/matrix-sdk-base/src/lib.rs | 1 + crates/matrix-sdk-crypto/src/lib.rs | 1 + crates/matrix-sdk/src/lib.rs | 2 ++ xtask/src/ci.rs | 4 --- xtask/src/main.rs | 2 -- 8 files changed, 22 insertions(+), 56 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 21f1c45399a..399ebdbfc98 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,13 +1,3 @@ -# Pass the rustflags specified to host dependencies (build scripts, proc-macros) -# when a `--target` is passed to Cargo. Historically this was not the case, and -# because of that, cross-compilation would not set the rustflags configured -# below in `target.'cfg(...)'` for them, resulting in cache invalidation. -# -# Since this is an unstable feature (enabled at the bottom of the file), this -# setting is unfortunately ignored on stable toolchains, but it's still better -# to have it apply on nightly than using the old behavior for all toolchains. -target-applies-to-host = false - [alias] xtask = "run --package xtask --" uniffi-bindgen = "run --package uniffi-bindgen --" @@ -15,43 +5,5 @@ uniffi-bindgen = "run --package uniffi-bindgen --" [doc.extern-map.registries] crates-io = "https://docs.rs/" -# Exclude tarpaulin, android and ios from extra lints since on stable, without -# the nightly-only target-applies-to-host setting at the top, cross compilation -# and otherwise changing cfg's can be very bad for caching. These should never -# be the default either and don't have much target-specific code that would -# benefit from the extra lints. -[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))'] -rustflags = [ - "-Wrust_2018_idioms", - "-Wsemicolon_in_expressions_from_macros", - "-Wunused_extern_crates", - "-Wunused_import_braces", - "-Wunused_qualifications", - "-Wtrivial_casts", - "-Wtrivial_numeric_casts", - "-Wclippy::cloned_instead_of_copied", - "-Wclippy::dbg_macro", - "-Wclippy::inefficient_to_string", - "-Wclippy::macro_use_imports", - "-Wclippy::mut_mut", - "-Wclippy::needless_borrow", - "-Wclippy::nonstandard_macro_braces", - "-Wclippy::str_to_string", - "-Wclippy::todo", - "-Wclippy::unused_async", - "-Wclippy::redundant_clone", -] - -[target.'cfg(target_arch = "wasm32")'] -rustflags = [ - # We have some types that are !Send and/or !Sync only on wasm, it would be - # slightly more efficient, but also pretty annoying, to wrap them in Rc - # where we would use Arc on other platforms. - "-Aclippy::arc_with_non_send_sync", -] - -# activate the target-applies-to-host feature. -# Required for `target-applies-to-host` at the top to take effect. [unstable] rustdoc-map = true -target-applies-to-host = true diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index e9ddc2066ec..d83dc6eb6cf 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -51,8 +51,6 @@ jobs: # Keep in sync with xtask docs - name: Build documentation env: - # Work around https://github.com/rust-lang/cargo/issues/10744 - CARGO_TARGET_APPLIES_TO_HOST: "true" RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings" run: cargo doc --no-deps --workspace --features docsrs diff --git a/Cargo.toml b/Cargo.toml index da337457d4c..7a9cdb59105 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,8 +131,26 @@ tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "c paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" } [workspace.lints.rust] +rust_2018_idioms = "warn" +semicolon_in_expressions_from_macros = "warn" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } +unused_extern_crates = "warn" +unused_import_braces = "warn" +unused_qualifications = "warn" +trivial_casts = "warn" +trivial_numeric_casts = "warn" [workspace.lints.clippy] assigning_clones = "allow" box_default = "allow" +cloned_instead_of_copied = "warn" +dbg_macro = "warn" +inefficient_to_string = "warn" +macro_use_imports = "warn" +mut_mut = "warn" +needless_borrow = "warn" +nonstandard_macro_braces = "warn" +str_to_string = "warn" +todo = "warn" +unused_async = "warn" +redundant_clone = "warn" diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 1b2fdbf6526..09cc6b36c2b 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -15,6 +15,7 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] #![warn(missing_docs, missing_debug_implementations)] pub use matrix_sdk_common::*; diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index d136a2f6bdf..cfad6268a6d 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -15,6 +15,7 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![warn(missing_docs, missing_debug_implementations)] +#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] pub mod backups; mod ciphers; diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index ac35f3f73b4..c45d7cb13d8 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -12,8 +12,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + #![doc = include_str!("../README.md")] #![warn(missing_debug_implementations, missing_docs)] +#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use async_trait::async_trait; diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 4507baf44c1..b833c69f7a5 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -179,8 +179,6 @@ fn check_typos() -> Result<()> { fn check_clippy() -> Result<()> { cmd!("rustup run {NIGHTLY} cargo clippy --all-targets --features testing -- -D warnings") - // Work around https://github.com/rust-lang/cargo/issues/10744 - .env("CARGO_TARGET_APPLIES_TO_HOST", "true") .run()?; cmd!( @@ -190,14 +188,12 @@ fn check_clippy() -> Result<()> { --features native-tls,experimental-sliding-sync,sso-login,testing -- -D warnings" ) - .env("CARGO_TARGET_APPLIES_TO_HOST", "true") .run()?; cmd!( "rustup run {NIGHTLY} cargo clippy --all-targets -p matrix-sdk-crypto --no-default-features -- -D warnings" ) - .env("CARGO_TARGET_APPLIES_TO_HOST", "true") .run()?; Ok(()) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 132335cc57a..1c5a5edfa86 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -68,8 +68,6 @@ fn build_docs( // Keep in sync with .github/workflows/docs.yml cmd!("rustup run {NIGHTLY} cargo doc --no-deps --workspace --features docsrs") - // Work around https://github.com/rust-lang/cargo/issues/10744 - .env("CARGO_TARGET_APPLIES_TO_HOST", "true") .env("RUSTDOCFLAGS", rustdocflags) .args(extra_args) .run()?; From e6db85b7d4a9951b2d600db987076aad50e58a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 13 Oct 2024 21:41:11 +0200 Subject: [PATCH 276/979] chore: Enable the default features for futures-util (#4120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We depend on the `futures_util::steam_select` macro since 9b36a04b. This macro requires the async-await-macros and std feature of futures-util. These features are the default features so let's just stop disabling the default features for futures-util. Signed-off-by: Damir Jelić Co-authored-by: Jonas Platte --- Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a9cdb59105..d648226e1fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,9 +36,7 @@ eyeball-im = { version = "0.5.0", features = ["tracing"] } eyeball-im-util = "0.6.0" futures-core = "0.3.28" futures-executor = "0.3.21" -futures-util = { version = "0.3.26", default-features = false, features = [ - "alloc", -] } +futures-util = "0.3.26" growable-bloom-filter = "2.1.0" http = "1.1.0" imbl = "3.0.0" From 019d198af8e369859507295fce80a7d6f4760109 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 10 Oct 2024 16:09:42 +0100 Subject: [PATCH 277/979] crypto: Sort IdentityStatusChanges when providing them via subscribe_to_identity_status_changes Fixes https://github.com/element-hq/element-meta/issues/2566 --- .../src/identities/room_identity_state.rs | 4 ++-- crates/matrix-sdk/src/room/identity_status_changes.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs index b7f1185160f..312aa67d533 100644 --- a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs +++ b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs @@ -204,7 +204,7 @@ fn state_of(user_identity: &UserIdentity) -> IdentityState { /// A change in the status of the identity of a member of the room. Returned by /// [`RoomIdentityState::process_change`] to indicate that something changed in /// this room and we should either show or hide a warning. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct IdentityStatusChange { /// The user ID of the user whose identity status changed pub user_id: OwnedUserId, @@ -214,7 +214,7 @@ pub struct IdentityStatusChange { } /// The state of an identity - verified, pinned etc. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum IdentityState { /// The user is verified with us diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index feab79e416a..47cbfad79e5 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -88,17 +88,21 @@ impl IdentityStatusChanges { }; Ok(stream!({ - let current_state = + let mut current_state = filter_non_self(state.room_identity_state.current_state(), &own_user_id); + if !current_state.is_empty() { + current_state.sort(); yield current_state; } + while let Some(item) = unprocessed_stream.next().await { - let update = filter_non_self( + let mut update = filter_non_self( state.room_identity_state.process_change(item).await, &own_user_id, ); if !update.is_empty() { + update.sort(); yield update; } } From 92a02a51c40b8c7e01329aaee476464853cdd99d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:28:38 +0000 Subject: [PATCH 278/979] chore(deps): bump crate-ci/typos from 1.25.0 to 1.26.0 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.25.0 to 1.26.0. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.25.0...v1.26.0) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4fac449bf2..f2575b4c6a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -305,7 +305,7 @@ jobs: uses: actions/checkout@v4.2.0 - name: Check the spelling of the files in our repo - uses: crate-ci/typos@v1.25.0 + uses: crate-ci/typos@v1.26.0 clippy: name: Run clippy From 2eca7271ea6a1c8ca263296375c4c7d7b0e3f794 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 06:05:06 +0000 Subject: [PATCH 279/979] chore(deps): bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/bindings_ci.yml | 10 +++++----- .github/workflows/ci.yml | 18 +++++++++--------- .github/workflows/coverage.yml | 2 +- .../workflows/detect-unused-dependencies.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/fixup-block.yml | 2 +- .github/workflows/msrv.yaml | 2 +- .github/workflows/upload_coverage.yml | 2 +- .github/workflows/xtask.yml | 2 +- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 6135378adad..9f6f4e6e2a4 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 04268861f2d..89cdf0a38dd 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install protoc uses: taiki-e/install-action@v2 @@ -69,10 +69,10 @@ jobs: steps: - name: Checkout Rust SDK - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Checkout Kotlin Rust Components project - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 with: repository: matrix-org/matrix-rust-components-kotlin path: rust-components-kotlin @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 # install protoc in case we end up rebuilding opentelemetry-proto - name: Install protoc @@ -191,7 +191,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 # install protoc in case we end up rebuilding opentelemetry-proto - name: Install protoc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2575b4c6a9..84ac2b4ec95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -117,7 +117,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install libsqlite run: | @@ -168,7 +168,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install protoc uses: taiki-e/install-action@v2 @@ -236,7 +236,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -284,7 +284,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -302,7 +302,7 @@ jobs: steps: - name: Checkout Actions Repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Check the spelling of the files in our repo uses: crate-ci/typos@v1.26.0 @@ -314,7 +314,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install protoc uses: taiki-e/install-action@v2 @@ -381,7 +381,7 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install libsqlite run: | diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 296bbdeaf4c..326dfd07cd8 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -58,7 +58,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/detect-unused-dependencies.yml b/.github/workflows/detect-unused-dependencies.yml index d7a7f520300..5282bf02800 100644 --- a/.github/workflows/detect-unused-dependencies.yml +++ b/.github/workflows/detect-unused-dependencies.yml @@ -7,6 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Machete uses: bnjbvr/cargo-machete@main diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index d83dc6eb6cf..4855b00e43d 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Install protoc uses: taiki-e/install-action@v2 diff --git a/.github/workflows/fixup-block.yml b/.github/workflows/fixup-block.yml index 5e5a06547f7..e3f3a5cfc28 100644 --- a/.github/workflows/fixup-block.yml +++ b/.github/workflows/fixup-block.yml @@ -7,6 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.0 + - uses: actions/checkout@v4 - name: Block Fixup Commit Merge uses: 13rac1/block-fixup-merge-action@v2.0.0 diff --git a/.github/workflows/msrv.yaml b/.github/workflows/msrv.yaml index 53c45db644e..46d1485927b 100644 --- a/.github/workflows/msrv.yaml +++ b/.github/workflows/msrv.yaml @@ -16,6 +16,6 @@ jobs: msrv: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.0 + - uses: actions/checkout@v4 - uses: taiki-e/install-action@cargo-hack - run: cargo hack check --rust-version --workspace --all-targets --ignore-private diff --git a/.github/workflows/upload_coverage.yml b/.github/workflows/upload_coverage.yml index 057660dd702..eda15459eed 100644 --- a/.github/workflows/upload_coverage.yml +++ b/.github/workflows/upload_coverage.yml @@ -58,7 +58,7 @@ jobs: echo "override_commit=$(> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 with: ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }} path: repo_root diff --git a/.github/workflows/xtask.yml b/.github/workflows/xtask.yml index 6f369b4cc1c..c4e151448e2 100644 --- a/.github/workflows/xtask.yml +++ b/.github/workflows/xtask.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4 - name: Calculate cache key id: cachekey From cdbfae2aee29e9c1ef3bedd2898a8c211204d377 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 16:12:04 +0200 Subject: [PATCH 280/979] doc(event cache): simplify module comment, as a source file isn't a good todo list All the items have their equivalent sub item in the issue anyways. --- crates/matrix-sdk/src/event_cache/mod.rs | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index b7ff8f321f6..69820e404a8 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -18,26 +18,12 @@ //! doesn't require subscribing to a specific room to get access to this //! information. //! -//! It's intended to be fast, robust and easy to maintain. +//! It's intended to be fast, robust and easy to maintain, having learned from +//! previous endeavours at implementing middle to high level features elsewhere +//! in the SDK, notably in the UI's Timeline object. //! -//! See the [github issue](https://github.com/matrix-org/matrix-rust-sdk/issues/3058) for more details about the historical reasons that led us to start writing this. -//! -//! Most of it is still a work-in-progress, as of 2024-01-22. -//! -//! The desired set of features it may eventually implement is the following: -//! -//! - [ ] compute proper unread room counts, and use backpagination to get -//! missing messages/notifications/mentions, if needs be. -//! - [ ] expose that information with a new data structure similar to the -//! `RoomInfo`, and that may update a `RoomListService`. -//! - [ ] provide read receipts for each message. -//! - [x] backwards pagination -//! - [~] forward pagination -//! - [ ] reconcile results with cached timelines. -//! - [ ] retry decryption upon receiving new keys (from an encryption sync -//! service or from a key backup). -//! - [ ] expose the latest event for a given room. -//! - [ ] caching of events on-disk. +//! See the [github issue](https://github.com/matrix-org/matrix-rust-sdk/issues/3058) for more +//! details about the historical reasons that led us to start writing this. #![forbid(missing_docs)] From 87472e767990e64e3f0d5dea973975b1ea2b7db9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 16:15:18 +0200 Subject: [PATCH 281/979] refactor(event cache): introduce `RoomEventCacheState` for inner mutable state This limits the possibility of race conditions in users of this API. --- crates/matrix-sdk/src/event_cache/mod.rs | 100 +++++++------ .../matrix-sdk/src/event_cache/pagination.rs | 139 +++++++++--------- .../tests/integration/event_cache.rs | 12 +- 3 files changed, 133 insertions(+), 118 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 69820e404a8..db0d6a9f19d 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -58,7 +58,7 @@ use tracing::{error, info_span, instrument, trace, warn, Instrument as _, Span}; use self::{ pagination::RoomPaginationData, - paginator::{Paginator, PaginatorError}, + paginator::PaginatorError, store::{Gap, RoomEvents}, }; use crate::{client::WeakClient, room::WeakRoom, Client}; @@ -342,8 +342,8 @@ struct EventCacheInner { /// be updated, though (e.g. if it was encrypted before, and /// successfully decrypted later). /// - /// This is shared between the [`EventCache`] singleton and all - /// [`RoomEventCache`] instances. + /// This is shared between the [`EventCacheInner`] singleton and all + /// [`RoomEventCacheInner`] instances. all_events: Arc>, /// Handles to keep alive the task listening to updates. @@ -369,9 +369,8 @@ impl EventCacheInner { // Notify all the observers that we've lost track of state. (We ignore the // error if there aren't any.) let _ = room.inner.sender.send(RoomEventCacheUpdate::Clear); - // Clear all the events in memory. - let mut events = room.inner.events.write().await; - room.inner.clear(&mut events).await; + // Clear all the room state. + room.inner.state.write().await.reset(); } } @@ -476,8 +475,8 @@ impl RoomEventCache { pub async fn subscribe( &self, ) -> Result<(Vec, Receiver)> { - let events = - self.inner.events.read().await.events().map(|(_position, item)| item.clone()).collect(); + let state = self.inner.state.read().await; + let events = state.events.events().map(|(_position, item)| item.clone()).collect(); Ok((events, self.inner.sender.subscribe())) } @@ -491,15 +490,15 @@ impl RoomEventCache { /// Try to find an event by id in this room. pub async fn event(&self, event_id: &EventId) -> Option { if let Some((room_id, event)) = - self.inner.all_events_cache.read().await.events.get(event_id).cloned() + self.inner.all_events.read().await.events.get(event_id).cloned() { if room_id == self.inner.room_id { return Some(event); } } - let events = self.inner.events.read().await; - for (_pos, event) in events.revents() { + let state = self.inner.state.read().await; + for (_pos, event) in state.events.revents() { if event.event_id().as_deref() == Some(event_id) { return Some(event.clone()); } @@ -518,7 +517,7 @@ impl RoomEventCache { ) -> Option<(SyncTimelineEvent, Vec)> { let mut relation_events = Vec::new(); - let cache = self.inner.all_events_cache.read().await; + let cache = self.inner.all_events.read().await; if let Some((_, event)) = cache.events.get(event_id) { Self::collect_related_events(&cache, event_id, &filter, &mut relation_events); Some((event.clone(), relation_events)) @@ -567,7 +566,7 @@ impl RoomEventCache { // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. pub(crate) async fn save_event(&self, event: SyncTimelineEvent) { if let Some(event_id) = event.event_id() { - let mut cache = self.inner.all_events_cache.write().await; + let mut cache = self.inner.all_events.write().await; self.inner.append_related_event(&mut cache, &event); cache.events.insert(event_id, (self.inner.room_id.clone(), event)); @@ -583,7 +582,7 @@ impl RoomEventCache { // there'll be no distinction between the linked chunk and the separate // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. pub(crate) async fn save_events(&self, events: impl IntoIterator) { - let mut cache = self.inner.all_events_cache.write().await; + let mut cache = self.inner.all_events.write().await; for event in events { if let Some(event_id) = event.event_id() { self.inner.append_related_event(&mut cache, &event); @@ -595,6 +594,29 @@ impl RoomEventCache { } } +/// State for a single room's event cache. +/// +/// This contains all inner mutable state that ought to be updated at the same +/// time. +struct RoomEventCacheState { + /// The events of the room. + events: RoomEvents, + + /// Have we ever waited for a previous-batch-token to come from sync, in the + /// context of pagination? We do this at most once per room, the first + /// time we try to run backward pagination. We reset that upon clearing + /// the timeline events. + waited_for_initial_prev_token: bool, +} + +impl RoomEventCacheState { + /// Resets this data structure as if it were brand new. + fn reset(&mut self) { + self.events.reset(); + self.waited_for_initial_prev_token = false; + } +} + /// The (non-cloneable) details of the `RoomEventCache`. struct RoomEventCacheInner { /// The room id for this room. @@ -603,11 +625,14 @@ struct RoomEventCacheInner { /// Sender part for subscribers to this room. sender: Sender, - /// The events of the room. - events: RwLock, + /// State for this room's event cache. + state: RwLock, - /// See comment of [`EventCacheInner::events`]. - all_events_cache: Arc>, + /// See comment of [`EventCacheInner::all_events`]. + /// + /// This is shared between the [`EventCacheInner`] singleton and all + /// [`RoomEventCacheInner`] instances. + all_events: Arc>, /// A paginator instance, that's configured to run back-pagination on our /// behalf. @@ -617,9 +642,6 @@ struct RoomEventCacheInner { /// events received from those kinds of pagination with the cache. This /// paginator is only used for queries that interact with the actual event /// cache. - /// - /// It's protected behind a lock to avoid multiple accesses to the paginator - /// at the same time. pagination: RoomPaginationData, } @@ -637,24 +659,16 @@ impl RoomEventCacheInner { Self { room_id: weak_room.room_id().to_owned(), - events: RwLock::new(RoomEvents::default()), - all_events_cache, + state: RwLock::new(RoomEventCacheState { + events: RoomEvents::default(), + waited_for_initial_prev_token: false, + }), + all_events: all_events_cache, sender, - pagination: RoomPaginationData { - paginator: Paginator::new(weak_room), - waited_for_initial_prev_token: Mutex::new(false), - token_notifier: Default::default(), - }, + pagination: RoomPaginationData::new(weak_room), } } - async fn clear(&self, room_events: &mut RwLockWriteGuard<'_, RoomEvents>) { - room_events.reset(); - - // Reset the back-pagination state to the initial too. - *self.pagination.waited_for_initial_prev_token.lock().await = false; - } - fn handle_account_data(&self, account_data: Vec>) { let mut handled_read_marker = false; @@ -755,17 +769,17 @@ impl RoomEventCacheInner { ambiguity_changes: BTreeMap, ) -> Result<()> { // Acquire the lock. - let mut room_events = self.events.write().await; + let mut state = self.state.write().await; // Reset the room's state. - self.clear(&mut room_events).await; + state.reset(); // Propagate to observers. let _ = self.sender.send(RoomEventCacheUpdate::Clear); // Push the new events. self.append_events_locked_impl( - room_events, + &mut state.events, sync_timeline_events, prev_batch.clone(), ephemeral_events, @@ -789,7 +803,7 @@ impl RoomEventCacheInner { ambiguity_changes: BTreeMap, ) -> Result<()> { self.append_events_locked_impl( - self.events.write().await, + &mut self.state.write().await.events, sync_timeline_events, prev_batch, ephemeral_events, @@ -869,14 +883,12 @@ impl RoomEventCacheInner { } } - /// Append a set of events, with an attached lock. - /// - /// If the lock `room_events` is `None`, one will be created. + /// Append a set of events and associated room data. /// /// This is a private implementation. It must not be exposed publicly. async fn append_events_locked_impl( &self, - mut room_events: RwLockWriteGuard<'_, RoomEvents>, + room_events: &mut RoomEvents, sync_timeline_events: Vec, prev_batch: Option, ephemeral_events: Vec>, @@ -899,7 +911,7 @@ impl RoomEventCacheInner { room_events.push_events(sync_timeline_events.clone()); - let mut cache = self.all_events_cache.write().await; + let mut cache = self.all_events.write().await; for ev in &sync_timeline_events { if let Some(event_id) = ev.event_id() { self.append_related_event(&mut cache, ev); diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index f29f84ceb7b..ce0a8e3dde4 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -18,10 +18,7 @@ use std::{future::Future, ops::ControlFlow, sync::Arc, time::Duration}; use eyeball::Subscriber; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; -use tokio::{ - sync::{Mutex, Notify, RwLockReadGuard}, - time::timeout, -}; +use tokio::{sync::Notify, time::timeout}; use tracing::{debug, instrument, trace}; use super::{ @@ -38,11 +35,12 @@ pub(super) struct RoomPaginationData { /// The stateful paginator instance used for the integrated pagination. pub paginator: Paginator, +} - /// Have we ever waited for a previous-batch-token to come from sync? We do - /// this at most once per room, the first time we try to run backward - /// pagination. We reset that upon clearing the timeline events. - pub waited_for_initial_prev_token: Mutex, +impl RoomPaginationData { + pub(super) fn new(room: PR) -> Self { + Self { token_notifier: Default::default(), paginator: Paginator::new(room) } + } } /// An API object to run pagination queries on a [`super::RoomEventCache`]. @@ -133,7 +131,9 @@ impl RoomPagination { } async fn run_backwards_impl(&self, batch_size: u16) -> Result> { - let prev_token = self.get_or_wait_for_token().await; + const DEFAULT_WAIT_FOR_TOKEN_DURATION: Duration = Duration::from_secs(3); + + let prev_token = self.get_or_wait_for_token(Some(DEFAULT_WAIT_FOR_TOKEN_DURATION)).await; let paginator = &self.inner.pagination.paginator; @@ -145,7 +145,8 @@ impl RoomPagination { // Make sure the `RoomEvents` isn't updated while we are saving events from // backpagination. - let mut room_events = self.inner.events.write().await; + let mut state = self.inner.state.write().await; + let room_events = &mut state.events; // Check that the previous token still exists; otherwise it's a sign that the // room's timeline has been cleared. @@ -248,49 +249,45 @@ impl RoomPagination { } /// Get the latest pagination token, as stored in the room events linked - /// list. - #[doc(hidden)] - pub async fn get_or_wait_for_token(&self) -> Option { - const DEFAULT_INITIAL_WAIT_DURATION: Duration = Duration::from_secs(3); - - let waited = *self.inner.pagination.waited_for_initial_prev_token.lock().await; - if waited { - self.oldest_token(None).await - } else { - let token = self.oldest_token(Some(DEFAULT_INITIAL_WAIT_DURATION)).await; - *self.inner.pagination.waited_for_initial_prev_token.lock().await = true; - token - } - } - - /// Returns the oldest back-pagination token, that is, the one closest to - /// the start of the timeline as we know it. + /// list, or wait for it for the given amount of time. /// - /// Optionally, wait at most for the given duration for a back-pagination - /// token to be returned by a sync. - async fn oldest_token(&self, max_wait: Option) -> Option { - // Optimistically try to return the backpagination token immediately. - fn get_oldest(room_events: RwLockReadGuard<'_, RoomEvents>) -> Option { - room_events.chunks().find_map(|chunk| match chunk.content() { + /// It will only wait if we *never* saw an initial previous-batch token. + /// Otherwise, it will immediately skip. + #[doc(hidden)] + pub async fn get_or_wait_for_token(&self, wait_time: Option) -> Option { + fn get_oldest(events: &RoomEvents) -> Option { + events.chunks().find_map(|chunk| match chunk.content() { ChunkContent::Gap(gap) => Some(gap.prev_token.clone()), ChunkContent::Items(..) => None, }) } - if let Some(token) = get_oldest(self.inner.events.read().await) { - return Some(token); + { + // Scope for the lock guard. + let state = self.inner.state.read().await; + // Fast-path: we do have a previous-batch token already. + if let Some(found) = get_oldest(&state.events) { + return Some(found); + } + // If we've already waited for an initial previous-batch token before, + // immediately abort. + if state.waited_for_initial_prev_token { + return None; + } } - let Some(max_wait) = max_wait else { - // We had no token and no time to wait, so… no tokens. - return None; - }; + // If the caller didn't set a wait time, return none early. + let wait_time = wait_time?; - // Otherwise wait for a notification that we received a token. - // Timeouts are fine, per this function's contract. - let _ = timeout(max_wait, self.inner.pagination.token_notifier.notified()).await; + // Otherwise, wait for a notification that we received a previous-batch token. + // Note the state lock is released while doing so, allowing other tasks to write + // into the linked chunk. + let _ = timeout(wait_time, self.inner.pagination.token_notifier.notified()).await; - get_oldest(self.inner.events.read().await) + let mut state = self.inner.state.write().await; + let token = get_oldest(&state.events); + state.waited_for_initial_prev_token = true; + token } /// Returns a subscriber to the pagination status used for the @@ -353,37 +350,40 @@ mod tests { let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap(); // When I only have events in a room, - { - let mut room_events = room_event_cache.inner.events.write().await; - room_events.push_events([sync_timeline_event!({ - "sender": "b@z.h", - "type": "m.room.message", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content": { "body":"yolo", "msgtype": "m.text" }, - }) - .into()]); - } + room_event_cache.inner.state.write().await.events.push_events([sync_timeline_event!({ + "sender": "b@z.h", + "type": "m.room.message", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content": { "body":"yolo", "msgtype": "m.text" }, + }) + .into()]); let pagination = room_event_cache.pagination(); // If I don't wait for the backpagination token, - let found = pagination.oldest_token(None).await; + let found = pagination.get_or_wait_for_token(None).await; // Then I don't find it. assert!(found.is_none()); + // Reset waited_for_initial_prev_token state. + pagination.inner.state.write().await.reset(); + // If I wait for a back-pagination token for 0 seconds, let before = Instant::now(); - let found = pagination.oldest_token(Some(Duration::default())).await; + let found = pagination.get_or_wait_for_token(Some(Duration::default())).await; let waited = before.elapsed(); // then I don't get any, assert!(found.is_none()); // and I haven't waited long. assert!(waited.as_secs() < 1); + // Reset waited_for_initial_prev_token state. + pagination.inner.state.write().await.reset(); + // If I wait for a back-pagination token for 1 second, let before = Instant::now(); - let found = pagination.oldest_token(Some(Duration::from_secs(1))).await; + let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await; let waited = before.elapsed(); // then I still don't get any. assert!(found.is_none()); @@ -408,7 +408,7 @@ mod tests { // When I have events and multiple gaps, in a room, { - let mut room_events = room_event_cache.inner.events.write().await; + let room_events = &mut room_event_cache.inner.state.write().await.events; room_events.push_gap(Gap { prev_token: expected_token.clone() }); room_events.push_events([sync_timeline_event!({ "sender": "b@z.h", @@ -420,16 +420,16 @@ mod tests { .into()]); } - let paginator = room_event_cache.pagination(); + let pagination = room_event_cache.pagination(); // If I don't wait for a back-pagination token, - let found = paginator.oldest_token(None).await; + let found = pagination.get_or_wait_for_token(None).await; // Then I get it. assert_eq!(found.as_ref(), Some(&expected_token)); // If I wait for a back-pagination token for 0 seconds, let before = Instant::now(); - let found = paginator.oldest_token(Some(Duration::default())).await; + let found = pagination.get_or_wait_for_token(Some(Duration::default())).await; let waited = before.elapsed(); // then I do get one. assert_eq!(found.as_ref(), Some(&expected_token)); @@ -438,7 +438,7 @@ mod tests { // If I wait for a back-pagination token for 1 second, let before = Instant::now(); - let found = paginator.oldest_token(Some(Duration::from_secs(1))).await; + let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await; let waited = before.elapsed(); // then I do get one. assert_eq!(found, Some(expected_token)); @@ -467,20 +467,23 @@ mod tests { // If a backpagination token is inserted after 400 milliseconds, sleep(Duration::from_millis(400)).await; - { - let mut room_events = cloned_room_event_cache.inner.events.write().await; - room_events.push_gap(Gap { prev_token: cloned_expected_token }); - } + cloned_room_event_cache + .inner + .state + .write() + .await + .events + .push_gap(Gap { prev_token: cloned_expected_token }); }); let pagination = room_event_cache.pagination(); // Then first I don't get it (if I'm not waiting,) - let found = pagination.oldest_token(None).await; + let found = pagination.get_or_wait_for_token(None).await; assert!(found.is_none()); // And if I wait for the back-pagination token for 600ms, - let found = pagination.oldest_token(Some(Duration::from_millis(600))).await; + let found = pagination.get_or_wait_for_token(Some(Duration::from_millis(600))).await; let waited = before.elapsed(); // then I do get one eventually. diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index e28185a8d26..40c03c67d79 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -339,7 +339,7 @@ async fn test_backpaginate_once() { // Then if I backpaginate, let pagination = room_event_cache.pagination(); - assert!(pagination.get_or_wait_for_token().await.is_some()); + assert!(pagination.get_or_wait_for_token(None).await.is_some()); pagination.run_backwards(20, once).await.unwrap() }; @@ -428,7 +428,7 @@ async fn test_backpaginate_many_times_with_many_iterations() { // Then if I backpaginate in a loop, let pagination = room_event_cache.pagination(); - while pagination.get_or_wait_for_token().await.is_some() { + while pagination.get_or_wait_for_token(None).await.is_some() { pagination .run_backwards(20, |outcome, timeline_has_been_reset| { num_paginations += 1; @@ -544,7 +544,7 @@ async fn test_backpaginate_many_times_with_one_iteration() { // Then if I backpaginate in a loop, let pagination = room_event_cache.pagination(); - while pagination.get_or_wait_for_token().await.is_some() { + while pagination.get_or_wait_for_token(None).await.is_some() { pagination .run_backwards(20, |outcome, timeline_has_been_reset| { num_paginations += 1; @@ -691,7 +691,7 @@ async fn test_reset_while_backpaginating() { // Run the pagination! let pagination = room_event_cache.pagination(); - let first_token = pagination.get_or_wait_for_token().await; + let first_token = pagination.get_or_wait_for_token(None).await; assert!(first_token.is_some()); let backpagination = spawn({ @@ -721,7 +721,7 @@ async fn test_reset_while_backpaginating() { assert!(!events.is_empty()); // Now if we retrieve the oldest token, it's set to something else. - let second_token = pagination.get_or_wait_for_token().await.unwrap(); + let second_token = pagination.get_or_wait_for_token(None).await.unwrap(); assert!(first_token.unwrap() != second_token); assert_eq!(second_token, "third_backpagination"); } @@ -774,7 +774,7 @@ async fn test_backpaginating_without_token() { // We don't have a token. let pagination = room_event_cache.pagination(); - assert!(pagination.get_or_wait_for_token().await.is_none()); + assert!(pagination.get_or_wait_for_token(None).await.is_none()); // If we try to back-paginate with a token, it will hit the end of the timeline // and give us the resulting event. From 1018d71bb793aa01f3b7a81eb6589f358074cdb3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 17:05:35 +0200 Subject: [PATCH 282/979] refactor(event cache): get rid of the `RoomPaginationData` data structure It only contained two fields, and it avoids one extra level of cognitive overhead and makes the type hierarchy flatter. --- crates/matrix-sdk/src/event_cache/mod.rs | 17 ++++++----- .../matrix-sdk/src/event_cache/pagination.rs | 29 +++++-------------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index db0d6a9f19d..391c6d71012 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -39,7 +39,7 @@ use matrix_sdk_base::{ sync::{JoinedRoomUpdate, LeftRoomUpdate, RoomUpdates, Timeline}, }; use matrix_sdk_common::executor::{spawn, JoinHandle}; -use paginator::PaginatorState; +use paginator::{Paginator, PaginatorState}; use ruma::{ events::{ relation::RelationType, @@ -52,12 +52,11 @@ use ruma::{ }; use tokio::sync::{ broadcast::{error::RecvError, Receiver, Sender}, - Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard, + Mutex, Notify, RwLock, RwLockReadGuard, RwLockWriteGuard, }; use tracing::{error, info_span, instrument, trace, warn, Instrument as _, Span}; use self::{ - pagination::RoomPaginationData, paginator::PaginatorError, store::{Gap, RoomEvents}, }; @@ -634,6 +633,9 @@ struct RoomEventCacheInner { /// [`RoomEventCacheInner`] instances. all_events: Arc>, + /// A notifier that we received a new pagination token. + pub pagination_batch_token_notifier: Notify, + /// A paginator instance, that's configured to run back-pagination on our /// behalf. /// @@ -642,7 +644,7 @@ struct RoomEventCacheInner { /// events received from those kinds of pagination with the cache. This /// paginator is only used for queries that interact with the actual event /// cache. - pagination: RoomPaginationData, + pub paginator: Paginator, } impl RoomEventCacheInner { @@ -665,7 +667,8 @@ impl RoomEventCacheInner { }), all_events: all_events_cache, sender, - pagination: RoomPaginationData::new(weak_room), + pagination_batch_token_notifier: Default::default(), + paginator: Paginator::new(weak_room), } } @@ -788,7 +791,7 @@ impl RoomEventCacheInner { .await?; // Reset the paginator status to initial. - self.pagination.paginator.set_idle_state(PaginatorState::Initial, prev_batch, None)?; + self.paginator.set_idle_state(PaginatorState::Initial, prev_batch, None)?; Ok(()) } @@ -923,7 +926,7 @@ impl RoomEventCacheInner { // Now that all events have been added, we can trigger the // `pagination_token_notifier`. if prev_batch.is_some() { - self.pagination.token_notifier.notify_one(); + self.pagination_batch_token_notifier.notify_one(); } // The order of `RoomEventCacheUpdate`s is **really** important here. diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index ce0a8e3dde4..03509371102 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -18,31 +18,16 @@ use std::{future::Future, ops::ControlFlow, sync::Arc, time::Duration}; use eyeball::Subscriber; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; -use tokio::{sync::Notify, time::timeout}; +use tokio::time::timeout; use tracing::{debug, instrument, trace}; use super::{ - paginator::{PaginableRoom, PaginationResult, Paginator, PaginatorState}, + paginator::{PaginationResult, PaginatorState}, store::Gap, BackPaginationOutcome, Result, RoomEventCacheInner, }; use crate::event_cache::{linked_chunk::ChunkContent, store::RoomEvents}; -#[derive(Debug)] -pub(super) struct RoomPaginationData { - /// A notifier that we received a new pagination token. - pub token_notifier: Notify, - - /// The stateful paginator instance used for the integrated pagination. - pub paginator: Paginator, -} - -impl RoomPaginationData { - pub(super) fn new(room: PR) -> Self { - Self { token_notifier: Default::default(), paginator: Paginator::new(room) } - } -} - /// An API object to run pagination queries on a [`super::RoomEventCache`]. /// /// Can be created with [`super::RoomEventCache::pagination()`]. @@ -135,7 +120,7 @@ impl RoomPagination { let prev_token = self.get_or_wait_for_token(Some(DEFAULT_WAIT_FOR_TOKEN_DURATION)).await; - let paginator = &self.inner.pagination.paginator; + let paginator = &self.inner.paginator; paginator.set_idle_state(PaginatorState::Idle, prev_token.clone(), None)?; @@ -282,7 +267,7 @@ impl RoomPagination { // Otherwise, wait for a notification that we received a previous-batch token. // Note the state lock is released while doing so, allowing other tasks to write // into the linked chunk. - let _ = timeout(wait_time, self.inner.pagination.token_notifier.notified()).await; + let _ = timeout(wait_time, self.inner.pagination_batch_token_notifier.notified()).await; let mut state = self.inner.state.write().await; let token = get_oldest(&state.events); @@ -293,7 +278,7 @@ impl RoomPagination { /// Returns a subscriber to the pagination status used for the /// back-pagination integrated to the event cache. pub fn status(&self) -> Subscriber { - self.inner.pagination.paginator.state() + self.inner.paginator.state() } /// Returns whether we've hit the start of the timeline. @@ -301,7 +286,7 @@ impl RoomPagination { /// This is true if, and only if, we didn't have a previous-batch token and /// running backwards pagination would be useless. pub fn hit_timeline_start(&self) -> bool { - self.inner.pagination.paginator.hit_timeline_start() + self.inner.paginator.hit_timeline_start() } /// Returns whether we've hit the end of the timeline. @@ -309,7 +294,7 @@ impl RoomPagination { /// This is true if, and only if, we didn't have a next-batch token and /// running forwards pagination would be useless. pub fn hit_timeline_end(&self) -> bool { - self.inner.pagination.paginator.hit_timeline_end() + self.inner.paginator.hit_timeline_end() } } From 6dd2e3becf0a8e41c43e8f4cbb994e393cb9ba07 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 10 Oct 2024 17:12:30 +0200 Subject: [PATCH 283/979] refactor(event cache): use a single mutex for the prev and next batch pagination tokens --- .../matrix-sdk/src/event_cache/paginator.rs | 91 ++++++++++++------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/paginator.rs b/crates/matrix-sdk/src/event_cache/paginator.rs index cbeb21268f0..259074d2e88 100644 --- a/crates/matrix-sdk/src/event_cache/paginator.rs +++ b/crates/matrix-sdk/src/event_cache/paginator.rs @@ -90,6 +90,15 @@ impl From> for PaginationToken { } } +/// Paginations tokens used for backward and forward pagination. +#[derive(Debug)] +struct PaginationTokens { + /// Pagination token used for backward pagination. + previous: PaginationToken, + /// Pagination token used for forward pagination. + next: PaginationToken, +} + /// A stateful object to reach to an event, and then paginate backward and /// forward from it. /// @@ -101,15 +110,10 @@ pub struct Paginator { /// Current state of the paginator. state: SharedObservable, - /// The token to run the next backward pagination. + /// Pagination tokens used for subsequent requests. /// - /// This mutex is only taken for short periods of time, so it's sync. - prev_batch_token: Mutex, - - /// The token to run the next forward pagination. - /// - /// This mutex is only taken for short periods of time, so it's sync. - next_batch_token: Mutex, + /// This mutex is always short-lived, so it's sync. + tokens: Mutex, } #[cfg(not(tarpaulin_include))] @@ -118,8 +122,7 @@ impl std::fmt::Debug for Paginator { // Don't include the room in the debug output. f.debug_struct("Paginator") .field("state", &self.state.get()) - .field("prev_batch_token", &self.prev_batch_token) - .field("next_batch_token", &self.next_batch_token) + .field("tokens", &self.tokens) .finish_non_exhaustive() } } @@ -192,8 +195,7 @@ impl Paginator { Self { room, state: SharedObservable::new(PaginatorState::Initial), - prev_batch_token: Mutex::new(None.into()), - next_batch_token: Mutex::new(None.into()), + tokens: Mutex::new(PaginationTokens { previous: None.into(), next: None.into() }), } } @@ -245,15 +247,19 @@ impl Paginator { } self.state.set_if_not_eq(next_state); - *self.prev_batch_token.lock().unwrap() = prev_batch_token.into(); - *self.next_batch_token.lock().unwrap() = next_batch_token.into(); + + { + let mut tokens = self.tokens.lock().unwrap(); + tokens.previous = prev_batch_token.into(); + tokens.next = next_batch_token.into(); + } Ok(()) } /// Returns the current previous batch token, as stored in this paginator. pub(super) fn prev_batch_token(&self) -> Option { - match &*self.prev_batch_token.lock().unwrap() { + match &self.tokens.lock().unwrap().previous { PaginationToken::HitEnd | PaginationToken::None => None, PaginationToken::HasMore(token) => Some(token.clone()), } @@ -296,14 +302,17 @@ impl Paginator { let has_prev = response.prev_batch_token.is_some(); let has_next = response.next_batch_token.is_some(); - *self.prev_batch_token.lock().unwrap() = match response.prev_batch_token { - Some(token) => PaginationToken::HasMore(token), - None => PaginationToken::HitEnd, - }; - *self.next_batch_token.lock().unwrap() = match response.next_batch_token { - Some(token) => PaginationToken::HasMore(token), - None => PaginationToken::HitEnd, - }; + { + let mut tokens = self.tokens.lock().unwrap(); + tokens.previous = match response.prev_batch_token { + Some(token) => PaginationToken::HasMore(token), + None => PaginationToken::HitEnd, + }; + tokens.next = match response.next_batch_token { + Some(token) => PaginationToken::HasMore(token), + None => PaginationToken::HitEnd, + }; + } // Forget the reset state guard, so its Drop method is not called. reset_state_guard.disarm(); @@ -339,7 +348,7 @@ impl Paginator { &self, num_events: UInt, ) -> Result { - self.paginate(Direction::Backward, num_events, &self.prev_batch_token).await + self.paginate(Direction::Backward, num_events).await } /// Returns whether we've hit the start of the timeline. @@ -347,7 +356,7 @@ impl Paginator { /// This is true if, and only if, we didn't have a previous-batch token and /// running backwards pagination would be useless. pub fn hit_timeline_start(&self) -> bool { - matches!(*self.prev_batch_token.lock().unwrap(), PaginationToken::HitEnd) + matches!(self.tokens.lock().unwrap().previous, PaginationToken::HitEnd) } /// Returns whether we've hit the end of the timeline. @@ -355,7 +364,7 @@ impl Paginator { /// This is true if, and only if, we didn't have a next-batch token and /// running forwards pagination would be useless. pub fn hit_timeline_end(&self) -> bool { - matches!(*self.next_batch_token.lock().unwrap(), PaginationToken::HitEnd) + matches!(self.tokens.lock().unwrap().next, PaginationToken::HitEnd) } /// Runs a forward pagination (requesting `num_events` to the server), from @@ -369,7 +378,7 @@ impl Paginator { &self, num_events: UInt, ) -> Result { - self.paginate(Direction::Forward, num_events, &self.next_batch_token).await + self.paginate(Direction::Forward, num_events).await } /// Paginate in the given direction, requesting `num_events` events to the @@ -379,13 +388,18 @@ impl Paginator { &self, dir: Direction, num_events: UInt, - token_lock: &Mutex, ) -> Result { self.check_state(PaginatorState::Idle)?; let token = { - let token = token_lock.lock().unwrap(); - match &*token { + let tokens = self.tokens.lock().unwrap(); + + let token = match dir { + Direction::Backward => &tokens.previous, + Direction::Forward => &tokens.next, + }; + + match token { PaginationToken::None => None, PaginationToken::HasMore(val) => Some(val.clone()), PaginationToken::HitEnd => { @@ -419,10 +433,19 @@ impl Paginator { let hit_end_of_timeline = response.end.is_none(); - *token_lock.lock().unwrap() = match response.end { - Some(val) => PaginationToken::HasMore(val), - None => PaginationToken::HitEnd, - }; + { + let mut tokens = self.tokens.lock().unwrap(); + + let token = match dir { + Direction::Backward => &mut tokens.previous, + Direction::Forward => &mut tokens.next, + }; + + *token = match response.end { + Some(val) => PaginationToken::HasMore(val), + None => PaginationToken::HitEnd, + }; + } // TODO: what to do with state events? From 0b57ef4bf62afcde256d6f9527f74ef95575134b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 9 Oct 2024 12:43:22 +0200 Subject: [PATCH 284/979] sdk: Remove room from m.direct account data in Room::forget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/room/mod.rs | 10 +++ .../matrix-sdk/tests/integration/room/left.rs | 76 ++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 1a5287abd98..0db017a3a1a 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2653,6 +2653,16 @@ impl Room { let request = forget_room::v3::Request::new(self.inner.room_id().to_owned()); let _response = self.client.send(request, None).await?; + + // If it was a DM, remove the room from the `m.direct` global account data. + if self.inner.direct_targets_length() != 0 { + if let Err(e) = self.set_is_direct(false).await { + // It is not important whether we managed to remove the room, it will not have + // any consequences, so just log the error. + warn!(room_id = ?self.room_id(), "failed to remove room from m.direct account data: {e}"); + } + } + self.client.store().remove_room(self.inner.room_id()).await?; Ok(()) diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs index fb359820cab..e9fef434c23 100644 --- a/crates/matrix-sdk/tests/integration/room/left.rs +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -1,25 +1,41 @@ use std::time::Duration; +use assert_matches2::assert_matches; use matrix_sdk::config::SyncSettings; use matrix_sdk_base::RoomState; -use matrix_sdk_test::{async_test, test_json, DEFAULT_TEST_ROOM_ID}; -use ruma::OwnedRoomOrAliasId; +use matrix_sdk_test::{ + async_test, test_json, GlobalAccountDataTestEvent, LeftRoomBuilder, SyncResponseBuilder, + DEFAULT_TEST_ROOM_ID, +}; +use ruma::{events::direct::DirectEventContent, user_id, OwnedRoomOrAliasId}; use serde_json::json; use wiremock::{ - matchers::{header, method, path_regex}, + matchers::{header, method, path, path_regex}, Mock, ResponseTemplate, }; use crate::{logged_in_client_with_server, mock_sync}; #[async_test] -async fn test_forget_room() { +async fn test_forget_non_direct_room() { let (client, server) = logged_in_client_with_server().await; + let user_id = client.user_id().unwrap(); Mock::given(method("POST")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/forget$")) .and(header("authorization", "Bearer 1234")) .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .named("forget") + .expect(1) + .mount(&server) + .await; + + Mock::given(method("PUT")) + .and(path(format!("/_matrix/client/r0/user/{user_id}/account_data/m.direct"))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .named("set_mdirect") + .expect(0) .mount(&server) .await; @@ -34,6 +50,58 @@ async fn test_forget_room() { room.forget().await.unwrap(); } +#[async_test] +async fn test_forget_direct_room() { + let (client, server) = logged_in_client_with_server().await; + let user_id = client.user_id().unwrap(); + let invited_user_id = user_id!("@invited:localhost"); + + // Initialize the direct room. + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_left_room(LeftRoomBuilder::default()); + sync_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Direct); + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + assert_eq!(room.state(), RoomState::Left); + assert!(room.is_direct().await.unwrap()); + assert!(room.direct_targets().contains(invited_user_id)); + + let direct_account_data = client + .account() + .account_data::() + .await + .expect("getting m.direct account data failed") + .expect("no m.direct account data") + .deserialize() + .expect("failed to deserialize m.direct account data"); + assert_matches!(direct_account_data.get(invited_user_id), Some(invited_user_dms)); + assert_eq!(invited_user_dms, &[DEFAULT_TEST_ROOM_ID.to_owned()]); + + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/forget$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .named("forget") + .expect(1) + .mount(&server) + .await; + + Mock::given(method("PUT")) + .and(path(format!("/_matrix/client/r0/user/{user_id}/account_data/m.direct"))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)) + .named("set_mdirect") + .expect(1) + .mount(&server) + .await; + + room.forget().await.unwrap(); +} + #[async_test] async fn test_rejoin_room() { let (client, server) = logged_in_client_with_server().await; From ee4ef2eb539f25d69ea3582a80646e467adceafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 9 Oct 2024 13:16:50 +0200 Subject: [PATCH 285/979] sdk: Remove room from in-memory list when calling Room::forget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk-base/src/client.rs | 11 ++++ crates/matrix-sdk-base/src/store/mod.rs | 11 ++++ .../src/store/observable_map.rs | 56 +++++++++++++++++++ crates/matrix-sdk/src/room/mod.rs | 2 +- .../matrix-sdk/tests/integration/room/left.rs | 4 ++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 35ec906b855..e39587455ab 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1408,6 +1408,17 @@ impl BaseClient { self.store.room(room_id) } + /// Forget the room with the given room ID. + /// + /// The room will be dropped from the room list and the store. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room that should be forgotten. + pub async fn forget_room(&self, room_id: &RoomId) -> StoreResult<()> { + self.store.forget_room(room_id).await + } + /// Get the olm machine. #[cfg(feature = "e2e-encryption")] pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option> { diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 9c774a769a0..471a17c6717 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -304,6 +304,17 @@ impl Store { }) .clone() } + + /// Forget the room with the given room ID. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room that should be forgotten. + pub(crate) async fn forget_room(&self, room_id: &RoomId) -> Result<()> { + self.inner.remove_room(room_id).await?; + self.rooms.write().unwrap().remove(room_id); + Ok(()) + } } #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk-base/src/store/observable_map.rs b/crates/matrix-sdk-base/src/store/observable_map.rs index e15124a9520..1650d08a160 100644 --- a/crates/matrix-sdk-base/src/store/observable_map.rs +++ b/crates/matrix-sdk-base/src/store/observable_map.rs @@ -130,6 +130,18 @@ mod impl_non_wasm32 { pub(crate) fn stream(&self) -> (Vector, impl Stream>>) { self.values.subscribe().into_values_and_batched_stream() } + + /// Remove a `V` value based on their ID, if it exists. + /// + /// Returns the removed value. + pub(crate) fn remove(&mut self, key: &L) -> Option + where + K: Borrow, + L: Hash + Eq + ?Sized, + { + let position = self.mapping.remove(key)?; + Some(self.values.remove(position)) + } } } @@ -184,6 +196,17 @@ mod impl_wasm32 { pub(crate) fn iter(&self) -> impl Iterator { self.0.values() } + + /// Remove a `V` value based on their ID, if it exists. + /// + /// Returns the removed value. + pub(crate) fn remove(&mut self, key: &L) -> Option + where + K: Borrow, + L: Hash + Eq + Ord + ?Sized, + { + self.0.remove(key) + } } } @@ -249,6 +272,33 @@ mod tests { assert_eq!(map.get(&'c'), Some(&'G')); } + #[test] + fn test_remove() { + let mut map = ObservableMap::::new(); + + assert!(map.get(&'a').is_none()); + assert!(map.get(&'b').is_none()); + assert!(map.get(&'c').is_none()); + + // new items + map.insert('a', 'e'); + map.insert('b', 'f'); + + assert_eq!(map.get(&'a'), Some(&'e')); + assert_eq!(map.get(&'b'), Some(&'f')); + assert!(map.get(&'c').is_none()); + + // remove one item + assert_eq!(map.remove(&'b'), Some('f')); + + assert_eq!(map.get(&'a'), Some(&'e')); + assert_eq!(map.get(&'b'), None); + assert_eq!(map.get(&'c'), None); + + // remove a non-existent item + assert_eq!(map.remove(&'c'), None); + } + #[test] fn test_iter() { let mut map = ObservableMap::::new(); @@ -293,6 +343,12 @@ mod tests { assert_pending!(stream); + // remove one item + map.remove(&'b'); + assert_next_eq!(stream, vec![VectorDiff::Remove { index: 0 }]); + + assert_pending!(stream); + drop(map); assert_closed!(stream); } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 0db017a3a1a..cdcc8bfad93 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2663,7 +2663,7 @@ impl Room { } } - self.client.store().remove_room(self.inner.room_id()).await?; + self.client.base_client().forget_room(self.inner.room_id()).await?; Ok(()) } diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs index e9fef434c23..13042ae1ba6 100644 --- a/crates/matrix-sdk/tests/integration/room/left.rs +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -48,6 +48,8 @@ async fn test_forget_non_direct_room() { assert_eq!(room.state(), RoomState::Left); room.forget().await.unwrap(); + + assert!(client.get_room(&DEFAULT_TEST_ROOM_ID).is_none()); } #[async_test] @@ -100,6 +102,8 @@ async fn test_forget_direct_room() { .await; room.forget().await.unwrap(); + + assert!(client.get_room(&DEFAULT_TEST_ROOM_ID).is_none()); } #[async_test] From 9999d3ba96c882974f136393a4ef50f39a39e1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 16 Oct 2024 10:28:56 +0200 Subject: [PATCH 286/979] chore(sdk)!: Remove image-proc feature and functions to generate a thumbnail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .github/workflows/ci.yml | 1 - .github/workflows/coverage.yml | 2 +- Cargo.lock | 1 - crates/matrix-sdk/CHANGELOG.md | 4 - crates/matrix-sdk/Cargo.toml | 10 +- crates/matrix-sdk/README.md | 2 - crates/matrix-sdk/src/attachment.rs | 174 ------------- crates/matrix-sdk/src/error.rs | 26 -- crates/matrix-sdk/src/lib.rs | 2 - crates/matrix-sdk/src/room/futures.rs | 81 +----- .../room/attachment/matrix-rusty.jpg | Bin 153924 -> 0 bytes .../tests/integration/room/attachment/mod.rs | 231 ------------------ xtask/src/ci.rs | 2 - 13 files changed, 3 insertions(+), 533 deletions(-) delete mode 100644 crates/matrix-sdk/tests/integration/room/attachment/matrix-rusty.jpg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84ac2b4ec95..320d71a186d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,6 @@ jobs: - markdown - socks - sso-login - - image-proc steps: - name: Checkout diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 326dfd07cd8..8ab837cd2ad 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -94,7 +94,7 @@ jobs: run: | rustup run stable cargo tarpaulin \ --skip-clean --profile cov --out xml \ - --features experimental-widgets,testing,image-proc + --features experimental-widgets,testing env: CARGO_PROFILE_COV_INHERITS: 'dev' CARGO_PROFILE_COV_DEBUG: 1 diff --git a/Cargo.lock b/Cargo.lock index 0fac945b493..01a2e0e8e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3107,7 +3107,6 @@ dependencies = [ "futures-util", "gloo-timers", "http", - "image", "imbl", "indexmap 2.6.0", "js_int", diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 1f2e7ab4b54..d7fb1387d6c 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -13,10 +13,6 @@ Breaking changes: `Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail` - A custom sliding sync proxy set with `ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered proxy. - `Client::get_profile` was moved to `Account` and renamed to `Account::fetch_user_profile_of`. `Account::get_profile` was renamed to `Account::fetch_user_profile`. -- `generate_image_thumbnail` now returns a `Thumbnail`. -- It is now possible to select the format of a generated thumbnail. - - `generate_image_thumbnail` takes a `ThumbnailFormat`. - - `AttachmentConfig::generate_thumbnail` takes a `ThumbnailFormat`. - The `HttpError::UnableToCloneRequest` error variant has been removed because it was never used or generated by the SDK. - The `Error::InconsistentState` error variant has been removed because it was never used or diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 2b2a5cd7062..cf356eb7114 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -41,8 +41,6 @@ native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] sso-login = ["dep:axum", "dep:rand", "dep:tower"] -image-proc = ["dep:image"] -image-rayon = ["image-proc", "image?/rayon"] uniffi = ["dep:uniffi", "matrix-sdk-base/uniffi", "dep:matrix-sdk-ffi-macros"] @@ -63,7 +61,7 @@ experimental-sliding-sync = [ ] experimental-widgets = ["dep:language-tags", "dep:uuid"] -docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode", "image-proc"] +docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode"] [dependencies] anyhow = { workspace = true, optional = true } @@ -116,12 +114,6 @@ uuid = { version = "1.4.1", features = ["serde", "v4"], optional = true } vodozemac = { workspace = true } zeroize = { workspace = true } -[dependencies.image] -version = "0.25.1" -default-features = false -features = ["default-formats"] -optional = true - [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers = { version = "0.3.0", features = ["futures"] } reqwest = { workspace = true } diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 5015e5e9208..5d1802af973 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -60,8 +60,6 @@ The following crate feature flags are available: | `anyhow` | No | Better logging for event handlers that return `anyhow::Result` | | `e2e-encryption` | Yes | End-to-end encryption (E2EE) support | | `eyre` | No | Better logging for event handlers that return `eyre::Result` | -| `image-proc` | No | Image processing for generating thumbnails | -| `image-rayon` | No | Enables faster image processing | | `js` | No | Enables JavaScript API usage on WASM (does nothing on other targets) | | `markdown` | No | Support for sending Markdown-formatted messages | | `qrcode` | Yes | QR code verification support | diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 6d1f0704da5..dbefd0d32a1 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -14,14 +14,8 @@ //! Types and traits for attachments. -#[cfg(feature = "image-proc")] -use std::io::{BufRead, Cursor, Seek}; use std::time::Duration; -#[cfg(feature = "image-proc")] -use image::GenericImageView; -#[cfg(feature = "image-proc")] -pub use image::ImageFormat; use ruma::{ assign, events::{ @@ -34,9 +28,6 @@ use ruma::{ OwnedTransactionId, TransactionId, UInt, }; -#[cfg(feature = "image-proc")] -use crate::ImageError; - /// Base metadata about an image. #[derive(Debug, Clone)] pub struct BaseImageInfo { @@ -198,12 +189,6 @@ pub struct AttachmentConfig { pub(crate) caption: Option, pub(crate) formatted_caption: Option, pub(crate) mentions: Option, - #[cfg(feature = "image-proc")] - pub(crate) generate_thumbnail: bool, - #[cfg(feature = "image-proc")] - pub(crate) thumbnail_size: Option<(u32, u32)>, - #[cfg(feature = "image-proc")] - pub(crate) thumbnail_format: ThumbnailFormat, } impl AttachmentConfig { @@ -214,42 +199,12 @@ impl AttachmentConfig { Self::default() } - /// Generate the thumbnail to send for this media. - /// - /// Uses [`generate_image_thumbnail()`]. - /// - /// Thumbnails can only be generated for supported image attachments. For - /// more information, see the [image](https://github.com/image-rs/image) - /// crate. - /// - /// If generating the thumbnail failed, the error will be logged and sending - /// the attachment will proceed without a thumbnail. - /// - /// # Arguments - /// - /// * `size` - The size of the thumbnail in pixels as a `(width, height)` - /// tuple. If set to `None`, defaults to `(800, 600)`. - /// - /// * `format` - The image format to use to encode the thumbnail. - #[cfg(feature = "image-proc")] - #[must_use] - pub fn generate_thumbnail(mut self, size: Option<(u32, u32)>, format: ThumbnailFormat) -> Self { - self.generate_thumbnail = true; - self.thumbnail_size = size; - self.thumbnail_format = format; - self - } - /// Create a new default `AttachmentConfig` with a `thumbnail`. /// /// # Arguments /// /// * `thumbnail` - The thumbnail of the media. If the `content_type` does /// not support it (eg audio clips), it is ignored. - /// - /// To generate automatically a thumbnail from an image, use - /// [`AttachmentConfig::new()`] and - /// [`AttachmentConfig::generate_thumbnail()`]. pub fn with_thumbnail(thumbnail: Thumbnail) -> Self { Self { thumbnail: Some(thumbnail), ..Default::default() } } @@ -309,132 +264,3 @@ impl AttachmentConfig { self } } - -/// Generate a thumbnail for an image. -/// -/// This is a convenience method that uses the -/// [image](https://github.com/image-rs/image) crate. -/// -/// # Arguments -/// * `content_type` - The type of the media, this will be used as the -/// content-type header. -/// -/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the -/// media. -/// -/// * `size` - The size of the thumbnail in pixels as a `(width, height)` tuple. -/// If set to `None`, defaults to `(800, 600)`. -/// -/// * `format` - The image format to use to encode the thumbnail. -/// -/// # Examples -/// -/// ```no_run -/// use std::{io::Cursor, path::PathBuf}; -/// -/// use matrix_sdk::attachment::{ -/// generate_image_thumbnail, AttachmentConfig, Thumbnail, ThumbnailFormat, -/// }; -/// use mime; -/// # use matrix_sdk::{Client, ruma::room_id }; -/// # use url::Url; -/// # -/// # async { -/// # let homeserver = Url::parse("http://localhost:8080")?; -/// # let mut client = Client::new(homeserver).await?; -/// # let room_id = room_id!("!test:localhost"); -/// let path = PathBuf::from("/home/example/my-cat.jpg"); -/// let image = tokio::fs::read(path).await?; -/// -/// let cursor = Cursor::new(&image); -/// let thumbnail = generate_image_thumbnail( -/// &mime::IMAGE_JPEG, -/// cursor, -/// None, -/// ThumbnailFormat::Original, -/// )?; -/// let config = AttachmentConfig::with_thumbnail(thumbnail); -/// -/// if let Some(room) = client.get_room(&room_id) { -/// room.send_attachment( -/// "my_favorite_cat.jpg", -/// &mime::IMAGE_JPEG, -/// image, -/// config, -/// ) -/// .await?; -/// } -/// # anyhow::Ok(()) }; -/// ``` -#[cfg(feature = "image-proc")] -pub fn generate_image_thumbnail( - content_type: &mime::Mime, - reader: R, - size: Option<(u32, u32)>, - format: ThumbnailFormat, -) -> Result { - use std::str::FromStr; - - let Some(image_format) = ImageFormat::from_mime_type(content_type) else { - return Err(ImageError::FormatNotSupported); - }; - - let image = image::load(reader, image_format)?; - let (original_width, original_height) = image.dimensions(); - - let (width, height) = size.unwrap_or((800, 600)); - - // Don't generate a thumbnail if it would be bigger than or equal to the - // original. - if height >= original_height && width >= original_width { - return Err(ImageError::ThumbnailBiggerThanOriginal); - } - - let thumbnail = image.thumbnail(width, height); - let (thumbnail_width, thumbnail_height) = thumbnail.dimensions(); - - let thumbnail_format = match format { - ThumbnailFormat::Always(format) => format, - ThumbnailFormat::Fallback(format) if !image_format.writing_enabled() => format, - ThumbnailFormat::Fallback(_) | ThumbnailFormat::Original => image_format, - }; - - let mut data: Vec = vec![]; - thumbnail.write_to(&mut Cursor::new(&mut data), thumbnail_format)?; - let data_size = data.len() as u32; - - let content_type = mime::Mime::from_str(thumbnail_format.to_mime_type())?; - - let info = BaseThumbnailInfo { - width: Some(thumbnail_width.into()), - height: Some(thumbnail_height.into()), - size: Some(data_size.into()), - }; - - Ok(Thumbnail { data, content_type, info: Some(info) }) -} - -/// The format to use for encoding the thumbnail. -#[cfg(feature = "image-proc")] -#[derive(Debug, Default, Clone, Copy)] -pub enum ThumbnailFormat { - /// Always use this format. - /// - /// Will always return an error if this format is not writable by the - /// `image` crate. - Always(ImageFormat), - /// Try to use the same format as the original image, and fallback to this - /// one if the original format is not writable. - /// - /// Will return an error if both the original format and this format are not - /// writable by the `image` crate. - Fallback(ImageFormat), - /// Only try to use the format of the original image. - /// - /// Will return an error if the original format is not writable by the - /// `image` crate. - /// - /// This is the default. - #[default] - Original, -} diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 6aed7490326..cb9b2789b9c 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -336,11 +336,6 @@ pub enum Error { #[error(transparent)] UserTagName(#[from] InvalidUserTagName), - /// An error while processing images. - #[cfg(feature = "image-proc")] - #[error(transparent)] - ImageError(#[from] ImageError), - /// An error occurred within sliding-sync #[cfg(feature = "experimental-sliding-sync")] #[error(transparent)] @@ -500,27 +495,6 @@ impl From for Error { } } -/// All possible errors that can happen during image processing. -#[cfg(feature = "image-proc")] -#[derive(Error, Debug)] -pub enum ImageError { - /// Error processing the image data. - #[error(transparent)] - Proc(#[from] image::ImageError), - - /// Error parsing the mimetype of the image. - #[error(transparent)] - Mime(#[from] mime::FromStrError), - - /// The image format is not supported. - #[error("the image format is not supported")] - FormatNotSupported, - - /// The thumbnail size is bigger than the original image. - #[error("the thumbnail size is bigger than the original image size")] - ThumbnailBiggerThanOriginal, -} - /// Errors that can happen when interacting with the beacon API. #[derive(Debug, Error)] pub enum BeaconError { diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index c45d7cb13d8..0ad8d374ee5 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -71,8 +71,6 @@ pub use authentication::{AuthApi, AuthSession, SessionTokens}; pub use client::{ sanitize_server_name, Client, ClientBuildError, ClientBuilder, LoopCtrl, SessionChange, }; -#[cfg(feature = "image-proc")] -pub use error::ImageError; pub use error::{ Error, HttpError, HttpResult, NotificationSettingsError, RefreshTokenError, Result, RumaApiError, diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index ee8d278a281..494499d87fd 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -17,8 +17,6 @@ #![deny(unreachable_pub)] use std::future::IntoFuture; -#[cfg(feature = "image-proc")] -use std::io::Cursor; use eyeball::SharedObservable; use matrix_sdk_common::boxed_into_future; @@ -32,13 +30,9 @@ use ruma::{ serde::Raw, OwnedTransactionId, TransactionId, }; -#[cfg(feature = "image-proc")] -use tracing::debug; use tracing::{info, trace, Instrument, Span}; use super::Room; -#[cfg(feature = "image-proc")] -use crate::{attachment::generate_image_thumbnail, error::ImageError}; use crate::{ attachment::AttachmentConfig, config::RequestConfig, utils::IntoRawMessageLikeEventContent, Result, TransmissionProgress, @@ -292,81 +286,8 @@ impl<'a> IntoFuture for SendAttachment<'a> { fn into_future(self) -> Self::IntoFuture { let Self { room, filename, content_type, data, config, tracing_span, send_progress } = self; let fut = async move { - if config.thumbnail.is_some() { - room.prepare_and_send_attachment( - filename, - content_type, - data, - config, - send_progress, - ) + room.prepare_and_send_attachment(filename, content_type, data, config, send_progress) .await - } else { - #[cfg(not(feature = "image-proc"))] - let thumbnail = None; - - #[cfg(feature = "image-proc")] - let (data, thumbnail) = if config.generate_thumbnail { - let content_type = content_type.clone(); - let make_thumbnail = move |data| { - let res = generate_image_thumbnail( - &content_type, - Cursor::new(&data), - config.thumbnail_size, - config.thumbnail_format, - ); - (data, res) - }; - - #[cfg(not(target_arch = "wasm32"))] - let (data, res) = tokio::task::spawn_blocking(move || make_thumbnail(data)) - .await - .expect("Task join error"); - - #[cfg(target_arch = "wasm32")] - let (data, res) = make_thumbnail(data); - - let thumbnail = match res { - Ok(thumbnail) => Some(thumbnail), - Err(error) => { - if matches!(error, ImageError::ThumbnailBiggerThanOriginal) { - debug!("Not generating thumbnail: {error}"); - } else { - tracing::warn!("Failed to generate thumbnail: {error}"); - } - None - } - }; - - (data, thumbnail) - } else { - (data, None) - }; - - let config = AttachmentConfig { - txn_id: config.txn_id, - info: config.info, - thumbnail, - caption: config.caption, - formatted_caption: config.formatted_caption, - mentions: config.mentions, - #[cfg(feature = "image-proc")] - generate_thumbnail: false, - #[cfg(feature = "image-proc")] - thumbnail_size: None, - #[cfg(feature = "image-proc")] - thumbnail_format: Default::default(), - }; - - room.prepare_and_send_attachment( - filename, - content_type, - data, - config, - send_progress, - ) - .await - } }; Box::pin(fut.instrument(tracing_span)) diff --git a/crates/matrix-sdk/tests/integration/room/attachment/matrix-rusty.jpg b/crates/matrix-sdk/tests/integration/room/attachment/matrix-rusty.jpg deleted file mode 100644 index e004351249ecec34cddaa8d4b1efaf9fd7b13044..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153924 zcmeFZcT|)Aw=Ni@Nmq(=qJkh@KtNg|B3(d0dWncg2aygTAV`-kAT=r=orpB4p$7z| z_ZoU9p+*Sd=KH&6-C1kyJ+tP_S?kQqy1vQ&CkZd_-k&Es&wlp)yqA-gi-2odYMN>Q z5)u-?bK(PVIRj7ukY2g+&y#qN5ufB&$;rvc$f+qQDX!8|)6&vV)6me-GhU;kzfMm> zbM3~p>rBinEG)DPtZX-!*%+BwnEyEm2`TY!$jGV4$*Gv>Xy};#!^dSS;QCc^K5{-% zlG}hQ*GWjPlU#NJfB*mqIq_=$x!`|0NUjjCk%ICn6*Ud<12xwGS4c=nuaJ@cbJfI8 z2NB-~kX-RtGtgY+gJ>SFnm%k-h@(7-(Y29=eWZ!a92?1 zzO;<&137t>$Es@T8k$;$&y0*sOwG(~U)sI0cW`v_^z!!c_45x1dlw!N85JFqobn+x zE&XFg=9j$ug0F=|#U{!e~gC;D}TjEt0w@*lrQuJ{s-^g0>&EeQ%nWqnE; z52o9aZ?7^xO8Q*cM#U>-fMI#@W{CO*-+lBQ>_4vk%d`J`j)nd|^6Y;&_J8;_4WK0@ zAub;2bpRM}VfZEHF5s{Ijlo|G{Kddu4E)8wUkv>J5(6ZS6XS8)+fRp-W%=h@s@);x z5iwU*X8!HPhn86ZXCo!ex_8;C^;Jo!?|upSYya^W2oI?;Ut`u+1^iIe@CnPoL_mG6VkFe>estY_X#wO*Kb_`VZ+QTRt?Ze=7JA!kG4#nEoZ^e=+bE z1Aj5_7XyDW@D~IBdt!hj;$b1PA>Vm-3{|}}v%a!2k!X_qwf|HM$RvZd1os-_m>EGa zcRzjlTk`&=OJ0(@M9TiRr2HF$zZm$7fxj5|i-CU+0~`l@wqif?F7jfGo-YfjFg$f$ zLCioFJ8!0}kVyEwBr3?<>(* z;sUX^m(51p%557XP{wg?ibsmeFjD$Y-W5VWiiQaMk=8&e3l=eRiM5~xIBW=V2MWGu zEPL*(B)+5;8H9G#Rc&j!msb7a9@WBFz$q{;NEQv}aDYix#viJVxq1uxQyA^8nGn<( zYz(AXmi9b#84JbCV+N{ZioV1QCjR?`A#~|%e&oUn+Zn_ja-T$rg-Z(M!R9w9e->u& zpJFWkJ1{;ia$2Mx=gDan9Nhgeg4Gb`|><+}dHsnN;c++6T|bMq7HC6SrK9b&z8}dC83bye2TgX(S-YbbSDJ+$#84M}NHMXt z8W4>i;LwZ6xEAg(6HHpYYf;zj zz2upGeGiho@9Npbl$cXQoT55bW_@Yr^ah9(4h-&MSq~^*ndi(Y5UZ8HR49iqn>SPs~PJQiO_N5AQgV`x3DD2{_q8Hzvy2VJ$N% z%?WIps8H_;W>J(_obgRJ z7%TrI+&!N760UW)t9}w?rT+-$`vm@U_TC-vZPGoB{G5}~cJIv*T*j`Um8x4oe$q1r zLrZWpOv=x*!;tMxb4*}G=sX4e$^|)eYFUrR`sd4fEFxhSukgaOsIn^c>1nHozastE zB1rI}Mv-ap^*5ZXifgce$oYPO%gdwQS6pjwk(QCVHUGoz1r6Ta(*0QL+gP=PqFKjp z>gXB2FZ>FzcnX0D_X4Og7GW?}t>CKOd3M@;9vj=O`K4}T=cUF6nb{)hW`9Hm=ni2F zB#*ryM!^XeZzZLd{Z8hFUd_X~!R2xfj@lH@k|0F?$KTsb+ReW`yMyI*slJY10{jOA zM^mhaHiDXo@$I|#u4uUA9)&`mzL%D($r3eSm0>m9og>4B&S-M0+yzImoFzcsgE88A z??AkxE#S@ho34^*s9XW+{sE+y{d(2z3`eQ2oIv4c#MIWf=3Ug_v4gCmSo$Dw4f?nK zy{!Jf{p-TKo%13?c+UKR;_ZOcmJFLK`^kZk)Fr~p@R$Y6GjWQIv_s`ZQJLi~+0vww ztZ)a>^aZ|`gewKTqE|mGa}U(M%2ja4G|M;5(@*n#4meA(=EW+UwBvj+C$cqlV=0~T z>p3TWN96)i{lm5o+QZ`WGVusp+qo_NA+DY;VTpmU*SW9^E|!`vrAj}W9dUCN$z-G` zey%^TXkq56$hz1Jnm)0+I~_Njaff|RiSV^*Z4IfXW7aEl5TFd5I#S&ah24Rw;3k$5 zqKj`sg)t}1&fLwz)*1-mtF2ca)w8`|MLx+~!CPQ~i-^|k?L__`xSXZ%<^Cp55vN72 zM2%_r6N(leY+XD)2rH%r@8aQW2sF@?alECW9^60=mPZS?vOY0>{>xi_y{PAx9fy8g zW{h=U<%L`I=bbsVHEGGK^l0gcrt z`!e zbuQ;qU^2t1R)NECY*qkoBT0C!e3^q`WV67_Hv}=P^zTNW#lAbEt3NZSMD*CUA*I0> z^#!`VPf!7@;)K*Cpul?Ts!rqUo}!PGJbw`4xWID(oP9fXrb)k`Gpcc_K=sa~xM#e7 z#qzU}CJ3g)8N!LXN~6%MgqjgCt913fdj4|@tTNuzAlGEeUqGX}8JsyWc>V-JrN^NB zEbj4I&FaibW7UyQ%mFeKTx}8#l7@mSvD`VwEpXvt?MC6^N7B`Ool%S z1ZsAS?;;}074!qDUW&MaQiUkMPx8-0f~d|+8rE4FLHwJ*C*#)5OK}42ClTw$qc^{W z1T+68hv`5~F9D&ge#_Qo&oSZTIhvJ-uCYLzX{)B8!93pZSx=#%|E2rC8Jl{@NWx&mfB;hKd;;x`XB>D^*${t-vgS=U)_KP*KQ6%qJ>g@@Hkltk!J0p+RAy9R z#1wOclVVuzNPk6{j4jQr7yrgFpb24^i4Grg)<~}nuQ~bO63iZ*A+Hs(=`|~2Vgz#p z?|dPq>$l;|`VYzf+P}rXz>U2NAo@=@JmHLr8uN5G-zE4pe%VcVqcPmvh{U6TOThOD zb<6!uciOtR7N8%zqiCDc>C9>@6k!m2<*0BRX0HpmnsRt4RpyzGzkdH zO!0#Z;y;lA@3tw0`;Q|F=OIoW7LyMm+M@KHgv#+ZB4Xz$$6!~87$^9LGLXTmqO%V>{$^BdISM+?6Kn z-3tpOgV{Pk|5)IQj8jV~UKK zwVNR=^<^_HWm`|*`v?w{YTkxnV*&IF#Rww*urbB+E&{`R&+4M3Edu3gqNfe`Ub6{t ze+rhuO|*KNR{t(%`=h)RS<{e8UiD;$Db)()K=Hj?dqyQ$Z0;;~+#=qTx}qd*uB^er zRc{y>1*t3S2)+vBeH?0Kymt3zsVa}5?*#8~%L`oWxsTsKx6G-Jyt(>U$5TZ~@;foh zJWVFQ(#|yBCn%)A8#8mCGQHOC4m44eJd?mR@AvJ=W7+q_nwJXfR+004d1j+-Wc_t1 zw~}I6G_M*fy-_vbTzmdoyUu_PG|28-5_*pTV4Rzdj@|Bz`x%tuaj2O6+~iZ9Yto^c zc^&`D8MU$lc80wkxrz#usXXkX>&_?flpNd%LK~-fE`dv5`uZga2T^aBIE&0=0cq6+ zq;*T|LOh|I!~|??86L5WYRHzsxY2uE$o0B^h6eMR=MVi7*yV{?Uz>-cF9Fy5zAxQ< zVfHH7QZ2EYM#}yZ%MqmA9mhY=IlyDdmp<;X;q!@f-3nxSIQ5X=eKvtvKe5LJVQSgZ zRR&$r$r7FYvY;Se9_LZs=v$Vz!+F{>6AU$mvT!-+Ntk`&iSEGPLd8W`a|P;uOo*kB zwfEl;IX)*Y6U4?(7QG*-NbTi!rf|aefS?nJ>c5~kR5+j)TH+lSD zCC_K9CbcK}8DjkL%-rT`fTv_fCJ|7Bi)s)PUNFXM6l!E2SfnO(LI+Y->^!saOhB<< zS-TK8ksF^q`A)3KFtaibb1D~{4E^7$d#)C>f}_j09WHV-*3_pUb=*fS+!t`DRhc7) zYtDFs2tGXbr2+Oa%K68DL5b!SKIG$Lgs^#KSjOB>?&ww&vI1U^QJDPn$75|k}i+; z6%XI1L~xhH_Fn>;mXp={o#+Z!*IsleQECmhYqG{IWC*=mlSMaIDnAYDe?|U`B&Lv~ z7i7Tvq7_?hpE=7zJDS|Z_e0<1y9`67N4HdF%?<73__z$uYJT_4rSS!kpdo8NuP27b z@A+{%??NW)7VF-Dgz*Za-j@J&il!XIo$oO-{zj(lpXb`RRufhsG>+fF8QyQVnmk)E%p>T2vq$$%@J`M88jDgK&;n@FA!ntCVioc<(bplUj{?7aMHb*G zu%|IFs;xI=K&M9e^hkHE=T_Yv;`DmhqM3!p5_@hr)hzFDb_t_qsjy8!@gxJ@p6*wX8YXy%z0huvjo(F48c z@uQE{>!iZpC}qgu<=Vg?I0lX}5rv&hqZOIZ7FJ?hyB5E64eui%qOVEtI#7Qs_#_{f zf;C^Y!vbTEwM!i(f7x-Z-DTS3c<#Gz?eG41OFrKT8?Mirs8Xl9l;w}kAKuNj>^E1b zTjiEIEaYS{aDAQEiXoW}3FoR>&vs_Athx$#2E`}H&E z?*{|J%~z*A-lrRRwstBHWf#|!xCV7;9Gyp_lWs>CNp#kA3~lX|)V~`=p8kshR+z{IsGK*Jd#9OI(c&KON_jmuaIn71ims0_ksnjy)OrG#7h_7!UrBSjb9(0Y~EgKOMwt-v#wnu_1o?m?ThPDMi! zhx-Wj)%Q6^d85ug#-MM-04Xo`fDN}ivnJ3%zDB4j8?|sqb^NV+{uVT02Qf#g@ZgAL zq0h`}3j!@t#x=3GzpQ#Vti5HR1Msipb;dj8bVHmTY!2<%m z7U^W0uwTV=A$)kQIr4d!|29ESL_?p?XuJBGz!73JfpVk4{hgOM!={gcbmxtRmlBV6 zxPMVwN&uYL`0FjZszj zaD8xiVjP(Mm$v--v;{~W+-40Dvh=fGYzbS8y`c||Mm#w+<0UASXK=b#N;W+Pp1pED zfc3S@)}5S;+>T2wsXcuyHQ+gEGI0sO7(=3fm{izE+B|0jM3RAF)A%Ir8i9RvPU3lx zRv%Fl2r=nCyI7s|O>nB??R}zF%-+go3Gmnv&eE=(Fig%%Nf*&AS!n0re}B+dwyiJm zsL{2Y-j@OKsi&+I$>f-M|3Ia{OqeUNN=vezVZ8paWl!VGT{RNWkFTl1{90(LR=$7$ z{edadfhmZYLS{PbR?CM^YS&JmhjA;6QHkIA5tnJkj?rvWggd>=%DTzO(ik>r@`*42 zp{f8$;MGR%d+XuS+8>_|zkIt=+{=EfL~bwW=r1+Yj?QsPD$sEnC;>0R7sy4cg9HdS z_^a4uPs8&@#wISFm=Q6nG#N@+H{N`>B;7FYcfeOS?OMTj|3{Ku|9Wd!!}3>CXhlu|_y@ z1?OOQZzu0Yv-K`KEx1}OJJTHn=d^hLywB7=Mf+!XDP5GR->>%4+y3BtuO$^GMxO8Q2o_Gk)Y1O$KVZFI*1ieOApbSj z)t}EQBA@F>Fn=dHr|a~x#-O0>X5E#-P`>Aez^12u#}$pac0(FhUYG&;>Xdb(ZUC;x z(q?`;AYFai9=<}YrhgUk(caC|fa=4w)K?;bON~H!Ckk!B=ebW%l`#T8xF&c#Z&OTD zm@*t*IO)jme)+NGv-A9(oZKbgcs$7^Gigm-$BuI=m%AZUVtD1AQpmo648jH-XiHw@ zUw5uyuf&P~mcKCHW}GscZO!_k?s7O$<9Prhm?Wkg28>ys-DXj!3yN!&)kI=Bter?j zz6mBpgR&P*w!rOh_D%RTyaamJdc?W&Br$*Mfkb~4%ful?3tJ5?xD-d)%E5z#ejE6- zowzo=HM?hZ{$>c7NB{9edI8oQe4THw2~vVnn$sybjO{aW19^0i99sr2NUyGaaM#vZ zM*M*Xw*4SxwzyOQ%rCQmBM?6FnG|_4|4&-d_&gw?WD)GF#Ddqu$eQP{vYPv*7IiO& zM#!*(lcc8|OlLq7x5^C$VKw5VAs#KU}A?Xw5hf}@3+9dDnJVB&)O?<3zhTcB|jIdx96kJKTBly z*l%baXOYJ zghtCQ0ci128;6xd%~kWshy83FJj(;B zeZYql3K#RFEQ(fO#|H<9emtpSpY4Yi^BgQj+U{=f=o1-7=Z+@Fa~CKRhGRC^5wARK z?Et!H&R%dx$2{wmUT#3)SvXtI!k4G~DBE&Hac_M}lP+?e+a?r~?n5UBf@&7}b5R2g ziSaw@zUw!EMW#BcZLg&O-^vn2-lx$Su#oSiFXxx?pab63}VrO2CEvZtB~FNH&t zWSAguBsuvbN z;M0i8j2~e>SEL!2Q;0KM(u>`1QZ8_g&$N5n6ff!O_K~oeX-RQwVDFDW8q3*;`Tb#? z%x))>1EQFkWbVH?dw|}Jj50{2|{Ed{}K=#*30~8 zVQpUK6m2C_}vPaHBUaoJyKdqQJscj67hSwH`C`ys~pZSIzt&NGBu$!*=%u9ko8O}Gx zBPxq6P5T$@M8q;q#j|>*NdUoc^FOn&TU~FwkgK215~x-yQ?ZQW7Q%#AP+I$Jk$3xX z-Nd!rR-RSyRYPvniq>Aw62JSsw^v0>z;VEr{`E=Y zV)7DeEiJe}V(WSum$Ae-q2r?sp)>1d^}Ga>CsmCCluCSZ~P)-(hhZVM$oc=}UrJE`#i@sWz}8mQ*H@H`MIov#eg+?NpIp%U^3J_4&) z2Q4nJZCR6GUnMMw@}SGTMPRH^M4k&krB5L2{wM2m;ZlPzw;B#|+*CGVrk zjlvX?Hm>%b7(#dr>BOCPOs0pRmw=v&c>f%sNrtlK;faa5{ciZ{)p z;jH>3%jX{gi_I)HlzUEYx1^Ra0C0~+h_ZXA7m&_0MD%_Zn@e76_hScEaJ(?ArEN+< z-S*(l8gbj_t^aS-kS4?#k?PZ36nY@^jSd=4mk$%dg>F3#uoQV>j5;5AfcChUuSwNP zEZ}i$%BFVzBG@Wn!;J|;xIf(eYN&Be z9p67Et`alGTU{CcJVvU<$OJ%(ac-;`DiZtw=zu^RZI?O!pNASJBUGm~a zGjaf*u%0Wj98*)GO_Q!QWbP@3=yR*#&PSAiIKOO7|+o3zOz;+s^L*mIrd zbhi$C6?;X>cJwMV`HR~Mof<0k)69G@+LcQ!ztrUKa3{1m3Dvw@H3)>ix&(Bnz z=xEf@$alu`H$5%^;-H)Lhf^UNj}lv%Os$E(2BE2bqo#d@I9}BuC|%*VyQTTlLJbJL zCUwXsmV@KMxBGi+L7#`odEaMeB7vbIqMuXuj<3_G|7HVR9T+dJH|>4&!Mb8+qsi=C z9j}fn^kuf@$AUhWJ?v-gssH$~wdXhM-U)Z33EZ#bRf=hbS*%b>b?$OyIdb(=aMzSDu5_$(PzVEpGixwR03dU{YKn z?n0LQ*L0JhFdVDi38wMmAFEJQS<(?udFwCf#9G-3#;!4w3=UbV_@A3VrQ%)^eoYi$ zMT&CmQ_wRsajGBfx%}_861@ATVZ*r!SoN8F46=2ANza)&En>ZswRZX`oRW*T(tE(# zWIIN&KSx8XaPWi4uS-Cc`)-OIwvpKW$M83P77xR_f2m>}0nMg63Kf&I_k6mtr6(jw zogMmm^6XJE^ykK~q=5->bC4n1zqaN?1%JN| z&_aUWUaDkVPv=7-HsK8`I5V4XquYk5$#K)pr4zxrk-@~F zaBAQc{_O|t6%RUI{79gik!~WGr#eKF0RjR{9Lc8G7!HctFPsPSSlA~2xC~d98n3|$ zSBRzOsmeL!F7;{YtxJIJuJDU@RNJ8b z@!)Q^UQ==kgu|>BeXI3+3YZ%3Q4bBB`m?8Ce((qOu2}rzl-R?BwYEJ^NbMz{QtxdZ z&i-7+Pv%!Sqi|NA^k_j)h=SsBze3B~5Te8ugMXN2$R(*kd8ff-bR1k;h!T&vwmE+T z^PR@$8p@tl{uw1~@UuE!_G@`;%y^w_RkiPr=c{bL>YZQ+znLZe@I@VpOMvOvv2dWU zvxB)KZ8AWY;x%PCOaZEcjf_URxF@95)@mGR7tPTqfkq|lLPb$2n{U%>t*d}LQrPn) z#8e>`^nEBlu0C&6eA*z4+b2{t$ou>bto=-UXCDEM_NLQ55A00tSXCB&d_&=R$NHq) z%F#gK_zy?5>+tHVMho7FvXXHvA;rq=*^_~(x<%_G31!S#AIlzor^Ey7FaLG&f=JPsxXieAkJ>;{Ancf!1cv zMPO&N$S22s0tg`rGuGX_GZKghYL9qLgg`kjQ7|)4feXFC0AxmEQvb zJq>i*R%$~TX|>}}6;#`26RT#qjwb8^%ohs-1dZmjW9fMn*}hogkD}tm<$s8=uBk3ONf#c^>te07wX) z)dSk=cKfiE>vWy{53{JT7 zlYU&B!@NvZYc7{RtKJB8hxc9Ej8~+oXX1y5LA@v-OIfgeaH#x1)ZPezUk1xoW|Qi< zW(oco6ZB9K{@fWmf8J^6#G0O7N4K_Mt#!&|2^RU?nQ}(tVO1pBInSI`{}9UvaJFAn zbfuBpH?LKP=KH(E`VL`-w%DFxCEj7sqxmt!l{`buZ^Y?+sA5Ga4_ow6fcfM%m zeDLU9G~}iKZI;Ep@~Wlw1k92Ap-ZJdQp?< zKB4y;we_L037pbZA{hhI+J`7e4dPv2Hbm|!VMreJSV48{nQXorsg8<D$!v?J%;KHTg zta#N4?^P}u_nPY0@r6-?;lR|O2#A&+l+RIbNrxbU*Nc{-X6yg`m6skal3bg)Q3k`} zMovO-&Y1PrSn&i!r92JfLHD+Pv>l$wXZepw)HVcT$PnstD2<3brnBwNjKr8ba6CR# zfIl>Q2CKony#!G4B)Q(Y?)X&&sjzYrKA~vxWfNH=kkF)n_QIToixJgPjuDONzPyLE zBTqbYhP6TzQgZVrf=faAW2M6a+Z2`O&+#@Bw`5^Vxa-~p8=W^e;BnG^_ghO7TChy@ z54R@@3;ffgzzze^p6b#IF~JV|kr0MmmT%)cOy3fW;XJ>@OyQ@hFq^GhKHS@7Bk@wF z%(xwks)n=OsGen9!h-=cJfI82WoIlq{j2N^fWFgKm}wLBXH!p=2>R}0p2PEm5#fYs z&PD0+`g*K83K<1caDI2&N@;Ho9kL!l!RZzEI*8_am?eMS`D?;&FvD>~RvVlGSzGbR z@Fd9aS;XCiel9@@72fb}T|dz5S_^H>@coQTF0s!rNm3CFUdmjL(TQ@L?1=|1Y*}MB zA%eiQ(h;qhP7Kfy0&h633+3yyr7?5C*RHWg|{M$1giQT`yO4u z1aWB=GlASqcDI|4D^eC`%bL|>XUxnl*4#(s2| zZ;Wp{y|YiW>RaIfzw_Ex@2W4X`jDCo5+{~GKJ!*Hgh>bkzJ4u^uesq5uhmSIyX3v+ z@^l&NlRP04dl&ww_JyelG^)+ZaFN(w-)vAFpDTwZM%KdscM4o81xOVStrK({k`;=( zx=)EkL)g>*)kd*=L<-nheCEvn+J!Zj1;drrfQpZzKKOAc>V-dr0y8dH504PJAg16p z^`^i@*%)xtnJY5Z>oE!c=>3oQ#>qeD-I6{(P+x_ArP)TrL0+s?1b&1jNOaj0#OA5~ z%=YyG>lQo5;UL(+b_lzoEJYn!)M>`}d!pStUB#Y+Q!Dw>K#rZ#VMVpfp?|cDe|~na z{L5OgDB?GSI69neei|bktHYpC%bvV-@H9#(A|PdwUIzxl|1&=RZOaq)R!x?_c%v~N-@mNCcQm{3vI3Sk1YyS$JN2QT z@UpmYZ;I4qVGBgW*-8jqL!C_g?7GV$fRfwtE~3%}QGP6JT2U?#zcy_&$5naH&SvXs z>HB4AzpC5?max}Eg{g2XQ}__j1h3|ji8u~N&$o_IWPS|7-m}OZG3DW z6?*icBDJicT!f%r<0m5x)w$)+LF+lnL#!ufI_O2mq+0haks*&|%}qWQ{+eA6tn+-I z@Ux*b!ontWQLtBf*U&h|evNHY&g4TN@WTSeC=`ilY*Kud!n;w#4omJO$}=qOUs#qy zNcdZNh-*`jp9l>YdS%-nc>h5t(+r~q9n|%zy)aPKV`8|ORvZI!KLoMfGw#$3fN7lS zO=h7fGO!)XCvl&))7G|CH3XU)qhtnuM)`#8SeC=uaKglI$vY6&ZFTe<1xlNy`+2wu z@XbTS0362`pJeNe6-LRwPcA}M4KLM&GL7aZuV!H+K3xLdnr!hseS9S)yH2h!re-PK zee~!J?m)TkN7^^OH$MPxB>j(B?)&s{IjP_D0)>7uUG~4BDsc1V`{OP}jqRHUqik@n z;CAkDPlD>)j=7rS9j>DtB_YR*H&>O^&BJeBBf+b4J2g!N+WNktt`rm{u=swLSX~QX zmga2pExri%i+^xhd8$Z_CfvobKEdTJTG_W}ez{TI{PweL{Ilg%?^8-jOhuhebePt# z$NhTS`KHJ&2s8hXv~a0*hR{N5a)9avl4~)o~XJ|(yv+s@;C?6K}~FyfW!`^O^I0#U(7Q_ppng-M$gS-3gnX}5-cZrVt03u ztvbm2_8{zz*<+gfTBSF=DNmD_2;X_}8UZjhq|iWNz$j)vS{h8XZ5&XW5-5{XZ2lm5 zerv9_$weXT%%qAVTFh(;tnv98eClQj313X?T(hn{b8f42@z6Q|=m9*>5gi+fuRHNpv_!K{7} z?F&!FYGU}GTU#SDnmkm5(c{bOO8>h-_AoDLB>A(8ci-IXyiH{F7 z+IUq(`U3#+nfNIjlGy&ym^cJp#))W(aQS-iOIA8N({U-GRBs#dJupR!cvL4I&UgTC1r?bV3hCr%b1HT5M2N@L% zRCHb*kO^kPZ5tpN-5^yneXE@e=XXD{QF51dO1rZzLrsxb6(hc(=EdrG`umJ|t!B!4|Ve4Z}Gq+cp`4 zdN9hhO#|6-V+s9)A7G9<8#&yrHJRk|i4ua)RAZ&e9 zm_7K3O#f?s4Vy#mDN;~9Y7>(73itD4Nmcm8B(fPk(FE+6bSb1%zkb)c{Bf46OJ+lz z?TvQIF@cydCK@T)q|>*g3F8fVI8~RUAZ_g`6eo1MIJiktSE=h~sYDZ$2 z2atc&l8N&z7m;_JTNzkEQA?!i>3y)5K4GK27u%G8`hnn%ierDop{|$lr;)hs2H0n5 zASI&Y_4advg?SZaPmFyq7c<7e|@Pt-R6mY-hfbbCwgp#JZO9OUbVkqLCx{`z`CN3#CXZN ztZ&`y>&iCXZzNQ5C}}r3jlVG1rd+Gk2fjUx^0W6p!%t4GhN`#ZC7TD~ww zr*U#x#cUb9&Wd~5==zHAu+HQPy{xPKRZ1?x{pPW62$S9WF`4j)-Re5dH8TijjN9fP zW0&b{8lKLl9f-Mo&VxN!htMIx-gJVl@7sUpCD+8j<~M9z1oozJafO zp7wQlr#^@ROCB;;r^VdvWS5%4LyyoqCPwWRoo6N$k#hCH*~sF!m|e3`ouE^{u3}|1 zZG6bYCnzUQE*RT=ZWZE$oz%>?5FHV16&b#QrXSkT7#RPm%lu{K#5`-*2laW*uwm}P*J#n#%$#>-vM)iEu?aRH24Mb-6en$DwW^mxOgF~KmH;@0B+n;%k)d6 z4d6(D-|1yMmm@CdVVsp7ajwm#HlVPaA?-Gf$6-mQ5Mi~nSxF9A4SmpVOCzr3X|O%7Y{mYlCC3w)IRpge_O>Qite5~B?c z<)QRW+d~;(?=P&ot<3t?W=s3iDQMXc{BuR|B_PnD zLASoqc4pS{`J>4mGsvEr$5fA!piJ8Gg6-%=SpqGtLm68T(t;t>6*||dzxDh&hw=bk z9KDN~_)6Rj^UuALJ|mo`eZ!9|<{&1`k-8%yUmW+uD!BMOZof}^5kRdoV@%%rHGZBo z&BXMJs>ttk>QCN$CoDvr;@lKAuVRvAgkk95M0^ldF7iu~Q=ZBTiEqh5iklwAs_tY4r7sV0HP&e0X zdEY!0&l4EKqI^bPywpA2`we2>F9Cd-j#no0vrN+x|0HXnM0t7gGo#=iJvHWsqvn1V)%ZZBwWtog0pI8uHREz8Ae@p-%|owI}^$T?vGl%$l@0t>0Yxl6!ZL zI7(!cEW>7Jlc*Yq+d|DWa^AX-e;XQ&pYCYS;LcJIXi&NP$l|$w9=bC$ndN4I%6p+R;AJ9&)jV*hIn5r zWk)bARMr!LD>bhg^a}Ji?`xcTB<7J#zoSbJ=#}1*Np1v*{XUWB_)~RKbWsmwIhUT% zEI=i&_-!diDJt3AoxAbyRt1}VAKT5haVTC=cAEp_?k!@jmKlcz-&qJJP@-f~^OIHw zmbE9fv=%X{)lVeDF6ua&uPjR7wsnWX3&y&RM-R3*YG$hI%)|TA)BM?1Kc;MHB1nN* zh8=;4zgzm6`;4F zQji|sDSvTiC5j_oqp~}MT$K$#-~MT?TYk7(_ud1<+s-ASCAhRYLH(FQFZsEo76?V9 zJ5aiHVLGR+Q#CHyFFRg2_gD?!@$-T~A7`SE@m-$i(who=8o%7y(9|#yenV#jr)TUb z+9)XJe)l-8;o!%5GG~G3&@SYwr>+@w-Lkoj#~8+-S)kCq@>dR~+X8O5+Bs6)py@C% zfFBrJm`Xoc#rd9V-*!wh5l16AZodkq-Riv4{cxpo!N+L-0N?uNnaPkej;a*rGVgRl zzBVUNETLlZXVuh}@p}W)e8W^ZE{sR~Qp@H=Dp5xGnpD1;{+1xGXfB4Rc~Omh-99y+ zokU0kT)DA%81%b-YT<|rDOC_}JZ**SAy_TBF?3^&)fGI40ySZJRp7@(MdfavTk7JM z=AY$KF-vN4QW#TyCi7;wbxWCV(d0YyY7O1o%8eTbdY(ecv`%LEN#6k6I?(0wN3aDr zbqfh!E#ubaQUdczegIF@lE2EC*0M0`19beps`Q@$?8r$84t}nyBl@+9I1_Uf9@@93 zL!*;e+AoL75injnpr-_S@i_N)*>2k8`4GjG(KFi8+bDY$BN0&<{F%7nPQtVbf!das zaw%pt(a7*LjNd|MdHu^46R-`Dzhp$7b?oFbz{arUg}b8X%8oqZJmR&u&w-xMVgI-< zR&-}CqZrQeNZN~d<9bK~uK6PrhP@hfct(}&sb@On!?)&Fj`=If!Ybqgd=N36_C~Av zS}8S1MAF6e4>Ctp$>^KNUv7O8D9ed}aS$FSPn5y@+&ZK*L4BIhh|Bbl+3TncO z{&!&z6e$u>dR6JYh7J*xE+9y6ktR(*dT0SeK#(FJ9Vyalq=p`P73sYv^qNpYsNa*{ zJM*7+&dfO%XXccv%rM*~d+)W@em>8>z#8IDooJ{32ms#BZ1)0P5O-ta;Kz>bl6;y) z()um^D=Z|L7m-E-oc&XF^KAldO(373mMgYIcO7Eu69(C{peuzd826q1ZY9nA|zk<_UjSlk{*ct0;m+i-a`-`k}&^4Ehkk_09*`lsry zzh?rns4)X{F*R(N`q`r=B~SN}-2rRa>NOJRQL*nVRkm0I-h%GSH`yHy184?8QF|MU z{$@2(SlEL-s_tCB&mk~H`o~brj=RMtkGGh}bf|9Q+TGXLc)Tp)c41Ybc7?$j{^AXp z&a>>u%h|fR*iE^{H6fqb7UiZ_$FEP#XR>&Z<{yrO+;08YQbA|~v-eUx@eh}N@^MTS zyym3`G-$70%2^$;5@e}HTy7i(7AFmuSY~TOQemUE*LNiu~QH_?U zd@=ntRSK@|#wcv`@dLrl0L+Z2lF1J-lJjdz7HL=PHpqSeoA2+sLLAf<-6-H@pQoI5 zue>lh@J@UFHWOlk?djjsm%1Tg*+18OJ$>l?ON{)Q>Dy5ymlB6wPiOK}TyEU&)(b71 zeXdmJIr{$QSDk#NqM~eT_EIuJtK$}K#ro6Kup4I-V6N)`65PWWv>F9vNC#k{r7jG< zH58uJZK~W?Mw}W$28>CQ`6mYfKR+ME@|)F_X(|>U)7+N{~?g8$r{`mnJ`*XwcxlOqYi(d%yM6$p60whQb#zY%%tO`XUoK& zdb7HAyEtCDf#vJJUfj%pedC!E@^8kZc0Af^UpInKGfAdq&XvHF6)W-sE-4avt37^F z{bmJOV>^a|)%EH^&L!QBQBu7Y8JRuhz*@|(Mq@MP>#Wbg;xhZ`dz>#OL;({slhTG1 zbk>;JtaFYKz4z)q_oIKo@_?C|R@Krdqrc6b5;bCJHLKzpr1Gd6j3p&sO(m;grOnyj zRDse|BZDNpE%1i&@A~t;AB%BhzoJgEJ-XqDU9iI4ESOk=2}ZmRm26cic529@5bJyC z93gEH*S;;y{^NeoYxIcSl*ISJKeOVR>`Gjp6>UP3Z}tZjg>mvX{{?sWFz{FJ z1vaJ5h<{^2rJ#x9Gav=_#lJT9*mP}o$iVSLPI3m3v-a7YmFt(h0%STGYUyTk8qk9P z+`L|!`@}g@=e#V$bnS3-$k`m4U>`muCYvatq(`ajdEBu(JA)_!M6D9ukYXY`FgVp- zOQzZ>FKJ!98G>vxQAa*Yy`#CvfjP98uHdj-ml7@4&en-L|0Pgxyq1_fS!hzPdJ4tY2@_ z>|Thuf|go&7OlSQwQFdJ%1?N>NA`!9t@0DjmB__j;E<0VOM8MhrAf6pl1z2gYICiI zpQU1SqKQRX1bY(sR72El8t?1IZ6FZ^Y!7j+vz;qX$GylosOEQl-pzI8*QkV2LpAs@ zbXZa}HBv z=>Jtq|3Clv8C3V6Vgb;R^Q}2rV@%z7efgDsf`2)=<3sjUY1sCDkHDBC?H94Ja*0|| zlIg}<0@$+7qP;nOc;<^Wz$N&`L{2IWL6g*eWT5usUE`s_=1A#R=HZ4mb9M-gGd#VGsELeLWP*HL?eUeG zxsY{FpjG+y2a+B~_zcStn&S>m?2FYXosM=996dDG29bo7>yVo|JxNK>f#oHvfuA}B z1J22RX)1YLzgFTbYfXq0y^L#g&p{G`*!6Fo3NAvB6tD$G$H6q|{S+B?%oYqH+?DD} zk)`ynhD7)5h?~=ojN4JGO5Cc}Qc*KRLU1!O`jlgg4?j zUG39WsE3wv^=_?q?tf4HapAHaGgAA_cuoe&B9tdj&YjBsB3&wJDN$zR4SQpgZ;CqO zD{cDfQJ|l|;S-*ZO|6PHAzSp5@RlI-=ZTu4feP^1Zz!cZ(g3Pdv25 z1Z7;k7F^wqh``vDkK9Z>sKFQg2Wa4*V0hj7L*$t*j#Z zBeJo&_bY1D34dTY>4o?7jcCRGBQVMl!UiXtT{3<3l$Xo6@Jqfs3u3=kTL~)fyt2$y zgdChZ%SDMSC2!bFehD_cZJMZ}J=LuJ#pJI^Ci~}zEG*+j9=D!vzK0<3HeshqVoUXI zP<&|cNfb6aI`7#x^B(~f$X+K>@mYhtSM%7RK};4rE#cb+$&xJ4FA$_V)u|!uEj+@0 zDF3RPPo~wLrSLvZ(m$437pwxc{oik7YL(3)w9)+A@r!iWhKc_OK7vV99~HRo-N{dn zC#jB~=GeGP%DySaSc^zg3_s0s>>bj1#IHd^>AnU6<A}iz~w=4fV)(t!bsO;JV)fQ=yIaWC@5af z=~rKQ1tis?9m5^v%?pfw;-pu1VI=zAtEnjzeg(rVn`3!_5G9yBh)%uxq z5Gc$1b;ROR)OM(Cgmj>@8`4;CRI+*bx8@e%xT#W<%=xP2>qn>EBbziv_)Q~hNbH|H4ukIg^vg(eJPwDr5P+RF(>89ILR`~INaBS#c_VVx>If&_h1STVTfiUG}sM~!SOoOL_Ms2t=UV*nhc$-T)IpRH8> zp{jjWaf$7ffGa@C)XF2&xqf6z-<0j*p?BX@iNG< z5~$*#o4EM{Kk3_F5yGjKtC!tKV?O4s4ex5yRWFDH_*G}Qh1bN#nK|3F)ITV0%}CSX(37U+bd5?%gP)ueP4!dTo;%uV&6f=;Sa43MbPIU0^ro9Y z%0MyqoMiX?z0bV9kDVQ)v^8{n>{*;X_b5$N((trRPJB^ty)SGiqfJ!rdj5*s8%Sx(ynxV)CodvOi92$=*&x3;R9v@Shi~NhvCvt6h*+)8> zws!hsBy4xgh%+8s$+N#}iTdaMq;TL6pG81Rf`FE+o6fWt(s~k@u$yWdqCBmg747qD zefp6b<#pJMpP$VgTe#~6D=&#WYFG2d+$hqpVqY`c_28C`BrGqqpwucipJtq&Jc~`k z|5twkP9HMLQ2LwMYfzqS+diK~wVP{(#XTeOSgU@v?)tlCW9yE|OFIPsqM8AjzZmm4 z2&1j{&bvb)_|`zmJ#gH|x?F=McX!I@`*?A!LOfRYwx(6-ntRW%of;DeSA2#uaDNsz z17!?)KDUv4WFx}?LpI8goFiUh9%LdbX@`x*k8`K$TINy$oLd};`uRi#?5sLzaApGT zU{wwy(aIj}77ew$Q2Ku0KBzYM&;Xe;|MXp$mRB=Ir<-&&XeRET;&_p(jALcg`JfHM zb8Wyfw|CH;(Jz(C5^Jwi)$yF+8~cuWvCCnWl-{AF>Hdl!`S`gEWsdC_BN_BDQYuoB z9|zLLsD=ZLfxjqkrMkTdZ>B>x%~|o)E!7wPd~!JOydU{x+>!xvfv3dmujR#!_nT{- zpq$VApsW*ghjfwB>>gp?`Y53uq%4H6FenhZ!1fg6U|e-pDdg8qhf|s=h1dQ3rE=&Q z)~@s=jNwv6K?Wv8lV*i>I_C)RbJdI+DGQM2loD3Vk%a!~bf=lJTTkYC+3Ty)8KKn4 z?E9bqHkdO{gqna3-$~U>x^A|luCKI?FYHAitx-D7GNeIaU8OQ61Vo!;O9$lOis<1N z5qb>9wjwgbhI4NXQa|(NpzKI=KTJbzn3KYe^@3EC6ofv z-J`M2^=Fbh<%u>Z;LlgKi*cGuIQqdJ{-zd$J}{)++BR$a<<|3w)F)w1Hp^E&zB0%c zzqbm{GO6FDc&Gi8k($LCw!Fu|wTszwQrbP>(-rL$HwIw?^YT9AjIXz<^r;w2iGBK? zu}+`J)EhBI9}Vq-F}FH1(sTgE$to19V@X45Fqbdlu_7fTNqwxcRkvJ~LNPGgWu%V# z)=WIGOf_&Dt`WyV^0FQWE^>0WJepzL({t$#>OLjxM-~{W_6mPz6~y$FR4+A*@R2-G zb|bgqBw0crtw4=Ez2&@3mNfdNj*PUlk3^ zh$dx!_W#)0B*7#|WMi?MuQoJZ*lP1k@gQf6>`aI9^`rLqr+${^a5Waxe(0Q0rG_4K zeM;zWvWl^h>S+>rdkk!mjynlc?gT z0?dQZ&sUG)@bJ20!}R8N58#rmggcBqD@1*p=8EL{(+x*(bntz*LqYaO0Yw_kUteC# zDcX%Tr`(hyZWC^t*Gk$+Af~e~gA{x9<_*nw==lY1IhJHM{%~`(pr@yv^H~173cSjp zi|cHzPqwYAk#0md{?b|B?8mD1#*?lZyLirMDj)yD`M{CE-4t>*$(ky50ZC0lc16i+jI zwqZ*N0B+6Vy-R(^l*=+>?Glz+B+?eI=G0!Y$Z%YYfaAfpzuPIy1T%LV*r+9TvS=Eg zT+#)I#|5z6)yv_Nbzedt>OzU#{mopJYc6kHP4teIH)0;Uck}EXN2@FEvap?xr|xx{}-SiIat$w`tG96D?)fgZIv|GhE5uNVrP(ZgRZwyiT8pKan+ z7Dh5h&NfgC@`ifRW=^-DfwMp7g}k0BhMJ={$DTv2QrGGb@qWho-nQGNcIIglrEAub z6=PzemPdP#UN0%exXdzv6H`l32*-0f;+j81Kk}!t&)1+*6Gw`2H@b?_7}c+I-Le!Z zTF&;)k9j#3J>FD&7VCB&WFPaX@&M5|1=mW&|E`{zLG@&lPg$=zlyh-@W7RK$P)HBR zvpJn`tI(7M@abJt1}NSp9dAIBzx>5WGfTLegiDsf?KmeEEUHG%6=%_rQAu5<-aQxh zj9moBmgRfCzAXESP;7-JF9df=LYXZg;jMNU6d3Ms4xrP<%5r#Y;Hw$x97*x{4P`bp zLN@<)o5-GQ$=eDbKU<_CA=df-1u-Pk)XNnKDaX*O5DC`&Yp24c!4%Dodp_h56CXQ)+x&2`J zcQpU1BK4XgXCM*I5A)@5wkR5!1gnWH2QJstm4N1xm{QTe(}PvC1|lQ}on+#Ky%NfI zVW~si3f0sYy5Q_p7|kiT{Shc9ovK# zm>z<3p=5Ar1u0g$Q)V*P_z+-B#kWoiv&eBA_q76KwD+AF`&3@7&j8@fu-S-n$);E7A(j;yGQrHRoIO15KZ^19>Iw*8s3DkgWAj9F4O(91i>pS@!{ z1ARb`3S%#8d(1Q(5t)-WBNlA0ozthA2x`{5X%@q-u4Z9i$YNjnvvFJW?eRCRmN6tA z`|?>z+oNw{ir0=T^gI5-Fm9b}mfV#rr*X1WJzCxC9}!vRFiXp|99BE9_?9O}kx6>s z$Zu`7&+6F{lph@A3(Bq4Hqp{91PU1yMa^Fr&1bf=)kpLaQ^`liQJIVi@}6_UO%GZH zxtb{B%YlXGZ51`S(R7`hejq7eWKpKH*MMz^oUD4af$9^HXF*CVk9-ta)PWvv#stXW z4kTfn^8XR=U75bq9t1VsT!@s9FvtiBSKj2^ z;~Q!XKm&B}PB@eJIAufy5UfB7vzTa=b9DVp7gP7C6VBR?IDv`dzvl>&`rEixW1DY{ zBc&f6K*4pYqbD$@EKYh;ZLIUD>W0jwKTW~52ASXrb{v#|gX(1TNWBxC$$J*rPo&WM z_QeayPw|;q*Y9-|Up+hyOTzcD5MCEd$kGSfj~P@2oN>zhA$x6*9VcVKkI8?23_kmh zz|Qfzb=eUyjKyc>JU+@*pzhR#tW;nvl6~uJ(ZJ|c6J$YMz8up3cf}?&I`W`_w&o}U zH}o!s+&yO|V=Q%8ffBBl?I;o8DSorF$L6Lq^QXS#O@{NMRbXo_0jOCCkNlUHLek7t zIX=G`h#mdAeC@8!F`b@1*vI*j*`Mk^g1uX(%=RMp0;FK!cT1W3DbgK-_R^Iuv{aJS z`3rLcqXF1lgP)(7KKFt62I5rOGbDswXm!4pxqQjhwu(ya$^_5D`xcmXQdbv6fRzn- z5AvVg7t&MvS#7!K>q-zeS5a0Pz^m?7bH3wGFVjUoq)}eynhCd^9`%})z||iUJIOj^ z8)k0}*}bC^jd*qeYr^11&nfOsW>{6R#9sC$#-$S+R zVCdWIUxAgP-3f&2q|t4IBK@z(??`=1bkG+_crQ58XrjQ~iJat{*PJf_3n|}07~&DV z^lqKez~@a%lj-i!pgU(z>?jp_sE}DwF|*YNku=SgKc%Z2RE=}cRaSL(r}@=dJIbd- ztYN8c`=KoQfjS48fMw_2BQwLAF|U?3O}&;doyo%&|3eWJQ1wzck=pxkljYu1m!~jF z!ZBgHae^4e?Slonp4bNgiAaer$Q?6vcGgfmi$Vk;I0w|QeHbZx$XjLsA?h^JdyW3r z=K{a_X$!+nmCSctbrskJsV#?{*laGVcKAOibYm8C9Dl`bQ8ES)47p&3p)4QpOXo$D z9xNaNyg=%Mkf&n*KEr6pm4dRJ_i=1vZ#_81BV&`tn<$E(O_rmLI_`MzU}Bd?>>Bu+ z|3zee>`iaI<`=Xm0b#rRp?m1~&sluFh!U|@1ZI|}9#=xK0AAxlt&6vtLdeOx#P`zT zLw9N1R3*C%7YxtG*7`qd2ZYQ(d^uNS?@V(lyo|b6Xz**NE>Ns3nh=Sv*lN3KMQ(5U z#)S^86cn#Sd{~kAu^A#3->>Nb!Y%LPjDf4fxKu4ne-!XO)viA<>+2AQ)sG9RHF=FT zjYdOmGSNZS=t`~LLAip!tUuCe`ufFMrHXs~xzL&5`p8K9qFE)bT<#L8M2@_nKMNvd z+HOoLPQz_l>#3NP?#RePHJr1b;iVz~IwwUDOi>|MHBw6*J}jP-LVQT=JZ|9s;WI%; z8Qy>!sDlCZ$;QV82A3+9BLhxyX$kcfL@#V&RUZ=Ukg#|T+UU90ZNx#Yj5@aFGkes> zi*BcJIg{pq&OgJB{cN$Zdu>VAFUkXZCTCtM9XN5y2YgI&Cz%AE%zzv?h2&c)^!&p0 z!zL%vz(mO#Cp_F?(E>z8KWv* zN2yUG;?nY`n%OeISBxXk01hm56Fs&-Ks-BaCH+Za+=2Tio`R2SAM>0WU+9@Sj8V2^ zxvSHhSA2D8!k65ON_Rl$KeM?`B(LSt{;@z8e`eWH@uEBk237btQ>f1*4=!h#8QA6hPx~wc8=|Vmo?J!t9!TBz3yoDQmUNOWR zZSb?~Si${J9bSg|Z1qLXx*+0{s6bwgr&gj%W%9N}eWOHSLF{Nah*942S_BvE_850> z&D>axU`ih7^>@ZM9|U;@;*Vh#yXnrdeF7*C#E*Tx!~`1SHjAr%tt|p@2C_^9aH;CQ zu0ZmbFZ3MYM#JC>f=}^?m}m?afs5TI_G}V&tyn4B52gJyXVkS7%Cf9O7pHhw8v&@v zvIOl{35f>uI%FQ-Ik-j^E;#IR>~S zP9(oT((_ZEB<52bv9tu-d1G^Ko~~kl%YNlvTa7vAl)X!!w;;nJbu#XzD88vH*v7ht zP8GZTnC%XBTITXFRLB#NBU(MH_~opbacI@}C;brk-Cq6R*k#prT;8QgL;AWh;YR@D z`N^lw&2tywFJ`dW4QNo`l2GjuU43Dpu|?G(9$8?&h>pep=KdqVeu>9^!^suzOKCAe zQhIJ9O&WCT3cF(57U#_ayBmA~>%hb5t0~)ps(93;a+kK)GP$y-SyhnZ*&x{^3~7Iu zP_O{`9ZY;92iKv8^wRf=8qM3-s@^ZkEN1z4K%*wiFgV{KIFm#$)8@9!dKi@am0y&Iiap*JBQTVt?;iVGB%+FThWeDBo& zB$7 zRu#7?#sv|7^P28k8g1>g-~{?j+ezt(Q zj)dm&c8!T4P#mfCx^ouR5cEJb7Lc00bS{k3@3o)Y&m@KWxMWdDXiC4z`S zTIojD>0++|K5%u>3ubC%Tp_JP&xGbxP|1I;OMS{kVW%d|GT&}l-`S0^F^yw*D>=|d zVukI9d|b-LZ{Oe<-_^S3?B}f+B_DChX%xpZX0v4;)_4bjxuA7=hBZec1=m{mc}%ws zi>@tZQ%IA==dNOG(8xwdB=@(>tyG~56Ss|^oL56^X6 z4D_GFQlY}o;Z;gJw#L@0C|o18V1Vx}XGOmA7sr9*7_>>WwSuIaseR+cB-ypr!r}ll z_;yK5D+_iK^pIV}i;CT+TvUtgF z5JC0V*Bi_1vSq*Hn*o*&f*H@jwJ%;hCJddrGAe9waq;XdbtqZ&v9etJi;?;{_Y-fj zbnrJ~K-!KUOI!q*X-*#JD?h7TZf*~1qRB_W7~11sbbh3X2^@LOV1VbWTXm7Kn&u{j zDbF9pjY>b9lU^cN&Ut%&uXX4=pS-YKv0Pwi&pOew=KJ+O=l*OGJU+ZrJHqx=LX+Dv zf}c|*sY>nY?1rDMJDUn!^egXs!`Q*6XS(h7HTBB8{|JaFvi>88wRo_oXTp(?vSplh z82j?{)!K(-1=;x13{;?9fG1_qm5*&iXNXop5#>>9PU)3VxM}UmEa+{=ro{8>45xn= z2s1xM#;qKCWE+{>!1r%2emI2svqJ-@m;8*&NQZ(yxfz6Z_Igk7CxZAHETs ziHuTYz-by%FoPXwQ{^H)`*y{y?pr8B3(i14yP|mt{?{t@FTKeM`wrVKnMku>X1Mx4(vE-Qz`R=LfO7V478PjuuN*iivxa52RU< z&7%u+Nc$V1_ywXqOHMknE4%u+F^SZ753dEAdYuVJ6lQ> zHB^j0Wmfb}ynN5C4fTXS`xUDDrqpi!RgWgB^f<7Y?nWliCGW)AqK0m1;}3|38FLI) zx*HO7@y4)9H7Ab#y_V<$)i)Xlh<;H}W6M=c-e8q%$N3fEq?9LHMcw?ok@~zqER0_) zKTljwO?viL@(XZU?eJ_!=5aC`*|!Soys4#$@f?~S@UN<$4~&OiQ3$E;&%&7*uby@;n80fcvr<#M zgO) zehT`S!{R`zE0KD-s|RdVD@@;rrz|7bQ-23yJo>b5-en+qBY!JXIX8~dt9Y=Iyx(gn zC{(;rNWugC2N{sfLC~VLK3#f~yew5~u^@24AujR8?^))oV7L!5$g^iE>>B zXZZOizm54I{C-c_FEv!2t@6eW7x^Id!KK?dgRi-{i@@{mG9N)oi#z=1!=0=&lkF@x zI&Jh!GWBQ21ut^4hqJ`(iuP`X94Xv0ZFnu2UQzBU!ZosYb6y$mJ~E5}*Jza#x18UM zKEXFO);Zy9lw4mecReA|j$=mw^TQ9Pn2*Rrq>Q$DnD60Ht7tK_^d+6 zWJCGt9NP-t3oP$X%|h*uaxWmH5T8bgioZg|EtVG9$px(+9wUPq z^^C1TuEcWwwca3}${=MvE@$kozMezAEsCJKPgeJ&*$Hbnp3U6odX81evj-h2TtDA6 zEy|(8%N8O!y_R21a1M`t7`|?L$^0u%C3wjnw*>eRmxfnreTa{x#V4HGWlT)>sz@T` z)xy-Ks5*F0|C}sskE6xsL}3*%LBGAO5BC~Cyd_I6x^8U4rM1^mxO8tY$>91fhco^x zO8;sP^5+_4U>!-oMW1QLfA`Pn_>Ul-jqJwr5hC5&Q3`7@Qg!&-+2kQ*$SsF8ph|&1 z^0v603Je_vF+ZE}(&fh9pG>cOcuWh07+>%wjOM*JKkgf)GQ(x|<~*wBXG(5`H#f!^ zH)NqMs$c=`X+B_jCn=qX@PB{=q~{-O&O$rz@(7GOeQdWUdYa*8X?bTIb*%GMgZna3O$ zwmoy(jBs6y|2BIS-v$1jWgRLO@*Vt`l)_6xWV9yrtL(x3t5}+|rS*J-~ zDfoUkLUgi8VekTYYEtCz2ZQx$=YqmJ&DjK2%KEIQOL`A)MsL2;`iz?__7gvR!qf4x zJ%lW^UA+YJ1yBjPaX#IZ;`RxCKN-W@`HF0id?s3JYE1^nwfG(GaUuqADx{1oe(i@X zs0kPHdtvwTA;`3u$pLj*_c~mMV&vLf5UFL_A49i6ZU1h)X z6LqKg#cjCN+r%yD*^9&%@z^vnw1p8;DhgQsB}&ftdH)fT{uvvzK5p38@0s}Wh;jPNFwL$BHV2l1aOBr&LH;Pa^o8q4JzhagK`%) z+VkmHLPsD{Sht@QRS+jvvqzx*@fM);(Ent4D8v zU<^zWcZ>hQhhhCNZ~fW6+1F>AJTpmjmXXT0FWDx343S|eAMv_n-PcXwaiP`xo^ETL zAMwm2J*tC%Qk}d0{eZ{`j%y$v*`VNT_|>*G6TB6q%+ljwKb&B; zK)ViB9e4}!dz~!uYmxU~&ka0L{i1l1@=o1uJB=ihFdz6d&Sn0ea%y2yxaQ2?vN>WK+!cgZ`Z;!j-*X0>zyj_R1##- z7#a8Tp6b{{Fr5S-jp*&)U~r*~5|d+Sq^tYFmu=}n>q#|^AK6)C-?D>B`xSy2qmgzE z;hP>bG@7-nA(ZOnF|G$Od|E0(9{!J=WGkzZoG$2+3R`)n(@<#E;)T`=FmB$SjQ*_L zCtEw)c9rk;Rh$kg`t&~nMqG0+jZ=+`b;E#KG&7n6Hg!?uZo|z%5xUZTdOvaEtN#AJ z&@$D{WDn~L8-BxI6!j(VO+8HyFRNB8v}^W}#ovy?0B}E^fH%>I0sHd`rjY>MDdx7` zjY&2`kKjrr3bpz7q1oH@K~eZ({PH&5>v-X{C5EyVnRdjEP6>Y;@7>}ewk*OypeFM$ z-<@@TZta$nS&#>@+{y39eqwIv8Q;S{TgmGuxw;qlX;kva;v&f5_6{kfsA{PN4S}wJ z3GmN{jcQ{+-;Zo~E1YM2B%!R6l}rP1O9FfsVzS;zWCWuG62BV}IBt@IWvM>5*c4N2 zp(CQ(lI}l(w^>?`6Xm}DP)(*9JTL~mjt!w2sNC((HgBkh%jZ-K_GjQ{SKFbu;x(xr zSOk7l<`vAc@6UM@yMC(|T#Hw1_HwR9@HdM`yn7`>9QX2qp%*>dTWO=5Zn$StvgjLU zt#&~FpP9d9|t#)Q^m+}K{x64!QAqV$$oL|*` zkGC(*Hxr+4l|zz)o*D}-|IABnzdEyZU5lG8ZJD~GhY_G-@iOM_U^bnCJ?x3>E9P z*i7rkxU}?TWEuPf26gy1p6xjTbxdC~q^!~Pjj30~(vi4xU-gD!lhh5u>Jk?RVO)5R zav_&&_7vsk01CYr=J<(Lo8xdIVzl3yr#2O<`ZkgDsFSi=+J|ix-UDCegs@rZ(kT*^ z@KE_j1>V3HcY7cSW|^J#{F&d6LKq_}DRdIE z^&zOIlpvLq>n}zC$2RO=6clcq4?sBX)k8YToYLT~RLt9v?bl38IFFIHM*PX7WQykvB(Kl30j8(GF+yo3?*yL z-y)^0R1)US$R4QQl$l}xi)Q`t=xwY8F0|s!A@i}d0K2y|-|Q0(rpU%8NjRg0sZ(Sm8(qR%7b`~a2c@)NLHgl zCb_%zv!1W9)xy61Mhl0YnW^QA_|cBbV^Ar6)Z*2wE%CdSm$W&KRVrf|3FX~`1g9YZ zMnk1C3ZpZYjM&4Y8oH+0jM?M6 z=V5jIIL=owdOEH-3jP-$+fM5e!=W;x-sztgTW0lM$%z`+!^=!jT(>U9KBlTwr*t== z+moZ^7wWCbD_9DRYT$>bXTZG`@<|~1Z#XS&s%nQ{YLBH1?k03nny*vQm zjG&Bl&vpB1r~TzxvY}QH<=;9dOH};X`bIzm36@>zY zFeKssY_m^RC2@yhkH=A%driV~+sGTX$L%yqd6uK_o;ya+{4BQiQR1Oaij2mUh&h|L z(@rtB<0Q0vQu9tVN>^E3$2o4wrm=bYQKowP%11#i_8}Da2KOw`5~UTb8MU5nqxrcd zCWgR-lmRtGnMN}fsDZwnt{k#79@^hh>wVyJ$X$wBX%fb%Jw4oYk&hX6eoTo@IJIF| z9@SHZBx(rusqUtk$WG;Aaj4gP!*5`Isi<`HfKK6Kw=O7R86@LB`n%|wnu zay2GC_jH2~#L1)QxH%evL_~P+SZ3C1(DqTe#+pjr#X7OLl>yz;b%IzO8-YTxc@i*2F5#xI$weuIdSA@ z@3RrX^cx#wCs%sGZ}0EVNV!2rk?#~tW*eM$U^KXL4Kyg?oF%}#?Ys7b7tR#9wPzC^ zOKqD3{$VME%VRrK##?^#k*-Cw!Q_yS>{fXT@Sz$}A;HP4z?ygxqi9i&Fx|zKZ|ua{Ca;?9^unp`YGwWs#bHHve55^@{59Z zxdI)DpGl-aV5fNV$ycjdw8wR|YJVpa>W-d0%^5wf3hRK6L$on&!ac4r;c7gjdP>Z^ zd;Uf!`IUw8WNnyXu)B(Ax0MYU`Fgav|0B0BC_Bbu#d62rp!Y9rqe(#&Y1Bi8Pw*<4 zj@|6(WysSGECL;0aZIFC7Q(;@xqDl(6D$O+_v~7&OQ~FEdkCO#G-7LUoQqS@yd!dR>g088Um#wYt>5`3)8BnSv#O1;#sJciwkfc*wOlL*oZ7<+b^NPM<4@ z^MbvLj%IU8_ku~iYpSNtW-KqU8%7O5-*dmf8U zAI(bAUSW8sT5NNdyWJ;q2QFZ4XhdvfJyU9(Z>Kbs`V`3gV>_H&y0NetcN7#^wcp%Q z&QcZgU94cv+G8nU>?2(Am_b2xSJ$kEh0cN4CbnL-&xVAAyC`UXF1n`rvfkuWV><~t zVM*VBIqv!u+CY4<#Br@EH=DqOD~dCK@#E^m z9{#@gh%qJuFa5oKfOd2r1fGqlC5U-_JCv~j$AIzd;qHoT6xh{9-7-cKVD!rpE4SnSfgd$wEcGh zE_QWJZ9vb)-tp|sV1n>v?E9wlE|m%1hopFc&b8DVyixyJlFB;&m1>*ckuk0A|5oKoD2ecKS2!BfYe|PI70UNqzco}a@tS$ll%W5k zYAxZ5|HZDEr$*}%{OF(YwE&HfiNsHXL<`VbZAIkE0G}j^$Pu1O24?)FCI)sx?Y8dT zh~|ny@=IrY96x&dR1Ee)Mkn~gTJWj?HnqpXzh8L__4sYX=Jvx-Gm_0u)iKL6K z!*+hOP-Wioe0KnAv75lN4~dmA5BjYEpm%&04r3V^E3)_$%w>LxF|&$e#7~!ks&?m( zQ2E}wS`s{;B13&0F_<@e*22|JA1_0$>3<-DiQ^b<7iwSO)7C&OMWUBNbgiV`YC z^CKg^qaKOMW2=AM{vOXN!!z8@`8HFm3J)J3UV@x-!e!kKS=N5Zp`^M+fOXCFq@}wv z1M+<&PgGbEOe<&&nyonz>pPt^4J-T+)adR&TWW?D7!YW3ezD5fAKNvDrSpwF*1#Olx ztaKj{)dem#1Uj$fEP|2S9i!pR@gZIoMk8ZKNRc`4vR&~65!;HN0HsnV2W2$W>YwKz z#8azb^6cEfRrw|fdZPLF$T1C{Hvyi`7XjBD7x)dKzdFr9tS|a3%Qdoq{(CoxTw`Z) zZh&3P!Xg)q-$wg5(1PVWah2 zlQCHN_l?9v3ki4cV=!tqlbd|+pZCkZR~kTty~0085SYuba>V zlhD52e<1PEO-t!$s2k5_b;OTzZp1_jAj*ZKU24jrX5Y=VP({3R0WngZ-Vi>s;*hgJ zya{dG241b8J8KZC&oeJ`4w93togCp-lbV6`43N~SPOD&%V7D_B6p zRN!XrMbz}n7pC{vo8vbeB%3w$Vv{}hHZ}O;h%Yu8Q()*b%-Im;q`Fuy*ks%(^Dw(= zIkHF{HUs#Is1E+3_@LXZa>rk+vr3t5e&WcHT>bR(i6}QipEiERtc@c=3wN*HZ>1~7 z!&}jdW{Ypi$azjV0s|*xSS*^8aFqA`gbz2bZFX~N-M&6mo9r=-j6HWqn>VQ*S}f+@ z9y|aj-MsN9iW?k~FcJW*|5H>qFh5sR!`a`F30Fbf-s8fHAne_SR8FkiTy)bgU%#62 zkS_8M!L5OPzUQ0@{5x**>{t@KY6%jifl(=Md5@tm4fGsw^CUQNan0hlR_y;kkJoRK z;Ryn7=s#0fpH7r-Ftxm4;)>&pua1C+rO;^*q@&tH$BsiXNbL+ZTM5#L>zjH(A-cDG zYTBpRM>>r3zcT-(G!%1ysxAB z8~>toVB-&137nMEz}>;n&Kf9--tIv>RrV(70kQ1EGs4d+KK)+k;%2q(gqQwsB>^2% zcFZ35_QVQpTOuaU;%kzM+85J{mqgpS4dcWV{xK;in&Qgu$ab9zmZ77~0!c#&_}2F9 zMX%`JknU$c0ZLzr@9y!Z6gK%~U%&@;3YKM4%-GjKk?qgis31%MFxUfU($oV zv7c9^(vLt^J8nO8ms=RC4=r)w^NoJNJx?tzVgo;BGlaj07IzPlx+2(s@)B)h$>oT3m$Hl${4hjHlf$Z9 zx#6K3D&)a@>yck>jF3pXsiTDn$qkMe2@i=OL+xor}cHRClhG?0&2C zooA-;TzS?n^r@C;nDx|A>ZI9W_wbGj;`7P++EG4mV0GPWMz*l3koDRuZ99HO^3ngy zzHoYmSqD>g?-xbG^o11Z_E%UET)njLmq$9wB*UQ6BJ_Na1<$nOWu@ZvQynQ}jPSAO z94C+9B`bo{H~968=>D#GPVXc2y5k2c)Y5)rJr6JB??Pta5w7+8Z6d)=i-PqFk~526 zm~BcrQJ{&Y)tE)RYsLLEs4)19w(2d5khv}Iu!Z45{>msOE)evsXF^*lY*}J@x?V?r ze66?LoUM|_RWzHRfXw8N42{5Ni9}+8l+l;`XQ#Mky}f@IYi?b!W+=Cw5%SJv3BT5< zBU;Ji)E5sU#WYria&rQ0+Jaw!N$zNIo@;L1nQV`CB;6esR5Bk}G(J%2vov99VYaQl zd)59P|Kqm58^Yon=$rf8-|NlPR(qN!!ZpK&mEA&K0kr*5r6eW2-e5e#6?YL#dH3`+ zUQyL`K_+j9QsT@A|G~fLxUSxmmDKeG!KF@5@fR>?HUsUGPrx-_oq^dSo*J;#2Lp6Y zK_GhMdD^yX!47Re_>b?zf@qXK--hmbFPt@gGY40kb-Xre0_%^e>3;?09~A zoi=s74YbunzGY!C6wQ%g$QfmwYkRMUVk0y(Df$w8N1AyJ?6q-1JOOGc2v)AgR(R3- z(w>siH?~+q1U86?-RnO~!^WoI_x0g_>9Vde%K1b#_Rc?g9UGb-+rclB?II`uK&pSs z#q7_ri&~|+R%^ju+MnQ1eXQb>UWa0n*A{nEiNFGDP4PdZUo>ZJk||UzdgHWTvYv7< zykjE5-!X}Up9goUw2RCK%H8Iix5x-n$QIQN7VW0|8QD~yl1R+*(AD>hJIFn+avYs6 z=4@Z}Yt1dT9lv~iZ3QV;#rPfyqJG(TpQ^h5Z^WHdP+Nca=AqD{g;HFK6ez`uJCssf zQrz9$A$SWd4#6p!Qe0DtyL)j7?hb(j4G{Qk{`b3gJG&RTNisPznKS46%JY1lVw1J< zNC)X1SOQ7(rj484L6=!z{!Y(*@*GN_WHpLMjf48*7V z+={_{5kOWsjb(8m03N$23T4KujZaaTc_t6?voU#DWSz(#aDvp%7m)nL0UOyR>iVHT zRk5YCI4VMjNPcvh6ccV>vuf@C55otPT05YQKIE)YMBb)+M3W%x#^UYW z3nXo>E5H0mA*F|nH|;TZitpE@)z4%3>D&IXPW)_9!u*HATrQrtx8lTnnZ{)~ET&#H zixFC%^&FNPbDO+l(WAIGyE+?nF5DKWRT(K2nkHCD=m{1GNM%Y3nR5J6DJf;JP;$C` z2E0prmSG>6Pw}@krS7R6G{dlXaI-Nbqz|*{)FWzu*@GkSH61*L(y%PjSkx~>!%Z=~Yx{X-%$4d={n{)Ztfp#p z5I#y=c64%{eG7>NH_mm{H~k2u1_t%}X)YmCl@Z)+Bjs}7x7%y*59_WH&)v|>q2_)!HJoyZAE8RK!`I(MNxn&Y&f-vzTX1+3n_hSvLeT&?}YILl*riy#GeVsAX%3(N*KiKoKo2M#%giCPSigwP8IdJ5xy9wR%R3H({eLBMwp6 z8)xZF_Gz26b>ZCpoRNht9d$>3H(P^#k{H4nhSawJZ;Y#Jvu=z}6yLFL51X9bhe|ix zAJ~Tw$n{}=J`IanZid7$yg;!b3i~dO=-*qi)@F6MRf(W|6aj)-He`sgXOLl$|=5Msp5DmIFDcIqTG+W?E5eK3! z%$F6n)w1XRWys(O#S}$)os*w@_;BzY`^S(b5?YE>e4;Vm!NS&??WA<59?GhlY{tTh ze;qREbaglZPz`;LT%8cCo+N0RUn;KY090N7he6@ci8ng|*?wa;EB~iy;*Mt*x9IAP z^Nu@Vo;KRG4@)lvo^R0!9EIZ5fll+^&M&{oX#Gj3%E3OkZ}A0dj->4b&H6l*+me#L z6FR3qUf$?;H4*g}&pox|3c~$1xOy25P`SsEt(~cNC8BlZm{=dui18b8b;nK6rn7c9 znrYi?u_@+;9)nUdh2+Ow(J@kFN41kQSyZkGxZd%xcM^k?Nx}^wuBB_7i^C89RBwTz zBW&0A`U}fdWAE~O7zk;E?=a6~w>#OKxwSUFaI%&qzBXcDhZ4r+^)=s!-a@eW{L-|H z!R7GpVjYFKowSZ8kWH`q%;LfDS0$Be6n<>S{d<3b`VYhR>#0JJy%aht zV2-f4IeEljarCp3|1)v0JES;xKxO{VrN{n^@jXN3RIkOQ68l8{pFs20g~KZb;$V?x z#3YCtB~h~89gnC}6guLoq1gd zh9(+Y-1$G3!eEZqH=_`&kR9L+xbE7r9-m-~?^6*9m@Otdr)cQNFTXhh{g2zl=&0^! zpIXI$miMOp35^i7yD&52VZu$B6eFIU_C( zNj0zRx%|XC5~}p1RPF98;UD%B?X9$9`hPj4``=s(=YS1Gp1N%xEkv+^6U+>UT^bCO ztteMJi`{7%kl%bPx@P2883;oG=Gl5+XRsUyR@4Tp$(78zlZRe8)9+rSx4?tJx2(e zaU8I7A878(Eh=?)K{#$SDJN>n>a;>TKD5ov=Uow&!4P^`qUF6iJ4Rn#^&LvD*FU?e0MZ#n+^zEvu_0~&` z`&WcFjekd5BrTX^X1Zkl!?0n%O|agy9sKQzy2&wU%a$s9K4MGFHsx*Z6FSudDMr0S zTrccK4-}X+`-x)Ra+~vp4mI8^%bd<{IX@^<7_f#1tV^HnJ7VI@Vq+rpGh>SX?e8^Y z9*58?#-?+O$c4DxR=*SzT=Z!Jaxo^zuaNAk^mz@6CqD&cUv$w2d;CG#@0^(9jg>eV z?M_gkeuHAL3mtnWuf-mrwcw z^Y)JWh<+5s;bJ{5B&B~MZu&RBBd?e}sZRUQD5Ei;3!&;2G~IjGY93;kNmRTQwhX$- zgC`_pF0uzY{Z> z{sMRFKi}#wR)i-u3$fQ%0%x${2aA>KH^Sim^2n zdseEIYmu=5_k9FpR(~DPDVs@HY3qbGp6prgyDwAwx?>}ciXG`iZPPmHBGQ+axQM%u z`ZyKN%jxr@rXgJ)MBj3^4CAao(IJ=SO2Iz0Gtz8|TPs0!pPO5};tiv+Xb=<9T@#%S zIlYao6m?}sGv*a4Li+yT zv-o!U!Wxdx>c5hEWb#7V?Y7TAJ!~2Rddc_62=I+JJPgLXS>DNtqHC>h&PGU{j8o8Y zOe{YCYJO+?VPrHP?|v8cY9Vm4uR{fx!d|Fk5SPGzWN5}O##p`w=+HwlKYRrCZ4$>) z8o?WGGilR}+wlG_+$s^T`A+UC;7-2iL^L6Pz{75?rheCTrpRCitHugn#JB0@MGs*WD$L!u@YZ&nM

QVs2_tzDbHC0qHM-z{BQqKVeTG=y%b!CTZX?PrkNR?RJyMnO{)NHH3ytI-kPF z(yvnL?;IM496jWSjR0+`O7*;E5`i;5+Y!(yEg#7cM`A6V+pR#mWW_Ih;epxsaGY(& zBPyF6Q1v?rdOQTa1Q-c$@2gq+voD#8%G$YKkfeYGF^pY4HysCUU)M}-_kPUhB6eFh z8FSnl^-enQbb0Xjh~k_) zj?<~|?}gW_E8pDjcbRoy0p zJekd0hQF?${=84T(Si4OQ;tnJE>(?BusjddMny)_?l%LWmB5_-Z2i7-{PpA3^nZRd zW_fP2lajeyp1($99^J0b6*!62TY}X?#-8vl^lxUNTjp_SB9s@no_I(d>m~^4SOIJ@ z4n|vVxM5$m&g^Bpgl^foEK`@eXxgdKLIB#Q4RF2d^;rRPW@{!)|CSTXF}O&_;+X%H zkONdMb$kvJLd*_;8^MEZAtE0e+ghjPdm5$%9eQ5Y!)<&gL>l=+Uf?)AYCR@&+2t@S zZETr=OC>!=wh4w8LbOf^Gt(~Xq~mf!$;m9x&WJeqzxXL>d#5{XD3W6ayin>xwBr&* zT7(Q!XrNfo`@;3Ul)9U0YTT7cPuI5JZ#QC1W1s$q^Tk>w#49kxIUjZQ z`06)=#~Ok(_NWEUs!fjKC5xV^CV)T-j z`7udHta4`zZdi=xHNn>^#Oah*`G^k`yCVB%EoqY~-#eg(X)GmMBeBEJvi@8v=CXL$ z-sOhz+tA!o!}4e|`4~#6@j>yMukN7A?`9NT*!9U=w`wN2hMB`1(Ys$KHpmL+JKBfz z$$ZUpRieU1&ZF~1;u~L?ddk5v?sM2OfcVIPm$&G!U1T`xOc{2ReOIa!DUqPDK%KZ~ zlTI=LYsQYxy@9NQ7iTwbBW3K*zYD*eaSo{AnNsyGdnd9kMvWZ^vB7GRI=Q}AmKO(= zte3(YnP7mw_VtcIYly;CI z7Q{)p?4#jcmgP5ERVeob4q2xsCr)O1Bp4u?hZSVWZxv6)9(eoU3_T+L+28qjV;W3r z)oux^xjryrS`!w<+xQJ(0=NGb)7za)+BRh+D0>^l3qF!uxqlDlEJezEoUM1|Sdft7 z@5OFxY>N44up|3bwA#Nn9AilAU4B*Br`xi%ra!wDbE6u7tTTTF4t9B}P!4Dniu>0w zO-Zp=hPZ1kvA3^3U$5NjayzS#uR=(I2jkEwN|}27K~rn9#GvA-833DVeY53H*Pcxa z4+6T{GlBTL-<=2O=Au4yj#+)y<%#4_Ka%t0S5xck=hY%9%O73qGVRRM+7I)0I%I=} zF{_jZSL0sh%YdBUE2tiBH|cW%r&wu+5voZDS`QG0V`>8*=#D1lcFS?VY9xbxSF_1} zpT4oTD!z#c2ABulqOYXaY5tvUMBlt&_$!3zPvz`NEWB=oi`KnW#vHzIBBdP0DIv`G z7mHA757Q4O{TyO~ zVy)NodzYP!k*diu@`-K%s`(PPpxIF#c32+oicQ|a1162~jfuFWVk|lSt@Nz)um&bN z&FQm^u}0fWh#hi7dMLBxixwZ0cn?Z95Q)5>HY8!;2pGQKe9$cd>RwclN}?T2^Gf0CzI}WkoZR@`*wMtTK|Gd+{JA)f zQT<%YrqBlIiIrywjr>n6zz|90t+t%EnCWEgqEGN$@B2`p%h%&lxrv|_n@>6sJ>wzz z0)lbj-e$DuJfbMIxxrc&aC5VTEgG_>f#%vj1wfj%Q2MO!*+41%a->L13wGs9q_eD< z=8Rb$!!UZ1@YTGrqd|MRoZ1oIELNWC^KMC5e@W|x^^3g!Q}j1kTGdWd8!6B8`zPPILs@HOP1%=de0$b_k9MltKLkRjkQ|tu z0$U}IRK*CK7emQdhwwSVpkbmsEo@DDdOJBJPw=ZDu^ot0Jw~Q83~eJcI^5G*y|~*` zU*{PT@-I5fK(wkcC6e^nKKtsuIuB#}GLx}p*^t4Xo80uP6d2J5cW&e#Jo)>Pzp5xP zFdoWhX&vNXG{2clA#|5TiP;&|VR4S<08*f9js;}9ofL+}Z1A5EG{xsPOgig7qt@bK z))f_VA-%dj|FUwjJt+Hfb@3}GZTLgq8=MZ0<^2MNSRsPNlC%MP{-;x$N-}gs<|Q_{ z6|3u1>0m~T!>$v&fq!hY5Fg_yp|WC;rI<0i8ZMBW-&cS-*_}+F*+$y;Q`{_MrkG63 z2v)VprQg`0&~_#Lc633YbtL{~c|5QH+p`oz(a_5L4guW) zc=g&@rhD+hjI6z2@4CcqTsfW@B-_#5H*?N^kEOyl51<7xrfdHfcLDVPJIK5_#Clqq z6A0)P_w5bqWvFy|eK#tCW%9*|e6-a%o%8Bj9m9()yuc+;1UyF(xK5C}1 zyfnn8?cAODC=AJ3F(A?%MzSH#mkP^&-;thFgB`5+Rl4xye;Cgof1P*HV-_=0gvNI=P4S*V)K7QaQ~ms-2a!~ zrzd%E59V$`HarL++mvUw41J9aUKHC2@1cgNR2!tI!_y}pbnkra%S@a?X=ksU$v9oT zk~0SJUM6}Ug%j_cqfMp5N&Wwm!1Kj0s}(~$)jaAI@Urg@$`AQ%3qWihcaB?A9pG7@ zjK?ZmSBE0fTFO9j5I{}cZW3{S38y&-9>c#j< zZneQ9M<1{k2<=rs4xW$E>+aT@mp|N|U5I)&h*x+W1qg0re$KR>kd)Xm_11w;6mFjw zJIsEbF=cK}&QhR8_Ogq)ck<&{y>vth?*|B}zL9?C_cYZ)qS7O6^>0x|LkS`{>-+eL zz;AZu*yeA*XAp*WZBEH%hX`hV2&c0GGOr3~ucKNC;7|;gPZ4a1W8}ZG5`Y%l12V{& zTTKJ6r2-d&<&|{M1@)1wt<7y)$Mw`B22vG1>W;VEaPzyGr||8aigv zHItTYiBdzX_t#%4K1v2*h(bEuEGe~dyY*t#s=gwU^(=dJ=rZ($`F;vd{}jxUd^ z(F!Lj&NY zQuD{$>k$2Z2BI0)pl)gN9RIBm(|l*XL?hwdB-}|zO&~NTruRX~*cLjmwtjF#tq?#* z&#u$Y#x`S`da<&vnPY@Nr(4~SZm={q@+)P;@UkkL(tUsp1%mp;&&o@Tm(0gK9<1p< zyAk<;kFKlp-rew$04Wu^W{V;(mFBJKHD$Bj%JhQ{yLU;BIl=kIoRxLffvgP%uK%1; zYsk|`Q%XD4gV!81(7cB(uute%M71J0xn8c-HkzBSa~ldNpFqzc&dU@%%LLR}7%{~O zNrrbHpxz-NzmUHfVERC`Ea9CpiO1)!yQC{A00xyEcd&BZtX0AkP6Fs`n6{Y>Qg&4l z_||x(Uu`90KCeiJLur_qaEi*%ejM1#5K+*bm8SZ}M$IqUBsMBk|{_;ODEA`${r`!TX(Ldd0FIeke-ZWCt)e-+DB?0nZ0 zQu^I?prJ5Lv8a(Q+i!sRyX6bf5FIblK&jtvbaqfu&cA&JRiinRx=Ua=KRhaLt`Vh77V=HE8KQJz2Kcz>?^ z`OxnfkM1Xfsi#BdJv}S_Vc~+(DQlTop3MJ2h55VYk5nM;16NX^w4>{;KWBT3ZP_3x zMoqUSS8J5Y>Xgcjf1^=~9Zm^P;`*eL(QjGs??xlWZsK|$Ah$~*De7;xF({PWkJ zhUPhHS7CaA)|6*r zl`Bm2asP!f_iDbfj1j8tu_Mf7>>OnF&K(P=vNhY&8q!TVMiAT@f4l)qUJzp5dowAC z0OhwEKFA?&O{cip#fam0x8VE9){j}PuGj;w4Wj!5{3Tj#Zn`0bVVmHvzh%g8zNNsf zezcR;aXJM~)|jkBWIduMy>uv5x$)(GT+#om+E_+9ZH>bjzyd1m+RWYf%OCs!z8Wfw z;EpyYJ7({Eo)&!bCwJ4jj_%Ujk0V-pQCD4hMgsS>=oE|7v)@GB_^s6^&<xu+vgOj#Z&Ed&vU=z&pLCyi4VieYEs@Q)Y{h&S}-;t2==cTVl_o*zbhb!p4(BYE^ zVj*smf#BHasi{o1^vI7y5K1W^iXSVH`X~Y2+<+B6=pD;65cfL@u?iR=tpVCZ>7^rs z{ZH%$#5-1ES5$8*GrS%+!-knHiq7x|Wf3#32hA;RO)x@|_yFA7 zvGQ-w!}$kNaZ93_O%bfw5>qHLU?gPAjN8(K^1idGJdq)%?R#R(M^`&)$(9O?D%B%q z|1hc0ZB>sQN%_z1JsR$n;-kh8^8q2H`y0DZ$mVqF62Wdgf729~@g7t}>ubKHJ8(>P(~jHzTKE zfIvi79YQZvXQQp_dhX$1fLhXC1H51z0IPLH4DHX^sNHDM$SNc zpD0rvHF@4yRpJp})G?GEAoWSN(8vru5zW(i%aj@49k1}=Frvci^1{q*XKi0gf+E-> zlNA!o(PLE@(XXqSmX4~PIk)w0UT9D?rJq|}8`)hc*R|`BO#L=faV#8D?Ql!NWN6*| zE!|K<_BrrvOBg9i9=>1wd(8;Xru8zZ%wUHxbtOggfP4l;3crDf^xe+U&lmk|YITUq z_rprWJw|)IPU?Nd>fO-FU=1_>ZRzPT@I{xsgZ`wc9F>)~dwo&m z0&o6#LRokSRRE!*3O93Q>f-5z5$a6|LbEJ-*`L2vjn-oB;fPs+w&+VMiRH}y+(2$5 zSS>K)dA7%g%=gS=yXuHxRbU*n$WC0i&e}K37rR8Ae;NDpS4&kcwf0vfT87}%M5&55 z-cR_B2-1G@7bw-u4%Lwyt@)h{tg3EdnM^LQX!qc%Vw!2!I?L{M2S@tb?E<1$bkVr? ze(-lWySf|f;xIH5XneDQ4B9zQpaFfo;#S(&FzgME8`f|=w|yDI9KEx@(z7b+=C}LI zxf8jEIOy%dKoPJ-w;Kk!xXfqp?Utt~_>1CKh;em|=LM<5G=2KO`scl@7~23Lq49}zcRWlMq*mi3?7%U&e|KVLKag<+NXviKx z=k%eSI7sbsAX|4}NPZ>uO?Fz$O&_UZyfPypv)BRCM{J_IYWHtlf7LU;3!Rv{_ur@2 zK&qCLxjWKaTi4lf7&}AlE$0X^aH+?GC=-K$_kie`_0bv5$-fGE3Q0+ZKkXU)hEB{G z@~qhxipmF#%)M`Gu!j+bW~|$leH)&RoTv5v?#^`syKq3XTTz0mk8@PO3x&P+9S_Mf zIc(isbjR^#s$;J2Sj24OUs%I3Bt!F@7wb1DkNvgHLz-?Ky7@(As#2SyPL){O&SJTMhmITG||PdVTpN z|7Q1(Tu#31pm&4kF$V!?DK#KWU!#0G@?3hl>TGF5^XkqlwE@k*3z zLy}~vCvYdt-JOmsXG^jYl3um0{7P|+@=V%Hy1TM^=~tnhx#B#X&#I=&!LR8v>+Cnb z)?vB?XG?4+K&P7{8{&)&xfANkCL4zJOg6&>o&stj*M1DN0~k2p#*kfY#@<(MPfXbTw@sByGU!{?TAjQ$tY(xmU57rpMo|SKV`PN>FpOZl!;hUH;_WG=? z*##E4vF|?gMuz4F#&)AVg*Mt8q&`{_v@HEhd0{}9KlHXOk9TwOJBqjLlbc|g2OQO1 zhTSOpL?4k~y8N38_%25sBEfqpQE7*aP4b9{XWa{s2oxgs-aT=rQk6h03m4G%41*-+ zuc0C@2Pa@3MVWs0K>pew-G8ZT!CKmG=v0ZK0o$jc3kEL{HA+5^H5#Eu)z3T<$JAZk8pNDDm?6xO_{E(Zh zY?JOysi^ez)$`2mJYK!_R(vSMC@tUTRSHe0*wNIJRSTFhbV8WHG2fZV)7_uK%QCIN z0)v^YKr&InuB9U}GtCGEFx+oUNf2X~E-dfg+mx^XW1N^eW6u zC9zwh*+=42qi`Nm0_dZbJDr}G^3(5YuqbNN$koBR`(b1VdUeUv59N{Tv;(&Q_v|H* zvcW6w2yi>+F$itJe8R`v0e%0n;(%Ti8g30Q5}MXNnBczPqiflxVt28z z%VWGiP5>E3+iY>ijg;<=g^s_ZZ=QxO2a`Vj>7+w>IsBRx`gtmp7^Ob1(kF)2aM~09 zhk?mf9l~6o_BQ}R`2pU7)A>SiYamRNLNza>;^-!|Vl?}=#Qe%1RLS%(% zHUDb^11Hb8pNTKqsSF)FFCskvJ%~@LmdO>lKY;?qGa$< z%Sd%JGuorJMTzT)5vgSypn#p75&5vgHrKmLj-q|eA!nESHz{w!M8Qszf~R((={@(`hhhJP6udn2(rxtP>HN#LS~vc)@|Jz< z{SGsBB5^^@vl}UH7w)S{)7x7K-MkXlP6Nv%FHsADC!t!lXs#TCCIgq8(MYkkpQJ5{ zGbL+Jdg!lTL~V(N&RhFCShpJorSZ$L8Ga2}4Bpw}z-94cn`vPkHwxP5@?PwlDUB$# z$u?dozHTa&GuA^4^>q;;pPAxG;${Z^hp~u?X^3H=zJz;n2oqz&M0ZjXbD?po&Mz+d z2WLeIqAuiK6-Qz19d)yWJV$>W_Hug<;a6>mEi4`q?W0a3sal_3Xn5lZC0@e;BVfU%hz2Pi)q1P*kCz>8-5j${p4Q zDep84mZm3{ieEh*y)ao_?Q|VM6P!Bty}JlPX-_uDg^=~tWGwoxUs!UD|7FD$p-Unj zKQaXmB;fRe>uw``A3DtXPXC%5WeS%Ix~SO)2H0C$&y`=-u=U=D%;%TCfh87l$JY+O ztQ&;UD)wQ08m(@uTN? z0V)~t<##MQ20C>2|K|(y!W>0bx1C?G>Ar+Qy33`fnjzAc58+k8GD!is5i6YE*``k} z%?CCJ6{CWQx7M6L&Dv0g(l7bk;^rH+n|6<15@}Gu14OqwD;`@U)p`5D%N0YPnzHmY z;5>9o*a35Y&Pe1)z#2{7sS}e5?(Tg4Dnni)~282&Cz)!?BAGK zVvpQ>qpN-HV4F;m`(FlhPHnqiCa$ONLNfty{ekJ@IA?JrrCEmJtI;a08MP$7#5P5M zuIMr6;uX+pz8>gHpcs3x5X{I&4qVCqMT8-0Cgpu(&KcW#A>*jJnNf)XXUo`*UW#rr zpYo~9JJuJ~G=$KYW=%8{=OyfFb%S=P@ql?>M*|~ZYOS-RhI8L@7{qS0M>!O6j=x{`g8JNO9TYw zyB(t1gJz-a=kz^cHa;#uwgZw1CrT4){W0P$ZAX1rAju?dshT?MIV-%R(OqR38XYs%=Ve(y zaK7_TGC&{Fdc3hLQGlkpwx8c)*G;Rx-L_#q1uY0P_4|Lht;TZ>DhhtNGf6xScfTow z26I|W8zPPS(GruDM&U{(oyK(8OLEG`-=OYO%9%=F+n|0XMaqv@_V&NJJVbm{l1ZKI zVd8}kAWqV?i3}n<6$g z(Lq3|SxKkr8~la?8lSf-jWLKZCgl^3C za2-;VgOic%A)S`dn*M3~u(ifXO{CnGrZeerD&t+`umPydj$sTgbkq&QnW6f5 zo{m?f!JP}jjQts!ey1lw!lmn|6E-8$hzw&dPT9NqlLV_gL))(5I3BubTWRsH`WRym zeBCh)1M4WV2L-L9ugsYqISbgpDr7|IKRD*uC1z6r!&kLgexwZaYP#=70*WSu|HJsf zYjK+*usXW15~}nN*>fBOwcG*|E+fq~9#|U<{oVZBw?)zOUk>O&G1acVu1hF03B5{c zt7Lpvz-W`656)E6S#nRe=#Elwj7+kScEYWa(9Kiu)PjpG{%dHYXTWs>E` z0)QtH^+S6~YR(c7S3$T}piUoo+;$At{>&LSkNeJFfp_Ft-R}Qk=(?)V&p64zHC#*` zw#rm0o#lP~Qrv&HE$>IIRL7>U#ougwd(tf~ILz*f$&f2_xLq#I=uwr!!^b1hcpZ)0Zu;Ar{Ij!e*%%Lxcv6wNgc6}g))iBp!eL8iU zB(97fCb>zktUYjAHlf^sf0rx%1o>K_|KAJ_Tn7Fmsg}u^){yYT(#fisPNrt%W!1uk zLH8dhp+Y3RQ0(ZsP>TtBbFP;X-Bzphk*UsXQvrVuRI$K9h0>uLbS0yOrQ|dm)5z zPoR06B%5#XnsHHvzl#2t9m`-4`Tc8Lp*sXZZj&cSHxAt(LaaW0cvzP@#(?yM%%WgM?T_OC40XOE?$4JydYIr5!5%y26K zXDh#D>zn~Oq&BUQ8iM5*Q$Cs`@##>fA~j*lASj7gQa=%yvkCfseYlfiDn2tBU>ZK| z-ugj9+`@AxtPH|~U^s<3mx|=PnOn&D%HemeMO>=!fpfXAUy%J>Np{zGD9!;8^3q!Z zV*P$%QdL5x|M3}e1Km*=z{4V7>%>tvjyo?EB%lqBcGl!&2{_{%qBua7GAdZ=QI0v9 zFN7jH=-HqUT)aA`FEk(vGNKxG|-^-D|^m)13nhh?EB zLrwOuNzaFm&aZ*0y|ZJE@CU%pul-vMV!Da%ETLm(6(KPP7{MVdl^%W}n`AYceEH6Y z5AI80pA)I}TIP7hHnYC`(xfN);CXUl+)0ZR+2Vc|VAVWJfhZtUn-ycs{}pIWJ|_8q zLmUa~VqWM$I_JzkF~?KpbnSxD1z|7#!_djHs;|%AyMLabiXJa+JP#%hGE8|e$5r_{ zV$&ru4&Gr#)2(BsQOu=BhgPq+UMYFwCVUe9=izB|z9q`iG?#+Qm=yS&t!2Ai z%`7OoUoj;WtRMM!QFwBGT<^|u? z(fzEXAspJbF4M3Q=Jw}IAZOb9m@ zmheYfm-!rX_=}X~N z&IeQ#BZ}F}XZ3bJi3mpF0_lqML6t^EMFAb9r?&+L_1Yp?0?p6gT9Fk<;dMWC8C``? z_S!4X?#^CqSG&_-aD&iE7T!>n%iP=B>T#}^X|hk#X^aIpjKWIPle4A8#?5mJ0`C7j zDJRc+Hx(0cC#9lKOy)UtYq~;&q2=e6e^KwA0gmgfDIR#U+Ta6%DIJjur#YN-N|1d}55m>e63Ul_~zkEE#Dr)>Cp`Bhq2F9t2RB!{%#19u&E zm08~xjxjYQc{RgeXO8!#2@ShE&`hX)zVYVA(@-l6T`K9HJEAAXS_2xnI*x}A>qA5$ z?|OIkkF6;A*T>DzS~T33%(wI!_{U3@hL62$BMEMk6yt-7UQ9M$9|3Z1B#_2$YHn)V z!}X}+M}Q9m*1zbpm?6zDV??W}Brl=Ue;A#HSx+a=h}U;;TIldsn-NhfRU3fjrUr+a z-XiWlQSzIwaHT|tuWq+HXaas13+|sEPd*Fg`x*yg#chDfhauUX7e0+J5bu0r(3-+u z4Uo8D{kQ9LfE>Q15<)*w2shv-@~ttf@Z$Ic4*|D*WjY(oi$7KYhw_0hlFJR{35EGg z2rBjE-kJ(V@YS4>aWC3fkgOo8P1OiS^3D0)eY0jE&cW>q_nxz`&lgBFrC%>9L^NI6 z$t(dmd}Y3ofgKh90` zMI12u^X*3|3$&ZWhE5EP^0O$K_~lB5r`5mV-6=9N@lEbT91{!K0WepAp_~$Jsu_6& zNvQ;7_$}R37+$z+;K088pa$R7tB`z}Y)w$>`shH1u?ny-&0skstN0V8k8Pw<&!#sR zUhxIVF(%hUYVs+?iIV?)K(;Uw9(KOe&DHHqP2|M-$%Ehgf@5jpm;Ua$DEwWE1F71n zK=hIQMze<7>ar+>HcKEqq3)}9V)MaNh`Rn?t=nud*0^%jj9jy)z%J(SRE1er`;hWAI2}utpboltxy~@2f$i6k|-#5uw3Xgv$sXgcUKd)*=XO5x$6bF>N7N19L&@jI* z9=ZP1Z=^UUHovM$waVI)H}tD9JF;0&W1c7OCW=D?N`i&vY-abt>g_3>>;YPAY8cXH zF9Nz{9-R)0CwVeO`KAlh*~GkV(0ea+!{4!w?yL$v-_q54((>+TJ>vsG|K`MbItVXJ z2IxYyd;ck|e$c$o*`>$bv|erokgqxmtG~%!Gs>K}y)zJ0q5X5Xiq&%vi&kOHFX4w`ou6ONN2Bhmhf3RL5sP%6tTW@ggwpb#0J3q5U_^gfboCEvXI)TlIkwNq@mfM#9HFc1uEb2b;G$|c`$%6sq+e_U3% zHeIA6?GcLk;K&^Rk7C>kGqG1Cc>xLWojItXy!}&%X|KvTFv? zNdoq;gJ>0hupb3gAM>AH3>T`0&Q_BoWZzsKVhjor`Qu>770B z*ih}__7f{RO%ZW?=SjwIS;Wyyi8mbi)&%!C`0m(4I`+BZG!jsFg~*Qg^|K`(CPfjyX*y7=%{WTrK&k#ah$g$sQR3Y#Hn`^ zUWL0ZK3zWDNdU~x^+QcfEsraM`KOilJ!$5m0c-krcOOSTIKztYtpO;HSCR6YjXe;Bd~Mc7U~ zREbs6hv&wfuMv8Xtv6ni`qK>r32eHJu2X9(O4dG-SIpT%KLcz&P}8SNRXJ#L1zom5 z-&BhHnf&{No>up0cfLc?J3BLw8!sa_Q~~MPn<4pE5Eqp~Y|u?w>WR4ziVay%8f*a9 z1%Y4|88aKYYhvNnStRrS+`cCgQ7d8lG8m!$Jmz-U@xDR@Ba}YKzsuap5hF8qx+qv| z2dyaph?#Io-AXsE>w4A+r#tD9G?TK^Rij4~6x-_rI#_<*6JhNJDaz6fd)8vFm4g*m zb~UR~b2}+Zb0h)VrBNDjm0?Z>UCXXDr2-*o?`*!+WnkMEuxtipUxJV-zq>B|jv9p~mHRCxa{7FJ=8x@;^J$YJ(V`Ws(1ORW;;Zg@>!o zQiJN3%um>K(l5!H_h(7@r9cb1aP{U{zVs=C#9G{}9lIW30%>n*>Mgxt3UBLZ`SL9^ znJm(nVv{K}f#lt;M2$H4t^KH`!#&b-PY7aA$j+mgrMWnKRFvB08$9A`=?qo^8~77V zHkpA{nX%+WXK(Dg&;8o;?&ad`Jj={fg{*qOnqRThy0K3@BsQ94l+H%p%2Div;7vCY z&{062RP!guZ?$+!o>O#%yRTiMzJiP~n3E75VBSk_?)VyFfycRQX8{p8XOC)md}Y5L z29+ustsDydx5LesJ`ZhKdR57}ltt=ZKi*{NXqkux(>2Yl!15he@%bCoeoKuJ7xs|K zSuejg(9Dq;pvQXdC)bteyIP;oTXZvUhPI5IBdI<04X4zURy4$R*}AtM|H-tWN9cb% z_EXZJGL*?LbGrAx&)y;(JgK z#(y3OychIC>L$rEY>k)9^LDxv4hxc=2EVfo#UF%d+iQI?Cy4OhELeI#p4P;R630Hu z+(e=%6@`I!eT^Pss%X}r_t{=d;+9uR-clj4F68whaX-Vyl8nWU@HbJ+=efibp=9xB6`gPgvBGQ+!gfPfD`}g+n5KgPQ&?`;5$SBX2(|N)LnATDXqKj{E@H|bW zs_6ny-5iqHyXI)cQuU|pJfMMlfax^ZZ?xw+gkqLaBMa|039^y1a=+X!D0g2g*#Dv? z;XkZK8Ou{*^Bfm8{|kRJ2}2y@#FI5tgV#01MIEVeRJ6Zon3_RiZ1+~F2KP6xXt61A&;fLyw{y)9xc(t&rnlbPUAkwSXE{ z%=^S|=*}w{!mm>HnP`6ckWohJ85!wPrxnI1b184=>dU&Hts=iRt?C1?Z{x1D=TFv& zyK~(70swIZbrCn!v+0r-9a=j_LIYmyuAJRig!$$eWb~$AsCPn;Qy@;QhHhr04@EP_9ZA*Fx#d`?mRx^y# zP6-8L&x*jvq1{|v977@4pM7Z z*fqNEHaZJ$vaOd zg@`cc7<2E1@^gojKzA)(OU;zCX*LzQldpZ7%iQNT0k9iAfdt^M%kUR2IerW}r%-se zI6}wfE!TWxm|^0ge#X103`xoU6aM!cIn|-6S-NlxkMFlaan&q5aPA%!o|BaJural1 z&opKQ-Yds#2=cfjR1gtsg|v|G+Mu<6s4CNc`vg1Zc4G?zdZilJr@lpxX7W5>@B?9G zFx*HP`!(s{H;N4Kg=#*HvkqZqg7eT-BY06r@RGTPzp$D}B5U0!r6k*3s--h#{pmxZ zFcNHsBcY)*QjAvfHxl;7*z|nUZ~&~_Yz4(3jk>IEXONgK@oIatk53%buhA(RhLmik zjrcw6oefSkO~T49%GkcLzBX8Gwl5a&oGOOWX^B0GE-h>?S(hgI9QD90O)lYKk92~_ zB_LUeFvIuFP&|O(!zn=OoIzSURey%J@@U#CwOPqhLHl+*n7hcaZgy_I}56f!Qid^~iTbD-;=n~{UUiNLs5idO`^Wo;PxHev6 zD%IyDCP*~A2%GM*z=dt95YILxj3pu6=IdLpW;e(lg-DLv#!cNK8$mt)01}&`&~e0+ zHNBc8jVZ|heQ-@+yaQBWO3Y+g{7u;>^42U{FMApWt z+)w>u$yuF%W%vR5z)nr#R8XJF)RnQRFBWu;5;{ps*l`$5Kme_ollOLL^&@T>&%h29@M|02KdzI?d4`ExU9MK zJSI8LEn}$z{G6!$KFu~)nY6leAR7=TpT@i_zXn{#S~X^B`ny!QYLN!b628+NzgG6Z zl5qzj9=Pwx{j@b_|8&e2nmaHXIFzL1afCfNRF)1QPrnN;6mOdBp1jrBj7&&L2%nI) zFQMo_h0>ZiUWql`Il-SHBVTgjj((I_>};VqFu$mf`sk{;htwW{!EI{!sdNP+b52@2 zI$hcv49{#$*b|P5_B2_I`%MhrE4{~5a7#qlHVoW(VH1Ah8Z95J;;r7@8q#$ChsC(0 zAFOa&lZ)G~+iCJMBLCgWi%lly&0s;D_}EPwvcfRxVO=%&=y$=~U)MEq-jdr3Cx81! z5;>=}U3ZH805Gz>30GHR(}@qmt|_T3U)zP&Y}}wsiC(R+m?~G1W;aTv`aF*3(h<)l zJ>go|9>i14+L2BCH3z;-?YV+(1Z=>qP4%5)bRd4+#;EC$#PYl!32dx9Z6}0O!g$Hx zxQLwnFU-x-$CiM7CdM2f&ts;{c~L+~wO6HdF~bIIuiy$2Yxjti47%lC5l!dgZ=VRK zG@gU!W$(9JP`akxvg-4yY?%Qph%%ADOwrc(BQA}8_8*Bw+N??07w~|RD@j`*v>7BK zC_Hkgh2cqpwRL?xFh)3YoLubl%=dM1?6(g+bY#Y{I@dA#LN6XFC~Dp`+-pL5l?QS4 zl{u`1KOeuGN3jK*I%J!KT!oeBzA`90o?FpF{N^t-r9Q$v`6;Z(@^JPPi_LfNo%227 zOI2ZEQ{7Oh;fruuL3jKe=AEt%uK=xlU>W<-AC1lUAhM%3a%MJIDSvIK$(J>h(Cgw_ zB$ef?BqnuiO%eyYQrTCGknFOuiev9jm3zYqCV+DZV4~PIxBt(dqcw0c!p%35u>9u` zkq8YJ*^uc?n&PGRm~=88n9b49&cVkI8DuL`FguR)h}&&<&s@2at^ixH>rX14#0z0& z0!;S0Vn?Xdi@k3LW+`#uttzEk<8)$nW=sBvRE3^gJzqsW*{FBJM@azMoyzH>xrtJ` zQ7kN2_I=7h970O2jVFop}oKB z&I*i|q(2nP3x}Q6c=zB0(oUR-wNN}v;V3*mwO;NaNc#BzG)5R`5q|=Ul)xQfiVY22 z&=giMxxo7n8)1@`|FC>#_?b*5W@JNgd_d}S4y2$!rFM><*Jb|V>M5b&u`7qoL z|IP|~PoQ5z{!=RVdZi?B?1|vPzyJiq@t(6x`S?F9M6@W6VvX*{v@z-4;h!Dsv%&4fg;-PCFu_ zz`PebD$Ndh#r7&YKzy5}Sc zoO3*iV2cuLdC?9XqxQiixV0C}Uq{lsZm&nX>hAAB36&$heuIyg(qC9C3rQQS{5m}} z-}hKn!_Bj4QG78cWbgVk;#f#~#(t`?L|v1Yzl=J;PRiA6svf#pGqiP*c0qR_d7>-n zW}o&R^yfsOD1*9Z7i;)hE~+!)G%vaFdCa_$h@*^7B&Ylkn`^h+Exiw16b57>UPLHa zIc`&D57XeA**iYNLWO7VJUsI_I)(Sc^T(_AT5Ts6V#&?SlptH)gr0+*tIz7X8E2QZ z7bo4)%$Y>vbBTpZC`?Lt8@2YhJ5m5iLAam|Z8F}KG@?B=6=9l;QPK**9-ZZeloK<( z6{=*#R=>@6+hjTjWhP;!EH~W0c~&=w&DQFwXW1$7Wm7Mh_|60@-zvM8OQrPCT#kQg znnLO}3k~DHu&@*o102u+Ik{9NjsudGE7C#Xz8VIBKaCCgruFvqvZ@wH~Rv+2TLNge>sP#Ur$_~>E z!)dB9^nuISP|H}9N{rB*=0FM6IMB2vuwrjJXx6s`a@Ahc6jOC3(=-Jx3@dSDotCWm zcv{`aBO+hC^k?)br{JJq%RB0kl#7SExm+ME{Xyw~74P5g79*d>I5p^BrK0V*@6HXo z8n&S`KU6{)e_y3?Jxf&i@h#x3>rNEv{7rM2xR%za=N{-|Nn8oF+8(g#@jmBryQp<# z>QN;Qu^_MZI%A9Pb_k0|dwS2DD5GX!FWu$k#9sTZ&cca~Pm=Kc%6j6?>lwxhjFc_n>zB%^_-B2i)f!`1+E$3AgP& z#0$E!Vrf_Re@6q~iW(#->#LsDbEb4ECa?2ov)4*oT~_9iI$oFeJdG7@HSfnn6Yu`E zDfu-0`?5wU38g(-oY*g?Ul=*75hmEPh<9-)EBv9Q9@-MOKa29FKBv?zU~YQ%x}(jv zih<9rKqk{~;Zvro7*q?EyfOd~Z2=K=(OWtbIVZ52 zj$k{d)G;5!aXjG=?^Z-OQI;>cAFHO{m3rLHHV(Sqdr6zYpCV5@`r?32=yRNe&epRx zKkQ=GKBsgu0Ju=@p~Q-NHYREBOI$1#gLk%IgLzN#q5-6kDhkDTqb(tm20rokOC@`- zbu)PynnB@*gF}9#O^=6@317;_kkt~8k@2Wd4ViL(8sOam(<#@@WwW(m8XRX1`I-Bl zb(qV3V<`Xm(xdwZPsi3mq`I03zdmN;9rCbEgVE4u&lYymX^@`8RcG6xwhnZe9XIhY zQEE@yl7F!!Z}SMRh9@Lf1wXmx3>mqD9Z_uj;9;8%Vzpj96POjeP6Esf_`i6DmPbsWzP=Zv|2*V((3^yr^=h?CuJZ!c^TIDMgEEIY zm10M+w@nllcpTEPR@Xn##>s4%YyBJg43w33&IO$5QWjGID(4s|1#qmDDZE{{;KofE z3ay(uv@x>Q05u7eHsfOgGcIjb!D{6J+6k1>S@FP8j}10lynl4Nq1a~p{Qwdbm){)& z{Z+yWtF8JL*i4o~kB}WiC{1>GHwfALIwX18 z6gL?JLCYny%$OR1DB9geLA18-`=4Vm&4fn!|A!TCsF~@gyUuF*XTX*x;+eCD8bcKD zIc!j>b$RJs*c1)bH@0^f)ExPbCzv9q{04oe>7k5pF8K1xC0ETwpFrdThJtn0t%JrC z>fXS?kJ~B+ulRFmTQwK?%Y%a+ehnNfCWu|9m?~EQGLZ>IemJB_9d+aWSHBZQ&ef40 zvKHLKX(l|AUDF`zRW|C)b6$X>KYg0OX2hfI_f3^%H>)Ihn2h&8aM}i7A$i?0N#+$_ z-8vF79W<)QLsdIwA^5FXbp)0hEh|w&DPfGtm-?}I9bf^s**{xYp^M(vEs2ddl9dFv zYQrC@>dvXUlapzQd+9VaG-omKTQQx>;JJjo_F_4sk=F0Zpufra)o$`>8Hl6GF$P*7VY{GBb*=0=K9SZRdYObGb2V@mt=r zLyVnIJ9cR5z3q+VT3e)P;sUZ_^;-%xV1PELsA1-x-~OokWy6c!oZ0-X`h9=;Psu&* zM_fOfIgWs)6%9Zax_T&BcIS%uM$xy}!7z`!UE6umuVg^yiKCmChLK~-GP$BmRO(aZ z%UkJiOWi51$d6dv^iMP_%RiOS>U<2VS3yTQP50}inIxeDYX8J>E0X+)s59 z3NrP4t0anp*O)QF0}9J)T=#S}!cgaoBBvMvV#qcm3qkK|XEFO}9t89iEh1yRD zrpd@01s)(FQ9(YZE0Jq(nk-@>Cts>X;nyh^c8dmPX?FB4`I{Q02ao$o-E-rDZorT|SD`$I#rCj5PgOh_45a&M~Hk~E0?G^C7m#Yxrc zV7HLVt+8zJEjz1B%HIq6GxO+(Lwifom7greubdL&huCs8Y|2ncfreEMT`vqb1|B9I zGtvxpNwpB%+q3QU&%+Xqm>Mz=kBr^Axuroz;k5YXd)K`CfxbjZ^i zfaXj(dW#QYAOExJ#Pvg%t{QThiQlqX_Fw_?DP`4;qC{>ok9nH*VyR9i7INNqVC@Zw zc2)avY2pqFIY52HdiTeZTsNFAj-Os)yh+I)ysI>{i3duRwsK@3H?Sd;oVb#8y@^qRg8%@d#?RP zqX+7nA-FGZut+YMggtQ#;JHs$@4g8>EnAmZ>h9d9@&!s6*_QTVOBZtS6et1Wl*aqi_2Gy9Fbbt*ep+;ZS=yP{A z6<&LG?nc`St9YFVZs#<*TooLH_jeQt4)+B(;zNpccG&YiH3}c?2d4U!M7^d3+j)IE zsO!zz>EpnT}UQ4eBMT zEps!)cAe@H>>UGu{A95{UkHGDn$Z%U@S$@5VTlF}G*jS!<%frA{s7i~&ncf=LC%KR zUcExT2PJ^q7;(2OC3(JLpA5bX@C#j6x@F=vy#d$W%x1qzNbQy^8O+LX81z&l{3E1B zgF;tOQr53pAbVV+G-AzIxDwK>-|5_DF0?s~tyAf$lP>2LoND*-`UeMBt5>qtKjmz` zpAvr=eh9dDZBai$k@Rl6gN*6v9>(z(x<1ZDNLPqsF6LhI8mlc}#|N|Ve5^DV?#49J zS?PemlP8M=-@CLrKu4B8wyDdk>pXxi86}rx|H^tLgL$$zBVRc3Za%EudP$N*T$E71 zcWhNLf)&skcHEe*SI6hev-hVTmF_zf^31vZSz#FpasSOQpv?+`eTZu#j=E?m5C`q0 zA5g#UuffRYNJlxZ)=(v)6Q$!P`NyLoVJ^zN$ePQt^X*iiSe>+roxSMxUyqSyvpdhZ z)9Pu(8~rk%PUBpL&VuP6U64D;)Kgroz`P>gyLJOd8DUda6?$FD1g~@99xS4M14Ldh z5J;5*(TPi!q|YTHlK%PxGeuUPB1es%I9nM~80bx#cBQ~}F#%JFVr-eFxNX@-w%}75 z`jqU44({eGm58eB6qSe+(+RwMh3~lZ?$T%G!dy#xSTSJnL=FEqPngne;JMlCWnWa3 zehUVl&f~&C1G?5OUAx?gJN%)ivp(2ZK70WgS^TjOrAtF2KQW6PMYHy3(Q&RvyS4rO zeZATfhrx}b^Dxy#NfdpEs0$yM4j7?Ng^$PRnF5)y2f;#=g{0$Z+@0NcteY|oV@msp zMxLYTxC*^DQ(+~D7PgZHX{+Nsw}ZcsH;B4cjV3e;c7OzO=E;5gU3db5FTc!;8bnx5 z!s*b>pe#Im@w{k;m-77>ILhZ;P%%@-cT6_|Hgo@p_b@@zj>jY3?<+NryHECK(xexc zyP>qPl4=XA0ic9n48aX6I1!e&HR@dF|A&Nn?1jFruX|`03^m}i_sB34r0>T1?A4J0 zU%5~@^|xl5VpIAgr7x!DV(jzzMK}%q(RB67M1l0l(?u?(PP&V`;%=@fkSU>pak?XP zXxk55wrqRZmZ~x|v9j*G6q=*m~GUxBH6^6Kc?$KKp(W%kH`?)Dw9@J;E9;+Rge){ z-@`CBy6k$~SN%_gCg&kkhStwMd~l@5c2)M)60UboFS@my)9%3fLrA&XAC&jYeg4*z zan;OF=@bUfGix&5cB?nbc1iKjpKv#9qSRB9+^T^c46m6lQ zI$N`uAo#OXUztI>=C5I*Q!ZWHxU0#L>1f-R#dBpmA;xzw5KLCm=?>TI4BvEN_zOcx z8j8=r0sWi!(`7ZFpUT9b#+5apkcy~BEk%gMH3#>@+PTGm)5mn!PK$*!sfJqdbYQMA za7`Zynf}rUg;Js^mS1Ds6r0Sp}n5_@^w*22&fi*Z#B1871^D3rft+%|=KQ z+{lAp4d~@4(R9!Sv{gw7OHCjql<=%5C)K27mRx<9yiFwz-|ZaeRm<=f4ZA0@_bT_w zo{g{(F)+m9Bnc@C!-`xEr1Y*p1d`T{ecY4@PrH=`p)cnW=!v50FJ0uMk9g(AeX3g7 z%=5T42>tpvu|P<)s8C`$5+#<;#g%OZ@;$n~wZa-OAX`t>33$kPhv*M~0MS?1kb%c%3Hsz^XO|_J#>v z%XaMQ*@QdHhB%1x9`AG=8fDuj)71jPw{iDhR<#%*m~Ibsfs!`uQ)zdalg1L0CRWwn)p$s< zBxGfh`4_E$E6wnKSm`#?vTZi=v*Uatcs7ySOI_`RHmEIKJPC&Xu%a)k616`(cF!b; zQ`^6LU)9X8N&gI$fIS16g3KvLP2MKT#3om(ZwBP9X&Uppn)Cqz#o%>J6 z?mo#7MyP`fP&L~Wxr0WJzVBC#h z?s4x_a>|d2Ko`;P?Ck?=-_1@tTChUW(n`wC7iMHTy4jO6qMWZKWdZ}gv_96Oh^@hn z$Nu#_3LQq)*_rMgX&1M*bmH6;RqaA@v_4!0ob&(Ib3qA2|A?Jz1xMK{-s_~J((EKY zj!(?(l+hD<%gnVnbkz^mZCFNkou=*uKp@MT=_ma2YC36$zp$Tl4>6-s66WGoP5Ojs zXxhKW;8}`>XWMMke?HIc%201@4rr|0p)+^f_FOxcc_XF&c711@Q(9yAs-Y<>+b5q* zSIv_-mb~D}@E`z7Fd3u+nLw5vQ!;UXCY)4WZG%^DTY*L{%9H+2%Qx~k3boc zaa%4ccaN)?XRyi8O4yXm%5*=zg0fTZ@PngrL<9?&cp~VB8P_m_k!(~xeK1g&nhdRx z*gXoPfREKV=M-Yj`5QS`-=Dh4b>&e`^SLwu8x4U^1ZU?dOb zEMjYnG;|ntQ~8LiHNQiT2(DD|Z1i}hth}DOwS$F1CgeHm4;2K8H78B&Iw9dl?{2$R!zm9UD_ zIov7IC}N3@jWK5k?sU?p;QHqWW%L`NDo!166+843(&~)Wu>!xNJdswAI-2cdaIIxf z5;i)wgn%Yg{~gC!G4{{O8PX#EY>Vlh?_E~D{L}{xaLr)4O597;RWfEZih9?B^3fu~ zdjJhpY6J(o#EiYpRju^mP@ah>Pq1)u5Gq%qrs&7f@z6uHgr5PpO2t$xr>5rD*i9=! zqsp4*b(3w<8=Wm>dCnK3WVnhGQXaSvE0thho06rUg_Qeabya;|>2eFNH@~+B+!<8Y zl~H?h|A-$?v&7E@GG&9`vCayn=IQJizael@m@WF&yJR}tK(y&X7tL%#bAb_bVS;>e zOORG5Cr|<+lQHEKW}J?G=ph@NE=(jxQ_?uPQ&Y&!eBNa0?bydy1zLelWVZtK96zV9 zA@T%s9LO%`xj0Ownq@SEiQH`W?rt3D%{OW1L0#$ihJc)@ci@|>=Lo8dqUQd+8L;yS zA_`n-G4e=Cwu0m{QAILu5#x<0OGv!_R^ySKLoW_-`X^_<b;~H>{ZSU7S%SwP=dofMhmJt+(R60 zbR7p-H^dG+R&h(FWpQkyTBf)>M5;&A*@1Jd^Gl2SV!s#bTi-viI52OVuAq>V7T#_k zu!EFDmoPdwcCGo(cJ-Ok1p=h>z#rqIH2k`~Y3>Uc>XHM{Ow^VRI>!yuo1OJ3c1C@- z<%yhx1o!9RaI?9Fm7sS}$eI31qCeY58mq-3j}~`-Sy?hqa`xS@d7qkq-4Fqx35+*&Mk3UpY)!WWI$7I#&}L_1io*zqay0 zt779ac0!QLeie#mE@@X8GBp$t-zmTcaweT2=!#$|6rxklRM=4jfdCHP;AolIZgd)V;Q6+HOxg>(1Wf z75IJX??q{K8u2E$x!5^JJTrc`fwbkCl zz+e&C*jZyQmh7d%3%9D&f7&cJcI(R;s%N6<)dge0a+gJ_$JX}jVdx^zIc7+^a0uMZ zV**-G--H+Kp?S1RZgNGQDmBGa>JmQf?i2tGn`NIWDPbzIVUhDI4IgOcYX~M#lFDT@ ziR&!DW}^ZnNdqdR+=Cw{MUF5sEI|>ZabaDO zcEd0B7_uZ-%HG`ESrvimz{U5dg#P17Lcb-U0``u;i{gE8g5bwQo3LFnQe35O-M4PB?gd9sWBKExG4eb^cS^Ozpdqwv{T{sYe$E? zIX!e^$23nC5_t@c650!PEPve9*HGMg3Z;z*EbgkO$pWtfdshiX+N5v!i|SX{u!QT6 zxo_^g4lLul7-cv>YfOoZqJOLZkgC~Nm~mQO5u_`~m5+qdUe-gne8cx^{)OQ7fRb2} z_!_yUIx~g8k$EN&Ljq3@Gw%6-fX2|o+!4v7U%!o>Wqq2Nw;xrU61hxi>E6X+yXZOP zVHi@eF*ghkVfU+8|95Wg=h;si70sQ-$FpN+jJIp-83pSI#s)|w^9yYw%kFp9YNB%K zZ`{^``TJ;z@)+|9=V49M8R^5reB%!v)YwvI=Dr76H71Q92qM+kyfQ`|2Yu2w93`#} zQcotByslMX6T4~lDPb$@B|R)6##u8pVq_lHGS=a*W#_e zm6t|G7BU2IQ)>vsD$+OAi!a7|~F<=DHXHhDbPwLG^=JC*U1I4*- z;Ymv&6nh4%jdOkf)X8`+UmWrkF^4>gC4at4!UWYu1_hl;lv@0cL;$P&WaJC(}&DoQeeo(bIuf~@2Mf0`Nr*^{} zH6eYXC~f57v_^IMrCe{`Oc9eKoW^HX`5S%|QNw9Q-`FELVxV4A zXjnw3x4tmpcLL%05R8@ni5b;^W!&TIn{O2+%Vr{r@VLw9q>;gu3I48Bv~sER(z4-l`S6CNn45b2Qnh3?^I+7iIe9&_&&6~D*6$*|{uoPN;T?q2 zT3kTQE`sEFJR7EWW8L}~n6l4(Iod6UJ=sZf1JF2yM;}p+s!it`StSN!dw-Fg&jtsA zfuo_ep$k*QnESlGSanMkB8*Tl2WAXFDKl^?xk6hQHwqZEP%9l1U0S2=B%`}rz>NUt z|D3iTEjK>gK}l?iVsgKmDEU~PD#UTt=-s~i-|RF0-~WC}aKGz?q>CT@MadoLw`go@ z^R@gpSo<&jL-4725xl?bK?ilDGTt^LqgkmP+$erR#vb$@^G)^rmNZ}U_XL>*)Agge zvOSAPAM5)#`$v;@R&V{+v6(XHn55-_`#i7vku9$~z$NO3Jj>LAi><`E+2>B#dN~N1 z=8A|S};xU<2}LofZ)GbQ^y|I-ErisE`pB>s)H{;Z4eP`a zUT{XI3|ijDD!1YxtHAX3=D{W0J}jZ<7*_k48k1kT>_7&9&fptqL~@!cQkp zpe@YUJsjEIHM9s3Rj<*5y!s_ps)(+16->sxoSD}#$nv#>jL46z=hG)eaEw_Z%1*18 ziP-#Y6+fMvA?D%XK+m!?+HY*mgiZcljC)rH4N<;nuK!W`mgVJS=QNBOC&0QiNYb3+ zk!+M2a=;4u-COhp(li8CY&>Gcu|0-`e4Tq0Y$MyWHRPX7D0q;3{!n4lJ7*~65O-ut zr9qIGei*WM@t_PGO;kE+$(*`wnA3v&tz=CiSWpj|-f93(LV=c=?QKk2zz~)jM~26Q zpx3{b_U3enxz=l~dEr=P93Vm>(@V(;UrMobt^=D*9yB+6TlwCYJ{$KS0i#?Nwjg!e zIKb_IbVUvPPI1-Hpc7j|;WXiz9m7N~+`idj@`H$pJGPYG{8}BLxp8%C0!EDKV75UoTtK` z_MS|m_8Yo|hw59B^d@7d`o2i>u^Y2)ZM*9NiXQtwubsmmSn7!Ic=sPxmuKzT%RUCR z>dxm&OW|p3ggzAepCV_EgS3R&5|+%o&4;UI7Uc<@{dA(N1OLOyo;B^pE^pc((vsS) z(jd;vcW>tC0u(9v68Kj%hR-K>ntwRm_;*luHB!EIt<#Ij)*WTWr{#L(`9@?~{WbOZ&&i--KLZ(~|=aM5i z;P_G2j2AX|!ZgBu4h7EjOYE-h5s$l1eh>IAID}iwJ^iP}QOTcw=ypG`1w-sk7orCT z=0?m_hrl0k<=D@{_n*)jHO@cl?m6HlV`ZH06MuN#o*h{>Z?hD)WGff4PlC+y|xAIbXLXh4S`mz?U zy`cWXXK|)ORE^sR#OdBLC?r*DQ7khednxnW?!jwNp*=@6L`Sk!469V+hs?!Wdb<fc~v0lDou;>fZp+Mq?D{@TKH|F>?u zqv~N|{3FFe@S8vc-EBJB&3|yvhSoNx-xEgf{k+MxT@2KoK@1e_-|c;-`ymdtbprb+ zDLZa*+8Iw3E2C6myf2)Hk1$ryEL;9wH$ZwDi*xoMv3be5QU}@r$1Ux98!(cU0V195 z4k7iJ8I5AERPR!KPwAI^vUnO?p1M@H<=943kQwiG>r4Bsr8HbG!QXVJL{H)VIl5$KXyq&Z{sRxG>iea3iPoc zO@xNjk)_KlrMKS*66{xHJu|mcHG<&^B&oC9tR9#7iN(Kc4L=UDFnCO8JhLw7Qk=ds zG+ghBlDduyVko=Goz_-s1Uj_$&QU!v(dG$S5AA?5_Oa5ZSOk_jaeEC;OwQ-=hg^(Q zQ!(HTFVB@ww5?svwcBkZiuYXI!4?d(gQWpT&t zD*;NWA_T}vmU`?jrTWCp?q<#@rp-c_#6JQf89&EVNU@^qse2fIJ_H#0o))Y>ww+m^ zbQ`Cf{}`>&RrB-NqMKen)AMDIRS58cL+l@HZxlhN#rMjwmq!4jsDCoE85-2mPq2v! zSRE%6?Q=?!xKnEp$0GVa=MVvm)bDMTZ=AV^4Qy&*VKERFRJR3ixoASM<3vOL|E~xGm_0)dOAb znF{}OVy4f|!3!u1{OP17&(*ZOe0N8?$k!EJ+Owi zTK8{_n;S&cbu&&L@rHWo{vF$sTvW}vMR>tJsgdv>WgV06Vy@pDZn{kd@-iI*`O>g^1+a>ghBn=b0%y!L9pki&jMU*A9jU!`gseD_hSV6GAsK2rAF1OrWr>y#py=8)Kujjymu6&o1 zgGO}+kAhL~%}PN!YYP_0)cUP{@ewav{4MNr9jJpN(6!0&;p%lAc%fZUbT{~Kp_8uC zcf3_Xan2)Ti7Qai@+>=qVW!}5atgdXr8@Rk4SPK;%Cq?uOn+ykUWTKn&Ei=*F=3pl zs%wT59dZpb;%3t-?Hr9}e+6 zesQvAA#Woxxnis&!y>{ggd8aWM#*d@33p#f)&T$Z0cqvFTP*xZJuDFLZ^5r%rW=M? zU|nvf(RMDhWm%uPk8ZSxxy~KUQn-oKz5LqfouE3!Ew&+`B8%ZF-Oa%9WtV(Ewl_%= zx*{JM1q-};;HR5j*P_X1xC2wezHgEX-$S{#-{PFr&5-!yX|3v*TlScH9?_;cN)=Cf zb1t%f_ElT5y_gVAE&>u6>u-J7s-c~!)vXc@JaBC~_GT0S6Tf@_bo@Gr0$+og8y!tw zZm;EFigBqB?}5h&*Cqh*N%irOO+i+C9=lniga;7@5UcN%DeIIS84QaA z<-QUq`a~?lRs(;_5t2T)11&ob=^)Aou2_|AwAPf{wrqTV56$XOYr1GcbNR| zqO~m#C4i55i75BszJ^KNwPn4lc3HD{-dqGhX7|UR>b1F+8dHXhq3^~C!HtH-saS0F zO=y3(aYu9L8}x{>j?Bx3khQO_bok%??l5n6`s;Y;rXwy+rd#fUF_cpLMCqpD!$!B_ zj6m+&TuZUcb-QoHT&xQ3zbH_!!Io)Il5By7(k?+Ry!uv)(}L$>l!I5xL-6U}*A`rA5`f8y7DB5e!*>&lYcH_dHJ zQ)w#7@pm5(HaYMt{ z?TFSPr8oXdLWMYPO&L8?Z!15k%I}-3kF||c8)6xx5-Lezg1yHoI7vs~`G)|6R*|bq zbUK-1q4sxbQ((dKZXT`RW_ZB94At7QLEu>LB+_Q&r<#b}qR-~f*|S|-*;;NXfz*%* zOhGRw`gYo4c{C%w=n!}oAT5ni@r1N^@pn~1g!6B&zrFDU$I(`wFFKMOoB0o`&(!8-4vCi-lubaNEhwz2$3x-H<>$jXU$Ta?bw8x=gV`0(QzNBnp>p9S? z^ZY3T-xTogqfwlZvCA6?oR*gfr2mczi8CaNAYi0MGotFxEA^TmbWvs`T2C%6ADsLl z9FUpykCXqC9=nWtO1$y?Zqt75yi&62m@V#dGr|N}W{k=%`?rfK8&vijaFzYcAMKCUZ@lt@=Km` ztJ5i#0OYRcl9PeE(fo#)Bv^$v!iOnYYvrGRRSvhxOy%LRqVdp?4UaVtY^xUo#9Vrrv7R}6C`MgQE^L1fDTb8@}6j8wuQ$stlA^8R>P#75l%VB9Fg)~fW z4PReX1nfjm=|aC}*cy|Vu{o*QNeuSEC74DRJJcuj4W(3IYfJUFf%py6S~2I6m78ca zvfep?)+4aN&_doXWhs=z_2yNv@g6rt1s+SFey%3^@vt2GtLiX~EUvZ%WHPi@6yz}% z5?sjQoSry;I+}tekxp!;KTo}^ia%1+n+Xyoj^f`;Jrt{S{hhgmJ(iL)qGNklLW3M$ z3wUCto?#G-bLNrERbpS=de~NVQlm*ECJX}F1Qf69Yj)=%w{b@>ahILp7voYJ8Xf+B zQgzO&oj2W>m(;kW$s|7(UNM|~+$$z=W^b}>Q7y1eLAgGJDf@aaBjqrxoWBpE{~Aiw z>>RJc*pnG8j*6L;Lv-bi>cK+~nn0U$FdLiyj^8vkdvf5^6#8Hl z3WuHK?c7VSC~>#x^g{tvUjJde;uGa3|MI*sbLDD^nfDPn(wL>Ca6W{Rl<~BH06da1 zx-RoiZ}x)i8UHK=XPH#+a|5JTKaF|E*w2a{I3m!}$a%hm=}i)vmOc_k#_5lD^&liXS>a-Ze(T~VYt-7)5Mkp$M$01{%Yz>U_> z@CIP3B1a7W#eIAZ12vLv$cnvSap{EhCcr}~+fbHUDjRI!e07$b`r@ce)1iuPFO(fR z%-{vUsW#ZteXn2@(V&fAJ3o(&m5a9zf*%;?a@<&~&esoYhPx{?I+448z*-V}mwT4- zH$b!Ketfh-@|CwW#ZMCIVB!wj0}CeM1F&Tf8k=^I{ND^gZuqL`9-IJ|*thIgiM;fUQV%K&59 zdQpV3o)b<+jW1@D*I5@$w;Av*m=U=}p}5grj;?NIY@q5Z=QaBStu*SX_zwv%k+Prq z;ie^jQ~*VTv7%E$?=jPGKY4h^71f1!yb+c<-hIiw6lu<#> z7asl8=R!G#9I_yb_&-h`4+5NZ|0M2<9a{jN9CE7%HQ47RBnG_IQlGN~jof6cC&G|n zS#ugbqj_wgH}Y7I!AkzF(9_Oy#iRw-8aq?(lwy@cae>=t1wXTU3@f4DKZAhY8QESJ zY0+DdIU-$Q5al;J=lD#UIa$4BeVu#WNao;GRDEje!5;~2{QWS*o-4@-aat;I^VuLk z{ledcdA2GojnZg|Fns84j*2+aVz_~v9l9W2L7A7_|eefjPo`fD9~Qa>#DFaBO(m5CTe2`dsb$fcXj&$knYjBs{! zCVZ6~kTE)s%|8}kvywv6i#3E}pE@McZG4A4lX(H>0GU?fn%S=D(C1p>GSj+Jt7WgD z*fKrB#>DCQ{NMptaoa3Owx7_cixtPPfYzijvC>hdA57|zRSNX4((~mamts^OunuJ< ztCM>esXNZ1S}>EIv9GnOk`prw!=>%q>!xrjC&Zm=9_4M}X3K6t3rFqK&bFJt(^5Z^ zVG*PRrT-IRo6m5F6!Tr;U*T6(TmYBe5j4%YdxCQZBTlRiBJe)4#`ReLa!Pc`yIZFP67(ra)<0EL7n7leqnTacl>T z2j&9#W(}c7u0|BP&D)1^2)vkc>YH^X%6vsj?!l?T^eJ2qF~y#=YxzwGbr#w}p+dK= zh!MBr1E)yJt+>8UV){~nwv0M2obU{1{Q6qJ(9&(;`@KV>_MPnZxy08qtjKZ9ibQ!d z_HX!ZPw3_*IaYu4hNo)xpZvlvfv=h^(_PtI9wCh@MMa%_(PwJMv|Cab9wh#_wiiE+ zBM77o#7Dq1Z|^|Aaiz(}c;HHV;zGqstF<`BVp@uMXs!VRU-A9EEgmLZj#_#2_B4_h z)WKELGo~3n`S(R&IINxerRd7Fok4Pm61;06~DG~~LUz@h)UHvO{}N4$llTee;o zo0egPDrf$9PYBUBXw&wo8DId9dfdEMZo}LEosldjS7H4hCknBMWS=s0=hJ5iHoc7^ zc~$NVl&uTNm}-4>Kr>a(qG?dB>?^ux0-b2%D)Eh-F{X!+K@QGYI19J&rO{dfr(=Wa z90b&E6-kzX{F`_gy5g0pE<7`P06f2xs~~OF8J(YXeEa zJXY>z+E~S_D$~f%?0&|QNVID(DB*zY?L1K>D#NWJ#xCU z$8yB4EIKPc=X9;ukB#E`WeKL&;bNYfzY+nA8oF2mil^Fho^8j!N^1Fwo>q(ygT2E4vSM~)#?5DOme+HKYt z@d#xeH%M9Q*Bmv|eicOnoNFNeues=F_eW1x0X!;?plzQ`h^#x=X^Xr_1<9Qp?voU2 z#=WD^*Brz1DU$IHyN{Azy{(gs|I*noY{%u6a@sa7`+N~W|BW&lbgugoj&4X1|GRIK z4zF%f#9iC^>Sy_=OGk4x{^DYtX7PTe{QQrP)7Ho2#Kc<8Q2no^6!C7hzuVRCOE)^- z85|g>#2km*MSjwx>h>y#jumZC)DWACt}#%W7d%gk`-|COK5~%^;J#6XPNS%@cP7_r zx4DAtg>jgME1tS7dN{Vcm0#jh|H(@QY<0@hq+ANt(}Rh|DeiRh4V#zRAhe#bd<)jp zwX2;H{W2Q|N?9#_z$-WPA4M4(wwvE>4DJM)VeW;?o(lEPk^eLF77TgQ1Pg8-jr-{~Gf@;7!xMaj^B)TiqV77Cpnyzu_fv+e%s| zZc$-Q7|&-+feQ)7`i`Eq*E)@aMKe^Mtj%w0=_{4J@I54OoaVQq6eZ@b%vZZwrl8kR zDDxr6*jI>Sl%Ip+@())=9J!<^`~{isnz7yhfOsCQI9pPrn>4B zSx_OSOhQ&D>vj+kv)B)ZE-!qadvKmZehbMcczdU<&;HLHFRq7JnCNxCgRSno+2R?E zn`n@venRXJ|RsW@M<={2dThbN%PO_tb)r}^FAEe*9NyPZhjNsW~XhE1&+ds>|nSwS7IyU>3I{*ZSO-VcyI z&A2)Rj20OfEOVCg%KoWIHxRxl4{CEThQ>Q5Obay7M3x*g+@!<~D8+Eus_7yS*>PQh z(|!~Oh>A2Xsig|XcRb_~bTz!zfUxd={^HKY*;}LT8=d{R*hV&0FDO!{B`O>(1`nCX zFtfhh(>MnkN@S;$_0sJlkD0KjIC*7rymS3`4vjpwH~{N(%)du$D|S;LQGaj)sXU>iEXD^|K*F+S8hGl{75Te52u^(^*}QU=&9&j9e?1%zKef=!yfR?>)B#*jA9J34U+i7Zp{u=kS1?F-W}oo| zzni+PHYK6IuO^;c%OYXSYm5Ad&Ag|fQD{y>=G8lcB~L#?7vdK`t;v}Zojd^7sJ%hP z!HDWqm9a?7CbKCkN7n|%{(&fgp-#}_IgNrw z8ppVmaO7c~g&@J^qs3ku;4c~Pfj}B$%Qpj-W5WvNk&2OZ;+-zE$nYj2vk@Gpt z8&A0Kwx1QuMijCNv;IApa2J22%W?5$i$2#dTK6|91?)Z-n>e62^a_5MYtqwh=c*}* zN1d(|<>sfheaYj)g&x5_LYb*B0<<_FfUKO;Ye-ttt}x}KB)M-;n)(=T&ThJO^#*on zH~|}(E~+%Dr{Bhls1#ZPn1aNd5&84{nGji_h7=8pZyM6LJQ+tRLd;ck+qS#n^;wDa zH;w;GD72S;_I0`zwfnW)?@~7ZDz1f8#PIv81`&zzA}f!P-g5&R(u}ZLcPbT}8cG{; z`s8ntW}xWp=al1Bl3TgFTZcXw+gDuGe&$Sn4KITF&Q&}+`{II45IL-|*WRU7FePW;`zHF9j@?ER?D>Y_^NA5#6%T)h7)93CL5UXg}l=~_XW8=jZ8hLk>z+{$f zJZ)t<+8PEDA#_x+DrSKL`(tt!$mFDf2hEySf zh@FedkXGm^{33fc+dDdA>&m=_7Y4iZ_5=Tm7R%3g;F&sEzrcNuoEU+ z1$HUsdntWEda=70smv9V8xNwT%)ReRo`H1j9>US@vf5|-(vqFgwC=zX?w$X9X4h9p zj;(xU^tWQkF%Z)6e`F@&)R0wnXCOJ>hU=3l*CtM-A&L@PI@1N`=|->AwQeiKmbwsu zZ-y9bgd%s#7}`jUN4_Vp@{a;1f9jZo?VDLG<|O)%1UK|>K;(YL)M+fO#cQzvqT0sg z$X0czS=5C5GS@-bewK|&&6gG~>*dx;pPrH=STy?ysjkTy+X-BO*XirK5`vs?nh;z$*dO$Wl;dFQ(i3;(tF@YZmaQ%Ng2Gk$XUmgx0T=7|k+Si8urPQt!% zVP~d;ad&m-S>ni(Dd15Eg(vLaS)LY?D+c`P`sbs*epkuqdMMxQ zy_4hGpcP^z@Ok8M^gt@x%ROBDB_d_FCWE?9<>P?lBG^Gkcu^WvMsubWS~-B~-8+WZ zA5RL%>>3=n9noV^cY74R!hg_u+8q!fV_I!arA@@-@ankFskjK$s4F7Zm!H#_6|C!6 z@ke_}&8UX8rsmD6Fqs0mkaq{x0sQgo?y;_2kmzAt{FHHcD?n^MLscq{$#KL;9zE65 zE&u?`Bi7t7>CGmEx8RnN6nqyqI8frm_ni$BGz5l-6AOQXj>+uzd)`DmiurHQ;+rh4 zx83_Ze*-($$2tuUdy4FMcEJS>rlS9})w5^mpOYP_U?a7Et?8ia!4Cj-XPGnzQx;?J3?k>itGWpLFUZ(1*H2&u8;9A~r;!6MM1`QcOij@oFz zOY1~6rY!9FlHRkh({8V>jtleVI`)XE5!~hKt~=aKp!h`>aqa{C?X>bV{~iC~p$P7u zzve>Sk1LBlWXM?}DymD7s(f)Z>~}p0lJssV3TU^^GtUD5Owa0yWCiG&H})3iB*V>(^WC#~~97E_*w=+5WLE8!0Kd6yrWHiNz4~$|OW-NIHs}-;6 z^TScjB?S5YCQa1m$W|wBr1R<8a+H=HN3HIp8>y+QZyTGY$!Wj-nL!hdHESgz82l!7 z5sQwRljf1=yWdW1zp9Hy4wurV4LIdRCrqOZ5qQNv9clDb4|QEAMTDKZ2fSP>nEP2S zKHakqv@4oQr0D(vxpo|k=TGVxjQ`%nFeB*VCN;ak$_E?@W{)40scM!*)u;-0mUg?T)_uj|46bm{4eOB%oyqE3 z34jm!wFw7!UIzsR0+c$nl2C%+Y_b76!=Y1$(DaN|w%-IQNsX_p>7g1N_dWUAGdTy| z(~N(>`o_}fOL4h#dGVw^DPI-$J~qx$rADXTh_Y$FBw%up;<9`8@VCWL0Grg2_GKzRJ;a^+yr)g)7#N@;r6vhnX8+6S*BL{@|uTPvAE`sLLxzIw%>A-Y4QsW)8? z!MQ#;v#U-q(1iJ&tLK}dcRaKnFS;z$75cabk0-pQ6}bXscsqC|c+_dLTq%@E7?@!c2WIZwK+URi#y?Wt z>OEslZ#CL%B&oO1wr5YDKw|>#t)&cZqM*gLg4glq!90|p8{6;Aj+UDP*z~D~fq{@?I!V)r!Bx|W`VEeS%{tP_!Y$Ub3 zQziJuo8qT!ImxtggBmklj9aIYLTjUTnqU1qAB`dst=9Wu+$jh6w0aEf4gXNozGBD( zR1+U1tDCrE$Y*JNtVp;eHmI%A(6$pBYHdJKX!Ya&kDX-ypASv<1@dC^lL-dOt=if@ zRe6ER3n&wLqWKg;Vmz;03Xql09VmHFP^QN(JwNQBPILaPGSgE>=;hkm951USJZ3xf^AW4?R0MqSqGT$rJK6rMO15q}X)0B;b(yznQ? zZ00f$i}=e7s~ZHW-@2Xl`{_mwD5|Pr&Rbs#swHa1zKfx#-2ZIr58oXdF}$tD$$q+6 zh^y;&{lyqM1NgZEk{G|J+(OxHJ8)JEM#>)*u{djA9sO`*e^+P5juTx@i!*k#my>1I z;oef6$i!JPKyw<4D~bfQP#{(fcJ0f3;r{{b-O%?B4T+GvA$JIkbFZqVdv*s+If77E zQxVzg)~MR)qNB0X&F}D_w$EGH4trhKAIGJ?#W|)}Xns)3G!tUQLW5VsIC%<&?_15KhlK{cVQlw3ya&gcobg-uh4)8oZKGgJ3)jPgi{E~OH zR!u)HH#9hII*lwZVxXv0VQcrMcNq?6q~Nz5ZR#0Woo%&sd$^H!slrfy80B8aNKPlO zs}eAv9ni}|M-}hueM5XQ;=E32g8hWDwq33vcnnxTLq7%Q9{xg`P|`4Ozm$y@$3Asy zvlhc@{xAQ1}=AQ$lq?J#misTJ> zvPJ-@KCivV>yo*cIw`Tg+ycuxzT#9>uHF!EoLr3ysyNE{8;jZ8mQ_KbPQ1@Lx z7itC5SQb@G#2AFjOvf3_g?>^hp3fqfD^s>4R2{Fw-uu|F<2JEz{bTrOY?($Sz=7Vf z8qv0O{S%4)NaqjmqhI?GqJ|snQ3lF`V!eJ+AU=F+7oTcrNN6JFw?+yndPb!~@flRZ z#)O-{dUzJ79qNy9rLO!vP!Y(#HQ3vIgle3;;pMv^oXR&(HHc?nRV((JJN>b$TBYBeE(H_*@qfg?m@cA7*|1VAm*`4umTZY!FsH* zrs>P=>6G3HlQ)}Mh10tO#PKLYt#$Y7E&{uHy=aU4tUjN&1W9WiPIXF4p%}^$6SpV? zC&xx+R~5|0`OaA+%I`s^%P?m_ifh^t=Ccb4y{^_aZ+VQHYa$i9&)Nwtt*bX~S5t~7 zGs|E$RcPoj5T5MNYly|0jUyYk%U}QDnn_EzXlKkieZn|Jzj{Nk0B=&o%r4K0DRA0L z2+XmYgV_qy8ewo$T9R-y{v|k$-KY%(BrQq!9WBUUa+}vMShIjU;Im`zQL!kD@M>46!=3y5)&O-pfZ5 z{5~Ph(Q9;u-8G8wf%sQPIaa{4VN|+#k_|8Cga;rMuy>cyc(&Con%mDs3|iQ)tZq!b zO`rX>K=iWz$C&gGwFnbgsUMw>VCz{@*L zmr>e}Bb=u`Lb>|BCcZzOQv2#Esxx#gsS5CnQD&)j^5LiHumfR~jqspNY+d*Sc6@xat1$QO zl##c?`mbB4iG$~3%bb76$wu>SaAAYPn{?LKHcI$GpRu)}hh6Iz@OHOnp)<&85lmm@ zH&}=Q5|w?iIM8Zez*p&+l{<8rD|ieu%-FEn~UR5O+~ec<56tNgfn|2l_ZmacK4 zpmi#H1MrzDbpm6P?NL~L9zfIi+_4Aj@GiP2kZ+K6Zm>jjU5XIz<>LC7RxI`6WMy_!yR+u;;aCFm@hl^yudj80z>q>Z)9Yv z!>CxX`%=D}5!cuD+C;FevNDN+dea+#HO+3*F>Sy4NQs)wl9p_r0zNT@_jc~Zs6E0* z9tDSTSzRxx9aH-8idTWSMtc8NH~i3#d*<=;CI=YQFs)M9sKIV5+hn;IjXqA5fk_J`UyLp|8407R7j;$`w6r^w3SLt9)L%vp3V#DV=0 zV#_l9id#0c*I@jdY-yS<2#@Lto#y97*(Mu(v!M0jI$ z)SnqXP(zubU(W~2#69IceKfoxOr0<m$3T8QHx4(uuuk24cnHCl6#T?C)62qu`gV^G;xMHmV^`RuZObW?)!xuigcU#!Jf1^gLL14Kqp2F!Lv zoSLrT^0z$LmzM(KjWhe&S>_|&F@L6`@lEAl(DLPvZ7u%?xG(otj8(smSvy#;lsRFA z6;3XR=F9Bo?t-nlea@OhWW900zX*)**a)=iPCWJ^_$fzD-x=}h!}?ni=w1!K^jxPw zfe)vaZW$?$88@tq=J{URj#QF&-Jo6^*HuSgSNSKzsd*~XownN)nG)rJeL?xbB&r6c>g!`e8dzxk4z>yHhFj)S<1hw(;^W7z9xQXU5emkJvd76?pOzo5p6vP0*w?ZWz6s`6tw?11>+s zSD;5lsFQ{0U%MYbW3$6}j067zU_51|sYukx4Jo1lTT5U5&wo^Ph_biZiCwOop4%(Ch_ z)XW}twA7@+@@1-+euCKPUSU6{jM++J{V;7NE#LW6-ass#YVUk&orSshs@GMenptu1 zQnG+}5+pyb^I$0!i2fq{%N!;_$tgrNu0p%FXEHVJ6v(#D73TDh^oF+=vweB+mwfa$ zX0*IVaHEl+0{z_x$wB`B-k)MbFF)TK{g)^XSIHi+-zKmnCcT>-NWR%>s>&0Su)SIR zy_(qbKCck41Oh#|47oGnacFsM_&5a9V3lC=3%_oh^QpSxn+n-J$>lx)_SOZlTObHe5Aki!%BiuM_E6uF5b)YBr=OU-%(QlDWo z0_w(KNE+4nm&sqE#v^oSBjLOf-UMLzs*Z`6!^Kg7SFaoY zu+FouRn#6Xv%hYG*E?@fAk;u3-CiFb+R$`2MOP`w1byXdc58CMI*Ir!=ipiv4mVsyyS|)whO+O(OEn) zD_!X1Fcmt5KlSsLf_ozYfDH=?Eay7hEs=4%&N4m3vWkOgtcs*tPrvh98gO=BQoQp# zT@G-=K(k2FAIv7?`(;P;YXKF9a8o~hYUQD7W2<%HqaKnp@bt)@w8sE_Ato7R6(#$- z*2=1DrRc(q3(9&@aT3+ZK!hf&L85{-{!IDYQANAnIeC_^o&7ivr48r!rub@}r|Kth z7Yp4r!kdcfN#3`-_-w7`q0}`cX8~)EZFd>SDf5L!aPR60?sb*Ls*{;qFr9yXxf4F( zw46|aPy;e9`N94J^is$5;zfHEhg^E_vk6rvuJW;mC=v`Z{P@m^K|X{nyeAB6m@)89 zP0*Je+;yxQkl|?BiP$2$i>rr{_~C?YZh}3J{!|h>@6;$}xpc(>ze-{2=I!TV?o1!| zR6IMSVJDD$Bf<@Ed)7YGyq;3x(u#{17CO|`4xXK{WrvrH= zyx%|5E)g21l{)?g+QEx1EKc=Y@3nelB3}{Cy1acwoL49yZ|PtKU7UT zX0QeDvVN2NDlFk!#;8#kd>HBJpNx4mjvvn~AumPi1*YK{aE^Y^3@MV{J45YCKPJjj zIsir3rTOj9+)!Jm?cH|wYs*_yB@;uEMc=MwdtWG_A-81UjoM-%oiojWY}1XwxsXaQ zUDnO9T0?qXx}iDw*xM&Iks^WWpD5nrsP3z@HXoMgJj z(VI+z_k$HHeFWM%r!}S&3e!L8T#y`}4a#%NP*=nAE7`8mZ(-(0#|^*ijl+?)+q7Pm zYqyX;?}qH9X1|EV^ZmJcD9>$UJAgnHN$1Yx)%C@zcU%1lsMa4S?nD;^Na@EBXKg@{ zf#iHh$6oz-yvB-axkqUK=(2?h8>{>dc_tZKsCXzkIdkI4tlBc9c zxkSx(0=9Ei*=mra;hRx#e1W=iW$13IUNR(t@iZMcV_jV2jg1hV<&XF_W54QglW6W7 zYC*rfS3)*8cvJO}%n!AOKqJR=h&4+`{2=#Yp0px1UfAvig}T>C6}6rqr!wj4-7YC&mLfiA4!Q3BHHbzHMX8R^>s_Y{j#k4-=md%*i$)m=3>dFW z+_hY<=Y>c!U2ii$o<3PaJRkaeMZF$$J@fh@juxpJgAUejv!}8WCHy)z+x`P6+;QnC zr({}@iHva3#huX_AacOz?-M-;h8fX3t#J!xyygqY&ky{3ql^E}5Z0{i9XOj1i;I{` zOqx3U-l5kG6QGncf4|duQ%Ng5b!F>tVsJld{->3LNRgc5W7vQqT&=g=OR%~^AiJmN z@5ESMe66u-OgK}qcwn@y_pc?;Aq7k8^oDYJHg;sgEoAig&Rd5y*-m9hD&YjkTN>Ci zz-KtL?|tMczDjFEazZQlwx|*Kqpx4#t}*z+yoAbavt3tl@;GP0P~L42ny4ED&D}Hk z;ITEZ%F%>;G?0XGtTcsfylqJ2&6ITM;=LLZwT|@_sVerts!%SmO+EZhU+_T>-~p(T z(KkJmg(fOPaB3?^KBiKnc&Z3-xFQhLsY$O#L-+*7W+`h^#N%IkN)42Q#|hzt7=>XA zbZt|$_AjIdUcg`N1y94g3*G)rhC_E>zF3M}mt=r@HMk)c;dFg`yCv%cvtAGDX03;^ zmq*u>cLXT`QZsRoHao zmEtiM9t04=O^W3rclgOH%tjn)4VT&bDRd2q-n&R}3wAMyu)8uOlUbyS0HY(zWvvnZNW?D)wjJJnQC2JAokvUWyDQrqNwY#==?Z%8VEjTkBu^i*C9Nf)33?R=+mExCjqtdLg< zKGW`m+XJ_~T)v$KyOqZ}9w9k0!XlRJstVN5Q)Byr;bkVme>U@b$>6V<#^7_5{hDs* zxT-#;vE1`FtPp>QX6WNOZ5mFSwFpz-zH#V!=gaO*ERtq0=!Yqfr^{XSo38CUi>vi@ zq;bB$T=*s)UYm=J^PfaIih{mo^uzreyW%j2)g)wiQH`O!mU^6G3)_qE{yT96H!?b9 zjaRgqeCAL+F+_wh*H>x-zTn|UO)M*$F25coBJN(EW8k1`Ou00DukGsqjsokZ3ZdIw zVP&C!^HM;hydKaMtr_qp`ujM{VTY_kbFbNMHmck^rA=|&h!!<*$8`)#=iIQi^85i= z3d4+Zw1aU4@ZWt2`8{tItwr-ftwmab=h! xF>tb$WaZp)KDYZ26PM|C6=TbQ20 zA=2)8`NL10m{OKb_wlhk)FWyOE_rKNe_<)p&Y0|?@QMm|+3)+LEzQR;8^lN1rstQ! z>bF{6^`U+9FO8~)s~d;T&pV29^JqiO{t1Su7@RA*W9+60+XJ`(;)KC(-RC;?&(d=eSJrS zt!42II!>jdeipT2SZ%ZLD^||QS85=CIwN}$Mx{b)I>aHx3ud8fiw}Wvt|fWyXF+x5 z*zqyQ$xpMeXJdq9RRo8H6V8Wu68Uat3xgBL$N=AtPp>MAs0GCLXQPsKyvRRJ7}gF~ zr>6KB!2?im^NFc~oHy3SobWvLUzhN&MQ(+Eq8C&79Ww>fDcIQyD4QX;_*3OL2NR0{-QHJ%u$3;% zJ;%%0j03jBqO;;4r`N7JH#{pO3*Bc5{gMiSAC$JOsV<4LeifC%L}Qc0t2RmE#8Yvg zkX;b(`j3x(F1Q{N&nbJII9oHE;%eUoR?3Z)c7RLtQH%l_^0RcY9`s%pCUQnL)vIzl zs)4`8+@R61m*sgtD)v8laj|Z=DMRdh&^_Sf*D*RE4lAyZW0jc#J5LsyeOLFmT@wd^ znM`mKE?_@HS?{qa#Gx+*KzOJtbMa=D?0(+Itiq>ak1ZV!LbG0+LcdLrgJ&mSt6z}t z^|e`Bf-VgnJB_5HpKT962om74CAqj&#A&?q0G#|;p!B&7CyXheV|BM`hywm*LMUNK z6}WF? zS+uGAHX+@`>jk38pV+ntVr8SCsE8m}lUev*r48IY%CJ+2Cx`~wz6kR5YpP&THGHhN zWScBzWi3?A;cLEQvK1-k=_*p7qq{g&j~My(?U}l4r+FCr;;KrX`OKPwGm;dc<<7j6 zMLY)mn8pBVsTo zY$L7HpF{Q3C@zKQ|Zl7!#AMq>&L zR*7k%$KED@U;IwgiUuDzy7*(` zqtw~RGyW--fi1%2W3PjT(BMd#{o`MBH<3rUU1Q;biLJXD=F3f+$YeixRooNjX;iWB zu<9@07cJy$`}T~^$kmng)%e?OS6*yN#ZsNr8vkGCKvJhfTOlmFuFs7@xSo@6yW3f+U_2%;9GGumj)Q5_>LF!;b@hL&M}Opc8wS zRheq3x>BpJ31dcY4#Cgg2jB)XQd3_qmmYs>rPFle?Ikg*Vh~6##z}-fO(&^{h zU^7i9NHxkDy1XvR`C>pc*mz`J?Us2jK%nHWAJ-%*BV;rndRW&&^$pMVu7hrC&ju8dl#xrf zeg`#D?zV^1}&hZEilzmHBtFY&$;i z?%GEULAMTjPO%wB|KXZO(eT5WbPZGz_(3w3x1+E0*ckcw(`GR#iI&CYG;nJ?xXsyi zs|t7D>!|B<>AyX+Hcbc%SR-(}9VS^}lo1KrA}vEjQn=1(UgCNJ%z z?9S@vGKnu$(3nnkWR;KU#A<}U0o4p$s{&B&;jk*^nV8Lcd;ibdcSVMWY(WK@sr7S;vK&;9Mzt z5BXSEV1Wj$&bK1LakW*+_{8>E4%$9~VAAY1Yk_`DT_hS^T=P} zi*%nF8_a7Q80?I8)7pU3Y{~4`lNNB-l&&4$EQ3+Uae)%9OQO&WFrR}(Yqa1ZN=Si6 zHyu3PSnpqJ=xPVT5Te%hEk2`GqPlCdEIXFMun^Te|f)yx{_^*oO? zx_ReG?Pun>Z(IZ}JP^fA!aoOnC6EVz%!+>PvHOB(r`lU^bzQ)6TYOwr|KyXvnbvpv zJJ7}{P8#x{q3U^bR@Ghg*!26zlN)5ui`v)s0YM@e|dxaL1l@B>_zxVAn1g%V!ZqdoN5_!%7@Qi=d?a zOObK4i`9Z86`NS^Mi#wp<2{ z%k3$5?vqN+FRLKLpi=b9pO(Vq_kJmSorMapf0^xh z+u8t~t-RBS{4?;CZrKULSOR=5_KZ1%^coqUC9YF@V>|UDsaNO3k;;{n@EFp38lpAc zYF_Ub^E_=r%kRIqwjp1>*$yN$VmjeJq+S5GrlOnq3bVs&N>3vn36S$BH$ByDs~(cG z{C%3{?8$apGc$LGd9@Y54NVxWfz1ny&@w8x%c1$`7RRMyCNJ;CeZ5z5uQBvOgWJ7? z+MmK!teK9ffmL<~$b=I4UVay!q89_@mAajIZo9nwtsp{9#>yAG8n56YL+v6`@Re%M zN6)x<{|ioQPIHmzc@PHNFt#p%duaU2SM)N!(M}47=hr@l6K%RLY&l@d{oVkSZ*$AO z#Mfinix|D$Z%?sg$*zMo@& zL^xa$BW)w6*3HCdVzy}7XHzlfZ^C)-@&KIddEd}gVCYBjG0TcWC*9&mHO;V3&BFCt zcB`pJb;{+Xs3`T1?SDOb%T69jpaITFNyg&^sLf-~=aS{#7dGldWOF*Nez{Shx>w>t zbf9rMd6_z6w; za6a;GZr38;mJvhr-nhE9`^H(!4+kJc)ur6g!s#EZOl@1mNS z>^>hIHHI5YNq*=4(=)~*jFPc6{D9=YZnG_1qODiX%_J8A=;r!-LVlVA)!CBSGNuaW z(OZ2;C9{IxZMOKDQP(KHO<8iKFV&x{I!8HxE zvtYvsyOuMh50)CJC@<$8Xk+vx^W#43x&rF(Fzim1<9wWPPyrkLz1$#Uo-k9DzqeZc z&q{lEcp@Fv5?1`)8cMHiYI7*z;koI zynn)bKb&tF$rvYlpR?DRbNyy)N#LmILUGynnUqD{y9Ydb&){s6fVA&i6_gK?qE5k; zM~1#UlKZt9?k(U+Ud%T*QraHg!m-kQW_mWjt9xCKIL(2|bOue6YyR_BgZud+Rdx6y zVyLuE3H@EoTUOnfjAjBlbz?DE9jdINb zt|)b5SnEWp0u2hZF`c!4cbD+g@dOfIEx(us>_YccD0k1C(-tATy17b%;>N0y*W#1* zuimquSEZ7iAD#qelf^e*K4);(P-BEp1#3t1(fI6Dxnk>){E+zQJ~UZn`1k`0Dag^4 z)6s=gnIvPt2Kz`+aEEqJCuSeF{OS84yM)9R6Gkancq8%p{oHS7duU)b4L#2qPC`)1 zFBYhI6K7&cX&Oli9Y;XK#+v@)uH7FWDpn=I=Q1G9dM(v;M!_msbAu}yaV)zqeqzS~ zVmvxIQJ$#lHJ^H(W(Ena)eW?K{$Q{#ZeTD(iiY3(RW%=$iuBn3=Rh7vA1BY=bB_MR zewj0SLzkm1T-@zbB(0>~{6Vywij!nsf-%s_vr_Vz zq;-vmo~NfEvA{-#yx!NR7rpzj7fbkEYx;PQ4YDuelz}`9#6>E3ySOCWpU_KTwXo6qJr}cEtN<6xyidb-d7}E;`T-J%R_kehgr{mBkxl zi!5!Be(&w8ePJ;FA0TD7-YmQCd<7$$-#$K_&JKLi{Ht625LKJh*zH__I`3sY`jpRwS|UA({G zn}Y-zT>X;ywG~At^KVS!RjpW6R>)AwY3Czb4+g_upPsM!Jq-A|H=W$v4DNahd}@p} zVdj^iwC3t|oJ+c~=xjnnF1_<6Fe!H7_&qwG3?v&4QQrGF)4yYRt_x0jS4EY*m-aZt zT2h>vwvO&h^%wPaC|0UkqvKc!-jBAw9u8;tau0X1l0Ub0eJ#qyd<{;_?|}$Rw;Y&% zs|_NA95L9_&k+n>TBW6{z?ME{rp-zx_>NQ>hxTleW7mJ4ElqKaYgO*s7u^lC@f!4c z#o^6$wsBD2;EYwF`0zgGY@rNdX4YT6rna5O`f}~Uw#DTiE>J%#b!A=tm$;y0TJ*m$ zfb@R=o`13&ewuDk7t>B&)~stPV+*(Vs^~|1aq8O|4Pfpq`yLIemjuNSLwcQ#WqC=SsauR;BdL9WZhfd{h=0 z>TG6m!-`lh`)b?RiV*8JvsyiE4tS?xe6E(C9jQz}@l@R^@eZP?M0~pPMBXHc`VU~A z^Ng+cIr$4_K5=$PUYHXEzENPeP$9Iu8C_nDQ@Kh(N<`bmLKN=9GYH@{P_=! z|La}f1(b=OdA@X$#(Yq6d?w-6B{fKR5_T65@(s)5#hDT%HM8*zBbCL8z+?wu1Q}rJ z@1gOKpYqfhba?%WwJUd3Qw=q|b1{CXaM2VzoyXVh08M0WP%0}jLw#(V39^IVUnsV; zE?#kunXU+I36***N-48-!cy=Q}9Y{**%85Nchc z?%;Z4G&{U0UCM=Eu{UgOjotl*9Ia0IGp{`}U23 zDmHHS1w&On+`>E6njBR4pDrAvh2C6^>lYc2K0La%usH5OQ)WZ1xeBsE&cd!*9FZ(4kW11fDnq(Ms!fVcIJ~O|H4atj; zjNn%QMBqUupv)+)MLC;I22;QHcbOZ63+l{-P87dPC8W9rd41efS3nz&G}nTyoAKSx zrNt1xdvapFqIB(U@+JS_sItBI-26Rw^rtgpZCT+zPc-v`1V@Mfs3&25(DHr1sXp&5 zRP}KFi#?K}N0ze*4jRx9)0?|eskKUFuAc;YRHPC;&ZQ(Ez(1_rcRFKWc7x|%yEu`) zm529Vd=~HfP3l)-8*kPLtL1S+rjsLNyaxSg2B&e6Y13nxim`+`q_t+-n0#qU=@{u5wL)Dc`+zlN8YcaH`_W-BIT|&) z0o5MkuX}>0U%sZ?3ZKKcC@3qRv~4ina00nF@k?^IWvn`l7`e~^YW-QRP&|d=v;92M zRoN%^P+ezD>RK+*`#C2KCD9Ixz8Qo1MfK}h-CyvoRuMT7a?<)chkXargH|BLqR3wo$CFh40cqw=9ZyxC&iqHF1EHrOVIrr+29!Md)sb*VBf%C^8Xt>ek=Ijws3X^HraFk)c7- zUxVH-;U5)J^-tN(OmDZ``FVGPhtb3zAo@{5gQ9#G{);TSiAhsz_Jkut#(0%+e`GiC z!k!-Fg9n2`2LjE8C(ubS*Na78pU7QY{fy#y61hBzc69>Pv-O$dIW`tNkDw?lLn7m}^Ti?lG>tgYusaC6?@eGea<~}c%=1)h(N{^?EJ>Url177Bwh7h6D=ekHyDu1NSiX~`r%w{HkiT>sBWD+_V`3PEQ z^&F_f4F@}D!*Ly{ZWUcB<$gjj{8ry1OSaZ&1_LR<`)vRu6&rmRUOfD8 z?~uA-73rf@A%3P@;%^iOgle=it9~7_PZBh<+_&J!`w5nZYHDxcbVx}yBLEm z;%#aN@o#@dX#b1rV&Zej&3aZCGF4VK)0}J6agn7${sEC;YE>09!~0A{29p@HQ2faV zFt#VN7-`5b_)$o*R)k}c-OKPEtEGkWi!`|E__RQ+za4IK%Rf%kKXE)ozqZjgR)&s6*OjD$CkYXGVtD8q_4jor3B^vmP7sHKm@s9b87e6{oi6 zl8g_N2l@MdwlL-MME35Dd4vfKx}3CWPhQ7}Uj0(Gp=QCjYd`1|G!ZcvrJF9Vpx-qa z?!3oH2-nv&ZvLH86sMR5WGoY}bR~EvGPV9krhR1ip+Co&zpCn`Gq@FKRc%MqGs>Ay zytMp_A0%;GoMJl3`g<8~N*;i25=dC$uNqyh)P@0>1iiFX3TY9yF&1jIdf#;2l(Yh= zbbs*?dMy-CzJ0TnWZYiqF0{yaB$D*{68Pxo?`$FEq$9C3;!DoVc*IY`NMI*ZxzigV zBc*H1f_K1p*f^?VLZ_?H7(L2zUA~N8G0c%f^+Wfd_wZ1IENuP4Gq_4txA22YW1!Uu zj^lKO9zX)?^UoxFr5)V4`TQb=2DGUmBbq!QyTt>0R2)FKU-`Z!)gv+?GJ$@jJt2%v zo+aV(bNw>yN9aCDOdHqGqwcK3NNB4vhF_`T z0rXv7@JoK$3VwZ5t#l_FH+wWZdkW0qpI&cw1J_Tr{hGZDRPtLc zR1UPR=W2f{=NJE={pd0dWlE z??3DnFbWM3k#k>&U-r;UD!{kM7Y%I${%lcL((}BY5-*d&96i-iWm!@3D|aRb zlz{z)B=x0^j{^xD%G$D7;$C#_rbJYR&!q<7U;GD1vS^|S>$ZauYgG8<8b`T)2&`%R zZAWfq#^g?hu|+P6=mYO}Gle=Nt)>lcHX0Oq+uwTnh4v+^TK{z}d10muk{BY4S=tV+ z)ba93+Wa%-wI{pzSot4dt2~4m&MpR_Lw@>;jJII3KIYp0)g&F+&@my?Wv}huIux(2v4d&!ZRhIL+v8sR@g?mg7sqfTn@nf z#nKiK{1i!bD2Ek7bu#>e>kL{Uav*xVuR?|9W=9C6 z9a>Zu)Y^`3@7ur0i&?65r9c6e3 z?|GQtut7H{+wFEA2O>57V1Z;k5T<10VmAG3^bF-ColYyl+j6{PY(sH2i z)q!}{H8h)*;!y0*^v$Q#h@-6o*P#PU%tNGAm$oJBw5|B;dT|jhXH}7Bkb#@}-8sEB zV@2i_)Z4XL`t73rLI)9)=cv@-MXSz#fUbly89YN7AGl(GXgs1w6%m6{Y1H<|zS@1B zd$I%;UhStwgO2G=ysn`pi_#P&$XF#e7X$wkLbUiBz$S>=%$ERc_Z{G}9 zF*m+)tR0gQ0DNvK`&y-OQ4!dkn8Zs3`GnA$tDFh>`vyGr$Y2m!xUY?*KJSyIo4W=P zyLR_$%#L)|*{!JCz7>6cFPb`x>}4J_qv99LvywC{}* z8L@**$?vP+zn8e-E+_?fRILa>F5We6N7|^5s#tK2;4&>{i)Q-k?s#d{9T07QXvs>b z{r9}cy5+Y;q|acb^1Bb-(2^^Q=zE{EGIjg0MUN3YKKIpJh|J3jIl?cUqUgjt(m$*3 zxbv;TvU-z6PY1xN(^l?*ksp<@4R{1}{xT&XGoMRLRUf|83$QQ|aoteP_Z!|mjRAkr z6)h}LKXAVItl0Vz9OH#8KGMT-U5(Z`MXR>Z@ZV!Mtel$qbzvnHcA>i$-vC5wr{)sV z0k^wA4$DV#xIsZrVCTkJZS8lUmMuFLxc8(}DO%ER*X?p*GJq_(V zH->+?XtQ&88$#|>2uA1a#-LDaeq!|c69w(M&$)D75_F*>-zH73W7aB8xTVn$!<1`+ zI6%x*V593Bf;!W7CyC5ldB?A`xh+*+bywXCiVu6?sjvmklJPRN8-Cfv9E3Y#=^|=3 z*P&d&#wgYAQI~3xY5h>IX7O5@(RkkU{LAL_pT{JEf=~4cRFWdBJ9ECq zRjXz}6QFN$q}5$mesZG_k@*hYDqfR+IWq0cf^V0eedPCQ6!<5>1@MK+KYs7F`zT9} zG}o{gh&+`QS=glsNbe1vWK!it&p)!aR54o`bvJaMY*sA$h0AiH)U=YGe>{mFm0ZGwzz>k)4{OiL_%_C z0PnZ6{G3sj!IH z(-?&9#u-dn%BaLnX4<23AA*BqRa?|t8=dA;9RTB8%lmEE4}-cg&S@+KM3&p~Bdd+VcMw7o9mbyg3PGhI9FTv2onYTD1f8W@qZ+1-lMk zdqEIsASqFZ0}36X5aEiFM;gM|cBaE5OXeRFqShPL#|JKJM!9Z3jYeev-7z?Znf*}n z_mPU$I^TJMR5sEB8aXD_H2Fpg5>ru{$lfgzf9Ec?AT|VfaDJR9+H9a^rAM^N`ztd1 z5~p|7SPoq?IPgUanVGB{8JEwV<^6V@x3Z_^SCR7FD&J*7o-cHNs+tVpM(fvvvcgLJ zL*{sYuPeq*Xi`mL&(gi}p3jnFKzgf2D!yxf|Gx0=L0XJ&X62ON=r($G0x|oafUawBMGK~1*Epbp;poV0)DSnLZ zz{fY~ z978x~PdAC^jW^l9MS>N$RdJ6uk}OB>oc)i5Agriy3xnWDYthVC>g;0Lqx4|W5t8fT zd1;LwNL%v*gZb(&|C$fPXkOCxoT-W+c0zjvP+}z$!ygd;0pee~*z6lE?0NP}36TK3rc`BHen8CdH@Xem2Z{18H6qP}qbFkS)!6zG8yuY@*c+7oV zDWD1EA6!S0hOpo=s2MVio^CFG4*c4EIA;&~*yV@6O6E^d-adsQ9Ze}~wfm3VVZ3^yNG4Ukyok^HFrY=EPX zekcD>d_^VI{Bie_n1IBypX`(@DaYsLjWnh(o@sQAVwvNE}11 zIy@=QuGw`=gHK?ya>jECw8R5sNp8kU7#|H;9$yXv5Iuv6zp_=1<3t#vY=?Ov!HO>c z7s|pXo=X0wDn=DULh{k$5HXUW)uPRy8;9fwebhWl#1V=p)T9 zGanTyLnz;il>)xLZb4Br+@!ljFw>+d&qE1iNf8VjB?nnZ@QU*eZzLp)TtDg7DD75J zLK!0684!^E+*4Jo?!oF_qN8PPP*f6Lvrwhzn6VqBRZLDIt@E72-jn^6Al~Q{wps#o zO6B_8T({{;xrv6?@rtdrKKdUO7ojqi?&~X`IQeM-?~~R;1DL7ke*lnLo+g>5(wgs} ztWl@Y-JTOCb<5kyB`mCNhTN7C9xLCmAjh2fh_PA)@tq3MD~`d}0jYzm zocLQQn~UhL>U@`;;!!X_@4R4eF}x3WM;0cDp;xpVQ5eqS(@dgP)oQPlX80<z&2h9vy+JQ*B{C9gyri_xF^MgC>y@OhAHNiEbVK zE)lhn4Pp_e;S?oj?N+WDkc`#lbt%s7DyNz5d9D*j!_%KEMsd1Q=^(k+)rL9l%78bB z=hlID%~(Y3?J&$Wmk(HbyM3tnXtKJw_KPPZ|kFT<}e zTq6p~r9001yO$D8x4sfVP!H)Z(Uvq-JdijBT}LvbO|+V(1``XcR0!I{sD+qkizvC`;U>&4I7d#q3g{>J>wwt3;<9Oo za606Fo&myFYUE+Q=-J_{b8YHNw3GW-l|Bk6_HH4n=(%H)Flhpz$GO^xrn0l|xqeab z^2kkD#Vf$HY4;vwEH>I?Q z0_{VCd_fs6?W`%-Ebgyz`+4=Z9J;q*Flqd(vaB|su4okIFUo7T?+<^zZt6+*h(?Cz zcvgZt?q~eN5F5GsHt4^k>d&;n2Lu2UGbLe;aQ!)|qVz*Dj=eyJGu)8s^`=#7^On&S zI`i~YnT08m@p$-2G-Q60CTU|gq1S7CzGF#cviw0IaK2tfy7M4? z=rC6|Qoa73_Eg$NDk#=H zbVcNFgAV-?_Ofp=o`bL<(|3ZEcW7d5`x_T`Yq|na#i{*A9;fD!l_wdS*;sK<;cCWKTER?*7bA$XTmClvF=v9L&!f7Uzk@SBDig(n@7Oa%vKc z3I}&Hy8vY`^X!WrRW9J=v3i1s#);rd7>3_5*q$R|Pvt2ZvPL$%kUe6ySum5&V6#{6 zKYB|!5Uh;uEdB>TQZWzy2S~Wdi7p}wrs&TVd0sDAKgDn>02xj;1#b&j)I4#)HOju# z)D)$>K1kNA;aZ_g9xuqHW!pb5&^AXeGpyM^9c)Gsp|%xHzNt-9&7Z_GRz7dD zx`$XcJqusDP}uJ(cM?myCncE>cyyW=e7M*PWIMVTnC%F`jP@@7wsu$Y5>+U z1IY^R2NtLUeGj^+G))GHz+HV^>TQqpvM<&;Hm#AEI}P{(rt6x&>h4i7%+Q58Yb4@9 z(Xa1DVS6)5@^f67y+W;d{s`XjZ6@14-W;RYjgc#d?cGHo(3$L?hYv0zp8^dr5d<*m zDEP+0nFWPoKuEY%r7>?+?wkP|qYqiZ55Rtx(|ici)88(!Z$A;tgEDfx83Q;@Ai|Zi ztY-mq#3a97wAGRfg*Ht0$%VC}5i|pQarDte2wdZMoDDhB^~S_tcN_!u1aB_p0Tlpw zfPqf+qa1~w<(h78;$m*g^gDu^SoDti#@XP(71>2`c!6v&>3H#C!C>yre@G>Q=y$PY zWVB_N0t@}FoLzTZfO20`!C=lop9Rnlx?@_G`|UbdlW`nB>R{YTh?W5L-i|3U?y6v! zWGnynTZgtkqJQw-r862@l_4pb=>~{klwCr_jfD|)b5i*|v`J^eIv$&(KUVxhI|g2L zko*|sN8lzl(Ked+7_c-t_fODG2ak1aC%!a(&CTb(9JJg~G5Ob>MYT7ZJsk=A8FY)G#kNRT?(yF61pv&Ijgqb12-eJr(_09Dlf z7;Ms%xE7h`A=8mY?o_IYrag5jkBvVN+vof*=A10ZWv77A?9PU=qf0;m@}M0M;=O7g ziXn{wUwn|XAo5dIpXF%$%zCEsK8*~0T7Av%8uIA0!oJSFs+O81fm`+3;ift9j_ew9776B;j?VYj$$PlA3xzCL(KSYc93?I7aWs=Em2&l9~XBCr_!z%NB#u|+|&y720#iboB|w8 zRS(oIBQQQ;1W`1F!}eJ_oo@W=EGV8favtw-M4uSoW5(ZmmnqM5!^JFTf%dZee@jRlDthXLxy}{1@60*THK!42NcKOcrgq52 z3{8%IQIwwNlsoQ1TL}&I?BAlrgZO#J7@Q4%Oxj|0j9Ku{4xt^A(@wa__5ekq@-Thy z`kH)Fy+iT3n#n949`iRUv@@q0$s6aeHb< zv!Ot~k+M%nHc9~*wY?eLj-iN6CZ*?X`g9^%IUJ!=tSUhmOV`9opRRbh=DoYb=imBWUL{> z@+WWVo#(xLYk%lOhjxz>o~XdAGm3a=|@r&8rN6FQx3i!@_1!bRx5iy zT2-~%HqGo&c9k;C3mDA>-3is`T>NM z_ntVN7U3+&gx^5-VLeZx3$jeeDpdqiRM5Q-v}7vNphbXKf9@bHnNxKZ-qolET^xSvU9JklV?iySWcCVpQEZu-uwg1 zU7C5QDriu``RpIb1(XMA`Cy3RMs{T&{Je&!`N}~ppY#3x1AJ~<^IsuDphwUppa!H3 zKX^Pll}kJur;fZ5nm!oa@)17g%6piJqGL-IIs}2W25LjPnm)a2eHP|1z3{cxemyov z6hf1rqDQKvu*uD`e&L37Gy8#Ej8HCU7?^P~9K{?Jz`JLH*?p{5M~y2n zX(#De{2lV|Ur}s2?U+DQN#|cv`6aWpJWof(m;g_o%P<~O{@24e>Fg}GSO(3?LTT38 zW_~itB}z&rP>@<9J61SSN2Gyqn2pMSLv1zhtZln#fukjJI+ZK-fZ*?NVfmE>@qYl@ zO^~#!KD()~bs<~#Li^juaTfQ0hdD(OS|EM@(r|7;#atc5?v3vs$MF_6;KN+pTVM9i zKW*8q=_;%=^Bat-vZRdCpsghU!N;{oh(lSGRUYvlp|z`;57(4oW3r9I3D{ex9{t#4 zvdAUxQhs3g=ADlH_v@Dto`4ZX2cNV>5u2?mI?Bl+OfE!4U73JUa)8I9qXdRCsUWUn zBO*e4%eQiZ!5s|)7fqxf1B??7T;>G26HCZch;9cEx0TlMC@@OS@Ol)583k{&UX|M1 z*%3s^Jn~pAN%=AL6d&86tcIhrN8 zS3@Uzsce5}{Rp3i*2qhE9m((4U|!_q;m=6Xs{UR}8oTpKK&bBSk)}2Q?Nc;I^Xq@#7j?l_sxx zo`&D+wfVeOZauWY$AC$H&fOOx^+;LhnBI{)C4(<+Gs$hgMr7ocYWh)EaOFz$Oy4(- zhloQ-=DN3jP@BJ*%T+~6Y%zxJnE%9kw)=LWITrLG!)ytcZbbY1<4Z3Y5y~=6$jSYk zuS$r;z)hv`O?Q!>u11>nqKK0Q=hTaK)pJgx6y-K6?4VKGExDEHof8%(2rK-cFOlis z(Y7m%thFmqVoOwoXg0a8{S@=*`RSHuj?^a5$($9LUJ*f8h9E9t8~(k*FORvj-<-gC zZ?u=ecBgGP5Zas>Rcn(vygijz9i%zm;!I{Q>iP*cOkGo{2S=Gr#)(1`>GAOX=}+}6 zvccRUqKn!=Fh?`CFIdM0w`ektQz;-s|2ifrh_29&O*Q7jpD(#GtbJr(TN=Wx6z>7N zj!lW!fuoxj{N7v~pQj__95f-ieFF}3Y1Il{-DAtV(M2?RMvRQQ@)JuuytfHy zg(@R``e57V(KbZM_qGkL*N-!Q$2$L&lonKcDV|G-**Tu(d6pu9S%h(DE(NO!gpxX_ zQX5RsKB+`>^#$G>U=cfQcD(ypa$QwzX`K?Y$=cyGZ2X~JO_A_~u2@`ElDNB6Vy7#V z>bX;J(w__CtGk~?2+p8_2W>xdG=yBL#@>@E<4DS_QE?A!i~o*`spHe)UiLc%!L3~& znMH#f`D>TYaU{*$A8mA=VHTtZGFuOZXrS!j9UDZ*Y^cM9l9ffHa9zSDO0PDRoBa%3 z!;Ip8UG30}grgl4mkhr6Zqx7h({-Sx=hEq_aQ-mlY%hvr ze(J=REa%Pz2R2(1G9B705kim@-QAGbT<7g-Js+f}StKNgo$?7#O#o?K&1Am*GgI+K zMIzMDPLqjg=I1xVp@Mo1XcWqXFRp`V2R-lsaJm4Fdlkb*+Sa_9wnwnYWrmyd{z@A@ zd_XGM)w@{tLZqTV*aK$+ob$&&QfuR~Sa^A8^MucGw9gSfPyvxD-(g^sR! zlPX3Zr`UsRiK12F?zAvkJ}W71`&1@)}+U>LN*xv)9K zgbj~atKctosK|#wU-)`TdTdff!82iYr(}pg*!6VZ?`hJ1vbe1=A6yjAtG`_TmQv6p z%Z{~v=#d!WcPjn^>+G607Jst(ijC2VE3tc!moyx1#(Yw^{or)IDzx(%O2~iySC~;w z{qwuA(U%)rflvSJ{_(cFDL-D&PMw)<+eUEetH16mqif-$+l|YWd zpk!lbz&8ax?anN|7O%w!=?{f1mMpf0(g@?dwuNfPSx^11tCr8!+onS7dStioZ^w8K zsl=6SLhOUK5wII(xbk2D&LMqh%vK9rwCTs1$bMq^p-*`gRxx3WJkFj~B`By|`ho<4 zrmy|<2E_!lpm@*01_oWE2t7J=83*}z^?4XBlZ{(gEQl%W00|nC{adTN-#+}NP`LZC42xGcakAhZq{6Qrt zY-VG0bA82atNWCWrjMBt8NY)&C`W)aRI6*mdCk>-G3qLU%V@cwZ}LhgYTQ+vL0$qX zsP{R|r|w*tzih0C7nP)e?0ooWZ%#0eo}rO(6H5y6_VK_;EFybhfUiQ{zRN<(Wk3Gu z3!~FehaB8m2LXz!k!5-MOFtOZff(O+*r0F!{l2mBo91afjycv`^@SBXF9LFBv94EVH*Jh!kOsBMS0~{O-WwW*|=GFI@PxP4E-uYRA zK??A8(hq?&24Q4}L+6)q7d5gE>W@F=oX;^$4kb%6I=7Rs9H7;cGT+CAl@3?Q5|32a z#lE9~jL&bY<3rIpTw>m_EpCMK`MZ|1{pnYmFIOX0<97+cO?BvQ%P2Cm$UzR?6lC~w zDV|97!Bo=wJI~2c27E6J`y1EeCps=^gC#@F67@3p@jdV^AE?BXlc-W$jFvahly%v& z&)9yu1&-AwuF5P{#V&&$vutTIKZC~v2T~wqGo92yyV~isD%N#bXSUd&N#+fp_q5{$ z32Bf*u0l&|s)ENGPXh_IG2qrGCe(${`I2GHadYX;hRVRqTU!RIzg|g)ad*=@jhUi4 z%f?5(1RUuF=M*PLWV_ZELuL&rn^$Fv*tP=u7XBDl4(X6-lgj#(vN;=I*kor zjmwPB*@YhpcVqv^=@x|GXy_wA;a8pbWQ4vvvCtMQ*Gr?MtXed&q;+0!4uSEQbW1jlQmt+3W}75bIe))7rN&E`%S(1CUr9s_1|-Dr6x|iDmnh8 zA+d4KKg-Dz$==^@erU)zI`ccN`Dr%&s+vkKAT_|SXS0RowBRSQf4d9K9@sW9M|PXf z(;97<+22eHlfG5m_$kjnv9x-z`>Af4YTCHIxMQ+nCg0GWw;&s>P~#2}B!Mj$gFf!u z6$`%e{jxJ}7CPV4&90hcgD)qv(a}fyYivvqV+y2puihK_D-$fz_Ni^Lv=M%mcOWP; z0C9#Tr0wyAS}6u*qPrzln*jTi>cv~h%vdw_i?OjjZf0OXvez(>!rZj(dqoN5x%EGI zo>~tq?$Rdu-MZ00I?F>ya`GmbJRCnucADJ;#s zIgoonDmJjf5K|^N;E{JLbVcY!10mL9^6k73tO4q~(6ma@QhbO>%CI;S6UQw))dUMt zm7Jq-fD#$17JP|wZK)gUvS!-bB5O7U) z(MRm8HJtY^p6F}tHRjBfjq$Zcz#(_*y4E-Ep^lP`O@4nJ`DzFW-G`PDnE zd#K7JFlq1u6Ql0Wm;2d|VX$B4aRk;aIFXGNPq$6j-&zl3?q|-iJ;LLJF08Tw5c2pv z*TR1sR>A!KrGK7X3zI6FbFZoi>g|M4{3=-!m}GH%$^A@G%3nI}&+Hq^xI^lFzD9Yx zwcO?m(+iA~V3}hv=EnffRD~PLoc0Ew?buxAyMKu^H~tNXi~^jdtxZZYV3@Fg8BskO z<|QxHL$xgAk=IQfS%Fx*@#u)VC!utFS=uv*m9lP>PNDhvN(X>JU0d!8gTO;AVv-`%z zCiAT-bebXQYY1IeoD@T%=p;F7taKy(NQXO`J$U3Vmw`F>-HL<1sT;uIHBXMlNYLK)e=n0-nYBBKu2oySrFvd)HgAngD2Ys2emfgr?;i znvcEaxBw?_*jOxITFo@6||`qR{nZw_X^WV!rl@xMcH;Kjd81dk=MG^qD2AHRSY z9K^75KdHK63EItCZ#|_iPUJ{Kzh2FAu`?-3n3f;>DBre8#~kJvDfZS)4y7mTNtQgA zsk|Y&VVV$1%aQL-V4liT+QS7t%&P5z#Rs)m&=sB*MQw?q}YKbNbd4iBoBxV13bu#@5G?ep787N?g6F!9n@xFp83qrW>0Dm>j- zLHNUy=gUjeZz(O-P4D6L>DHr9>FjA?elX!-Z$)Xy+Jfs)$npCFIADP87J5_yKc0iW~r7P{JVOuUwAv)i61!@8_HU)F5c@-^vV+skgXKu+~-_~ z6#8?l^avc!R7(d1e1c<`OxuAnkStjVi#`aYL$hxD+peXh)k&!bN)j12n5 zG1%@4=|tSEg>SqNj-PID0q;Ua>Vsv&#g&ju0`)*&qGI*i zVu-#X^EZ*p&_O7YTdp@L>Y_8pFC1l934J!+U;f5_ir;o5AsDS9n=XDupJy_1M2>0> z29my+^CB`T?}Ypf&GlY^HkieISinNbA?a74{m^CQZN9#??RNNixNyA7TOeh3YZ%DA z*e-=Vo5x>9z&MajW<9~o|4P;zK7DF|=!XT5_rqtm28XiI$uvjTP;#MaDH(sfwj09v z&;J2#)E|qU#M%F%l#xw#TaQehy;lWjfMrLG%g27R3N38ZZpVPdrb!#^M=r@d*{6i}9ilEBm($E$fil)}ZMVeQEpC!5~26 zC*dnm_g89i*w32EtF+av-o{l`I%XI%<$HvdulHJ7J4b&!t1UL#`34yM{FwzHqv6*D zeQ*h)nQ>j2mv&%+x_3DLef6dZ0Q%v_>iy`@#_=0-tsDx7yczo=qWU#JEUfFrv3x>x z#uJmhwdE1LZ-SohW~1)_!{a6#97McOWDa?AJA}%)6`EJ^*to>kww0I&v zUp^SRdBOCKG1AOA!8SJb8y*ur#&=A8e>~sz4e3oP1Wc**H^)06H!fcgX0Yb#uMY-nv5GM6*UWrteNDGzZ6L~q%X z5p^4tn$LU0>7aacvi7Y-CxXU~`S$a<^~%%cob#4*Z+9i#_0COY^+WGX`K-I!4V z@T5LQFLgw1iJM6NX+=xYhi3Ohasg(}K}I|lAOweZsLxBMYI@$~U^n=HT))r+gPuP? z#`Qmd5W@Wp*>9$zjjZ@|<{IbYC*T}PF6h^haqv{G@CcVm?N9rqp?;nv7I!5)nPUz; z_F``x4-41)=|1UjIF1wxrYf4pdFn!(wKaMeBYjE;l&&&*Dh}v zl)dS=6JTB#CxW5)Ptny^PG$se>t}1&NI!lwDd`a4!eCRx{}BwJy0bvnAIV4+X$6GI zfUJ1?SDBWW>cOT4e%L9+E;ZAR2wHu|q`HrSxu{;P3u;r}WV-4r(=p!FI-AZ*Is~CJ zAFk_f|-;}i(T%^9+D_0sl9v$A!xyWd1Ni+VfB0YDxcz+{A)y}7}d}m1c zS)ImQ+)t|fXft;?D6x|JXD4hYBJKd?4?8D8FO=HC9T25s^Sx}mHC<|8Rb&riMEJOX zG`5y1JK#AK9OQtI+Oaszbl^s5`jUT>rN!Vy|2?x^IkHY{@4lW?d zs3>u(P?c#d6kCd9|9O?GGRLowAYIb+hOK3v1M;8K>mRa=5wqxegvYv_^+J-Bwym z!3X)e{^RD9ceLF2?;Vpkkq~6sP}nnvQ4O(5bLaiT!Krwqk4(j>l1!P&R`e^*K7rT( zHe8T-cpeDkAI(>5!zUUb;BQ*_IoVt_svqUT@dNSgv3}0ES^^9QX(6+vzSNLoV#+85 zlgOYvH_@Udkj-5yAF38lOXwJ_IV@7zP;5Kc^}87qqz61hF%Fott5~&$4}v*UProE} z>JK?fkmjac(jF^s)^3897J3XcS<~d!5@(B7e~PLKUx+N8h3gy*K%(x|j8-jdgXj=QcNm0)vYPbYfK zE4#p?b5 z8-DEkh(}pZCgbX^jsFYP10Td9vcg@i_!X?8jq}L{vzmkmO0+d{nomV1cvr=$N%wOB zQ|K&WYfu1ZJ@a?3aO%NTtsEm>KVTfiAz}U9_q)$s3~qR;X?9+?}^_JEo_rZfzqJ0s{4K z&~_iep_JX7`t4;@c8yGa?|&t<8tY?kt%XRB9~@L?g4My$|0&62{{zb5)z9%yF4tMt z^$E&Xq|*QK|INAoNBzTwS!i$6K|UH6;W~fiE8xZ07NyvjS7cOm6tgt>PG`|`j3syb z>la-e!}LCr{e-uz*iZ1XY0Qs;rlu(M5$vQ*E@*JH@eUj* z-A*u)Vv-EzRcc7Q8_S+h)~!V-Bln^a(5T$!s03u`udJ=qIM^hC$^i$~0KD^@i87V8 zF0|Efn3ao&kN{`Cp-}uFzsDipRI%9Di22tb)%l!~^fRCC!$7}k0QnF1CI)jr*C@)M ziif;93Q_}|{|$((PoPF?u6EG^pRb`pma+m+Hhrbna#B)T;yj|?-FIz#R`+trAV#_$;io{H6BK=-`+8Q3+gVM8q;`pm_ z>5G7$RSZ4^c_)~8A6Yz6OS3;*s|v~q?*8b0Gor;b?vg)v*eN#Wd;)J03C}bdy$Jg- zBojIGX{b}X!O4~ZbiKCrD+dV5>J$!0GQ?ehoiRMin15n3O=DyBnIS~fB*b; z3@4Bj#Df4wf>{MkjhYcdeH5MS^S|CQV*$}$h!v)?FC}ID-PT*+PZifqaw{E!7?;jJ zU8ZzKIRwosSEE%>Sb6X3{1#ldIeE`GHQ3?P7YS{?&yX$wU%9$l!|`?T`S!qjCD8_-OFR@wziLDB`vqA^z_URq9?}fdUc+;8}oU4x8%ytuj9o$`29LZAHWBbz~e0R*)8pLZMvaFe{gaka=;HS>AR^zxDs9non>cZoV{0pzk3U+kry|zM|)`@H0I(3 zpSypwybseJ)p-{;hikx$X*F(fk!P;&sxz~@?IhihWC5oilP-CP-_KetqIf&TBCh-s zC1H7j*QxDOLLRyGIH?E%y3J{;sp*u&M?dj(gVlUDb+bQ`Co19)kH8W5xs0I!qm8j*fFlm3w^Y}5u%+WgPyFf3PyuH!vqF@`MAnHgZ#BIN6R_?D^Z zS=jQ(cj^mT)?U@yh(={s=m!Y(rW`5BOT4K0L*DueiU&IJ^ADfq+G0J2m@-jzJ6=Lp z;PXqH%W&El*B{}eqIK6XTNco5zv>7G$i>YCSn|SQtdw43oKUcyDOvbf629KXO6dEA zuHS9>N2EtbTIzG*Mow}!uJl2N}-P@?d|-S5seM5(En=bBIr zR37Z?7>cCqyB^~T#RyHzSU_B;CKjAPHovo{7cr|;gpNdoaQ+vw9Ee zg;JuK;U##T%t-N!@=~0w=@Sl6qS&+_V=Gx;&yfXn6;{-e#1PrjIt3KkHeskc4>K5E z7QE&Jl*RP}Mq6CSF#{V^m(1eaK`amm5Rn@yNcK|^u?L|PXmEA@pw%w#W z+i9^W+5=2uiX3idUW6ZjiPqlUd!U8v*q45y-!$p;*D3`kRI|2;h0>l_VP{M&SOF5y zBi`{w8STG3Dm=;Vo{pa~zc|M{?);r z0A(~m1(?4nteqa7Kr`CSylfHc{S~_Y5vo=jc&`ijg7l2f#e2w>RTV41NwV;b{`4vR z)LUq9BXHJT%9C~;M%!{}#zUFCnG$+rh-a^~x>>pn*a3?lu+TmHk+pNS@C7nKWa2HK z8ZVoUl|=k}y8B$NMeH}o5*ow8*XKKVowR^1#s7@u1^=MQF55UB#(povmmk`G;PDnP zT9m5_sH)t0Am|l$@v1nSm)37)8VPVuEV)a6(Vj2}Zrtcez<-O$LgK3DDkQSa5L*nZ zoJ0oWB43p5LlNp-A=fubXZ;cVozm^jCN5+SBEno7NP*S6+RAwrbK?twDp;iXSYX18F zu66djH;{{Nd!;KEgR5iKcW(nxR;8Hi3nD;)aRWyLod;$&S$+SOY0oJJ4k$Vtv(N5$ zyr*^o>$DZthQEFM>lWH18A;YaZ_r!8}1rd*;IB zTaiwOjYv?W00fqIw;N1`-LR!D%+(tt6%igsxx4#(0fj>tf1(72jxAXKy?!8_!T8$E zmp%?sd*Z00X#fA#U_GC7=6+;bh|7GTE>G1^Nz>PFqPdrB$HhUg#`||zpp??TK$9O_^xw5Zvf?WYgngZu1nl$<+_;c2?t)Aj7mg1PK zDA#j`JXSOk+_|_(@V}J-&&a;LCqT9CUQaLI_k-~o9KthI_!l6p9964%Hv#_S30tfZ zGc43d?-(qmXz>Wc(iNjOEvV-yL6#kUh3AFGLMR*BQ}vYV!BZ}pnkwzbpLiUOM~fIS zQ=O*hyJe%Q=c=1teM2u!4Olj33y5iC;<4JUx?k(lM?=hER$N6tR_NCDA!A71YdM5r zK=QM8&Wefq@xdh5e**B?RHh)K>ent|pnAct&becKKw#~>XS&PU%LdW|9c_9p zdagW^yY+*m)w(^IC^PShTaL_t$kedFew?4@q;f%B6YWci;J5n+dTQd(om*B;d)B(V zApp~=d?(gj<51l9oP_5fG0hq+Pe1O;ivJ&u6?S`qs=Hnd?V~kp%H^DD=W<~wDp!U0 zM4eLc@@cMo^3hJcBI@l#EXXR@z!IJvF$|1+BF(|eprqU&(jwRrHp9k>7GQtxY=Y5m zk(7r0t^eV~Rj;3`5^YxS{3TbeY86fv~bSoIlI>QPr$K;WUmE7d-Ou&2B5lzG|07; zk!Qmk(Td#reQcq#hA=@imHM@A#)uS6ol-SzJ_UujQ2#ytb`AOn-OR{+hV=#g+38s8$AXh5irk>1o2j=tz;@-l!Oj$< z)ympIU61RgaO}5Fv!A_-5KR&FSi1>|R~50I+Q#QOrgHK^XThFZCohP1qEpy6Mi0>y zUfn=gIlU!0!$ThF1;@R3*$(mYy`MG}9AW*G^0COENZM|xvGULt5KD9P9+l?X`k~vj z&!tsvd;}QEWFxjObVviFLFCqMLCL4^>%r--HOjSOFx6o=kl)d<`QsAP$;rQl$ z>x`<4VBh2^=);zV%XJX#Qn>_G+FjG)73(4aEIhwTb4xoO_$t_H9%3#^BC=ukc)6d9 zmL79GxA#v0!w$BY)8tjJpSm2=l{wXckHODqn@K;p!zIC0 zscXzMK4WWajH9qyt&eMX6YedW?3NsmVn%Isd>N3Z9bC%y=AYf|Ej7fiZ0MJ*p-+>A zQH#nOr5%N#UkO3Q)bLh_uKmLFxNGzu&6hrjLLZP{wYBm}E4!kW!>;Qf>5a91wdJqU zRG$z?1{KFkPIlPuWB+pNK1&)V5bkx)WC4ZuK;$)!+wDS(&b{j4anhX@MP~QM+l>MK zKw8BM**t}R8AI4zOh!a~;{G*|U|K7m&uPqWIv!v8vrFV|Cvy6uQDqaNFdL4jjstyq zL>n)577d~tRb{exG2c0E&nacy^zAcr-(DCq*$|k4_r39UVs0OBQ@$^Rxb}q8VgWni zK|1l}e2T{755@oCJfdMxe3Iu(cr3luP6;1M_!Q$+0oc)YTpjNJs`Y4DI5t6V6=~7n z2PY9&mS;GZ9CS6xOLJS`YJiMwX|vjO@*ptv{E8KzW z3Eh~-ezS{Uj)r=|Eymrd!@r0;3RLBPA#By^c`NXPV6N!ZTDp)44PlajP z9G&S&(u)0Un7F{EZ}&b9emFW z)@IT5)yu7P!=&WdwB2c&fHBpT=OZTnCsxayi6y7}Tdhs$^l;VuBsE!gZfk3HJTm_f z9!?hPPsKK#kkLkef_@TlW*3Fl1mkHj!X`yow$x9no9A?%Y?o}I(=982@Hp76ouQ!1 zv;q5WYSM(?(M%Df4zX|@8oHS`1d43c4&%FG_Sg*jU7pp}5SjYVI$1ZY;8W60`YH~{ z?*sWHxMQihteWbdSHnQ7VXaJLN5E&ggA5ND*kN(EKu=zOS)hNWmS=iAUZt ze4yQm;P1>2X#MxDiEp(&rj1XU0?Kns+;8(9)q#vKLFDxlK%W_*fK@Amd$OSP+ML8 zP2uhSWOuK0Tg>@oBdC$Ls@$dY=#61RrXcUG0{vgNn`LbX)5T2mWi{YPT7O`!-G12m zv&wdzwbih*t3U3>kp;NOuIZv-dQIfv$M;U0Ez_e{@h4vaqDuCyHnNX33)@NX)1nH|Eetz5i<`S#Y1CQD9CN!?*v z3}gZrqw+K^Ncu!*u`A~!dYQMc4i0V&wsWcDncomH2*vTxSo5d%CKg3r_TcT60_VKy zjU6kk1HL5VTjJUJUEr`;t$g*nsWzaJGEGZ}ANZnEsi~(uqc?FT7Ihiuf7UlIUf6#O zob+%P!I==DMN)4;>7X{%_^1_&0LjYv6+MlL<*;O@zs2dC%A@jJw+k~tJcvL{M}aa^ zr()Wyz3fQAZS+v~rWIbIRD}a;IB&ka#t8cuqp+PuG3STa*!b`;hWgzc2^YCeCHHh( zYQ-V4)GW={S8X$G?e@!ijH3VcVR&)L8k8uH;YVUA8W@r2zd+pEd=v;Mink*gIjFWWncwCU4ok45$0H?B(}y&V z9EFBRl`59<&b-K>t`VGMX(-d!c8{;9eOw?&knLHz#mkV~w{f5uzA`48d3$u7fhwx7 z&!|DOp^u4o>F!{YMXU{eR9YdGTt{*a^ojL%okUkOH z4;>)8EN#-2)2z+CzLjcGgLYj=rPh6HcZX=K)9}7THhL^?iZ{Tw;$$m?aGu-heH6p9 zC+~%DsQ0k((jI2^YY0lOz*j=`vG$+hA3nZHEQhd`WP=;7;-5I))v8q}1-$44(L~rt2W2gVglc`!cNeH@7=3OT;1lP;SwJs@O@rOhmio9$>rw{<_ zzJCQ6XDzZPnf%0MmxmD=5yVhuEwt}VPCX%R`T`D8FsG(!v08j)jd1>!`~yG9dRJUz z_X!8QShCz9|Da5PA+UtKE4d41SW;}gh_6iarup3mmS|z^U(p|_bQE#^*9Y}qh#D%i z7{ZURDB21%SkY;#nSoz$#;DgNAG(Xiev{WC@Rr1JS7BPvR^)2@3dw*8qPT~`+8v6E zu2T>TTVfAcb!Dr@`$%opQCF?)@6m@?f|GLs&bS*BtqE(~>yaEEyjH~8h+`m`FeuC( zb$@N=do`9ogXWu8{@Mr5#77@{~{mWZ2eHIYCnqPk|*gmuN(>izaj2M|DX zrvZJF42>|yCf(B+FL^>1>!Oicvt%VJ4#{DrT;5#WQi{^PEL9n1yp1O1o_l%&er40S z*HMhW4jD);W&JMzYU|S=hmBB^^`J>4?Yr3Eni3?xs5^}|T9UTtt zVU4WvF-7Tb0%({H*Ps0p%zN@y(~91a74oG{-)7r^Pj-GeCq4`Lw&$+br3R^$;M-ZV z@or;ofbzChipDZJc?Uoj7iN{Yo%I9A!4kEvd~SD4QlLxQ8)!Egr- z7ZDjb9~11=FDZLWx3^xjl~W&I{LWRM@g}qBYFpwu_2D-^klWUzsTT`xL8A8hP5Svi z1^PK%EZNmGFQ3cic_uy`Bz?3eMTfOV(Zb@2u|B3{)^8`A_X_o~9sQ^+3c_f!N(~wucR`##Il57$7)>$R|Xl>eZs91f0>)W$0b#`EeK561=!&LZx zK-3f;8s&To?sO_B`jEb~8vcQU$e%qfKmL*IN2H;Sc)$FF^p~_Mry$LE2fb;PorvK_ ztlDaO3=>M8JVUOIj~kW_4ORarN^ujg5%q<$@u&9D;Sy|P^v5L2b30k;3y6mi3Cme} zZ4D@Bk*`Jf>&&L825!gvXvzzfw(~ybbMFKcbzmM0OtOdq_4$8^_}$xhEgjfu`j+7t zPT5bayvPE1QIi6{InnXkDtZYf^ra?A%jbF}b|&*3U}?1XkXl{Wz^^;0RGWE@Ib@Vk z`Ou!C1tXU~R24h?SbDCz3v~czyoJ}!VQM2&TltCg;av?bJ=&yb2uh*?WeQFwma-6) z3%hJyb8-#kfy=eAzSs0Sivu~VISkTfFerC#5DUf^f3tKUGavpm^U)DboK2s=9LW@~ z>8BmpWwj9rWfA*SivEPkDGkhKs*3mtN^y=>8BLifW`4R?>8YR5`fuRL2`ir$hy|iu znD-17s{ikEI&TzT)#oOWZrH!*TD)>nLO9zE^`!)w652bmrM+C;Cn`Qen!!Brt7ihZ zt*Cc&(Ymk#YVg78^SvxW2)XtRfakC$EBS%=pybEkH$^C18Ed(Xf0Na~^{4lgn11Qg zI)|+em;Pc?ghb@)qmmQPm|@e=WGUL0217_e?v{POidS0{vDHY|+737|95jU^r~%bpvfK_bj?ilU1qC{6g~;8@E2aEM;C0f&U%*^{a=hJrpit zIUtWrVj1GMrK(g=WhOpvyY|OmRfvNuD~p4ISkx-6;bO0MV`%_nBe=}iYaxmXhO?ry zoYBg=My6|riC>+>NLfI<9Gld})czlltA_jf{=R%1w#v&ECm5Q2Kl&;sL*eo+fi47f^ z#RMJX_zl|6I3@SFF35k?bu~f0FSDQooA-~qfBUyJVh~K0!`|1^H?#>R9o3yJelK8s zKi#C2{md=;QtaO)AiVXNv_!<~HqDpoII_+BaH28*W4sh!ptjiVe1DtWFS%G2FyFgb zupR--rDshP3!5D1YrngRa`~ENxX^$v=mQ1fo3BO5C<&F2;>%=b9>^+GEsM1ci3GoR z%uKs0*|UeJcqP`t*_>46yevw)ZUzfiCSYk8b;vZ|Z75YrQWkwIuMB7k3Hss!xwx!m zD6-*Cx}yQ`f8(UqHBi~D5D=(pR~1`U`^}{kjpuH-jbp_|x`=i&|APWL`Sjk+rx0iP z2|DgI-s`ETyi^%}5~20$>Cpb13U6-S{28VA^i#$B>i!R9>Y)ENdjA78_V#` zg@j8OO&Stc)k>H!{f3fJwaMly#rtBh9|(`@K!iCzV z^0tL*XJ53NdK`6ht$zUToH*SC#( z=uZ{RaLu)=?tvd8dV|}4s&Ox6oJVwC9aw;8=lr9%96C3EdyD_J7+S39J!U#Gl)JQs zDk1M4im{H=%HnYJbKCXTsF)I=Caj6yTa;Yz;q_7?l@I+Sc3qI)Wer_2If7mFLEfQK zWP_fg8p`@f2Kt-}%gavJ~~=)O|;laT-FTX&lF4P>gPZZ7i;D7-TypwRf6c4EH`Hi+hxUzTCY`2aQ7)+XT^ zvu1}BDeQH*hFk&<9knh_s~>6JpNl;jz9anm_S}a3heIcZ3*7>cD0vyghL)Jf znEnlTY%JWIa2;Ho*2#sYo`HVZh=Jl=t?~9V>=wZyW#2t_AhCc?Mh_1g?nc#!hddg- z(lJYqEv6ec{ope`zrAO&@0Ce=T!|&q;A+=~FAR1vA?wMxH=HU8qupXfB6=)kY}k+{ zCJ71cHi${>x%%3|+?+joNKT>fUbx+(cR`(^-9Wr>+Y@@RSb-g=sb zEc5cl)KS5AE9zfmYGdPhS<-@0?YKxri%!faQqNaiC;g?%@g&*M4w=GzG=P3cmf?z- zu@`I=zfM<(S$+b@5Q?RijVqM+4JryS?C<*8`4m<93do5V&v}?p`d~(Qtr4xvAq}gu zAPKx@_AQ514ve4mLL((w9g~^i>hs^V9Sy6d+y*5Nc?WzY71nt+qVA|!xCZaVwAysl z9gIO)Yq=w^t|tQpLwneby=sz5rnlu@F_Lm$a!74)u3cuYgm9n{XZS(LU78%@Tm57j zOTuu81QRavf?~P=&I9T2wtf=|dZ*~i)v+m?_|1F678LImHu1q@hcH7K_S0l$b2-$z zHd87d4+mKvtQwjWv4DN72GXs08vL?mdt3k-1M}+PK75}AyF!Gg5{ejP1xvUx9GN0g zSnSMTb)nhUtkZ1)Ck#eb{0tPRL&oIJ-AYt|a>DUBp5`Ci7Rw>4o(h=qy4=%U-X$qH zH4|vxFv~xmVc4VL=PiRCJty-G3*AU%Dm%QtW&(H-OwfoTlUcMvk+0IgX@mN31>E97 zg;LfVZvh+Jpn_sO$q^WWOm!#G_fA0%e`=TQ)c1zLGi)cgNHlNVYJ5(B7tr6RLQ`pH z-1f$weYHwf_TNg-#psArh0zJP%Ap@9g#|wJvEtY7v?84wtgW9#Lb8mCE>3M+P7B`> zVDMZC=wGx43;<13dL(aoz%928}F5|edmMqZH_y#M&`vZ?D# zn)&VS$8U`xRh$XOe(=c^UY+Uc<90?H#C`W~r8HUXswdKEf2H7L`A>pJcnTk3mY4$> z2R9J!I%qWc8L=q|3YW?Tx=qG{`3ha5;_tZs;5?SPZfF4|nFTC}D7xJ^A0mSoIBlx7 z7jJzTQQZGI_wbZs9fsv zD}MP$7vF+vLI}Gxga&t=-vCTzT~lwpRc;sisI#^_Z;IJ38nu`H!x{9?h?muexsV>{ z`ZnJ5d8m?HE=#|zwPqSrOQvcxVlJR(qriVuMQeswFNbCA)y=;i8y7esWndg;;9?EM zak!%GxzlXw;}?(<>=jnDD)!6$l;WVy-qM|HtO^okdUO$pk1srHAH)cyH94OBRX_hC zk;b;D(fAkrW=I98=ql;=t#+4_KqtC2!K6Eay97yH-Ew0k?U9C{Avm^)KqZz6TP*Y- zu+a4>FQW5#cEn8azryp01JAY)eAvQ`BOyz3wK$RhO-XTWTiA@ai9J~t9u9rq$RmH_ z$q|&culLvP1F8z!BDGTsk&5X9d6iakVQl)!2wV+ksPcm>prT+6R1CJD37MhzsD6!S zs6WYRmGp;iRt;z=<&zj}T?ETn1w}NyTM(hH{I7-6>rZ8aA?eI@=UOWxJ!E1SX06A zziR1askwG3@6ELX%HcOGKd={0%S!@_h%QH89Nov+pM@us=}I}${1)E|bL4_1J6<2Z zUiDJGWJTz~X~R2Zd<7P;$K6G1;Eqo2?Q?8A$B|HeasBBIp`zp{!j=}zn4Ufd##aL@ljNbjRU^`6_;6D`|3!?X9J&7W8Yf0IW`u9AyO~bbP@S$D)cDFO{6PJdBJfQOOa8rV(3n40cURMqkIxMY z9}-i8YroDl>z8RYTc|#n!2cD~kV43>^&`OQ@Ag z<;GaHw=>r(OQ+w@9sVui;_r>-u8bt}Df$(SgI%`#Z#{T(kqdoiBxp^<$za%4CC#My zv<<6V!}h0c$!=>oY+HskTl`=v12^stv%{v#fM76@3y|7j8nx0p)ois{O)=T(TQK>z zvB@SW_-olMlTRfe9MnMjEr`uQ1QnojZx{+OcV7R1VMbCAJ0z|K{>=t@43es_ZFS~W zQEnx4Z{VV>XE0-18>CBzu?6JjB2Fz4>r%>p3j-K-a~O{({k@8`-{C8n_k1{&8fq>v z=I|l-3Yy>@IPJ25Mq`_hVSYeb6!u(E@JlhZqM(EF6OZBF;$3|JQy;uH8Um|Bw;O9v z)i7o&*zV7X=ME#4nQ$#X$sI0v-#yt_SHGP9PEMGRl5Ic z|NJnerQE<30RhAQ!=XRuy-=915GxmP<*IE~{lNORKC>Z!P@{8>?dEa*KxO>!2ffvj zg;(^h%FJzF>UNt2w2|EV0R8`PWM`_$1}DrT<7)imcvFzd{Qy#5k&!B|aqm9$NZj#( z-ct5Oo0ly+ubbqE+~W649YlF4XC=6}WcBMe7!v8@w^+GqF01o6AIkXJ6}`lB5J$b4 zH~q{CLc~iZuMNtF>Fx4x0B)vO-g=iUD2n2B>q69Pm+-p}5i!gHfJO_vOFPCLgB$v? zgx8&Pjc#-OIx(x4XOzAvN{FgFF-%>F$-%s>#0ckXJ`T%mH5rnt--R6F-Ka{sFz^^cPuiYE`fmWvds4; zXuzMEP_=24%?%2VnrW@+5a zJ)jiC62%L%-JbE{r_?|eM^dFlD9rFtobIf`cb}}LNNoCxVX%=)Ud}v(5o!X-3I5MWGO@oU1lE(;j1Vpe9 z*QHgxp2Qtq#{4PM>dzqhsCVeml%OPb4STLEOLW_H{Q@OZhN6U>Y{WpojI7M4(apo< zEDa$fFNI}n=#7eAPDfq*++i1WrzDDfvnmC<>85pL03=rD-d`{M|QsMVI-)N?@TyM;=Tb1$Tl;Okz zxV~us6i+_pfT-*7@KsiY1aE<9m3LY!vI?5$rA|c$K(L|XuHPu1{(ruGgA7GC@esGU z202|#WUsc)vp{kMSp#ZenrYr$ujxLu-@DEol@OzcQZeI`WcB}W!dQt7cUI`n8*<*e z27Mv8;Z{b(lIMD?%m7)@;)sZzKzupgzGY8o>lVh{Z1nWszH*hlbmxEt!@z*o&yJ{o z4puleM1WGU5eG`Y8NbY&QPV+EYAwmjy$-)QUb&?+n2=j%TM&nTQ@queG=6J+c~|4s z(B<|)Y`r@0#RA0^?+J+kY@%){#oSdy9#ZsH#Q5G z)u36B)5eqiM*kQTs29VAwCZIr{@qafIav8-ZF0>Fpmx3?W*9gP9L#ZK0W%h#iLII4 z(cJgB@{hOhwu^idk=$t+-oDyxZ`;CFhr+7EJfI&Brqs7wsLg(_m@}$_Mc(vpy0D)D zEKnz4SzA_mpd@Ay!yloJh-*P`g~1bvI}GN~T6(Ru-iayEr))koN{th%r6E`G`e+ zDpNQcZsLJk3$HIrV%?%}g>)a^VgEl%qRYLgsgf5jI_lhA%6aHrE0aep*)~}UsPo)> z*s}h5>wep`$xb-84^;M~CE-f{Y-lD9z`2{(?0dI>RHEde^@v-2?2F6XhSAT?d;2?^ za<;*Pk?)IMFhkB?g>J5egVhCzdrFkKtSS_V3ulI@XLZro|5>y%#Bpsk$Ifsoxmnw) zPp=lko6nZ(whFLNOy26l#xn+c9~}}IjR@xh&X$aw_vIV*XO7T1ur7oeK?LDNDQ|8N zz9?~`Rc-u}mQgU8RFZ^E_iDFvF`V|i&2%ny0{y)`UT#b({IQG`6^`>H|JPN_|E-_> zBu*3%ia;O)d%4Oyi*cxuH+=uEn%GJFruK8ck2lFE<)RdOBJVKNhBa0?Vn92*aja4} zbdZ6nSY_dalksKrmWZLB#R@X_`&zSib>-1!-ioNoEYw;sdLGJ(=AIfWIQlbGKIq`O0*lY@+|W7$797J+w;KO(FOYLulT!ocYwimV+)h(h0_JA16Hxj z=#A6ayP3_Km`XGho0MT;YfRN<#^gO)8S>^9MeT{rt-j!XWa>Z~Dt0@5FNj$pMo4sH zcd#egy_sjRm1x1zNo$)*l=H@9Q!?}3qY-vHylg8eJb#bFMy$PiGQ73IJ?p>=OuEK? zaxY$BISSv{86Gk@F4dJ2(*~zVmdtp4zG`T@0amveIbpthHrU5fD(7pisG^il6Cd|+ zoK6IMX@|pZPSg=ITXMgKY~Um>j9jY>#nbkT?3muE*0DK|cxeg2SY?RR;cwlP*) zj%d10e8~6ecsme8`A=5vC;Al)4eWwE@o-VR#qx|=;JP{6Y=$( z*Cf(?CP^xd4RznNp)Y;;=%cLlW;lY)Ci?|fcui3eTdI(!2;9CIUwHqYmxFWg81csT z^KPbBxcA3zpdA(+_hQfdvdad|Gv&n@E*%a8u@dc9jdK392XR9kZ(F~;w(~RMp2Qn&s&e9oD za=BJHVt)qzhAT6)&(eVXq1oe#<+?c~)&WpZC1$&Ya0h$wp6 z=Tq@ubF5E-{>Q)n0VreN$%~qUb$#;}8Q;<-Js5KdsPGL?N*f=z8gU;aSd3L5cVv6E zIgBkwVyU-c5PlyX5iB)Y(enHBZ(d}lB+};O7Ow8(W7aq;D~6EKY)D?+ii%w*lIQV!Z?WZp{A40x3&_^ zzr`ps8J77?xG2QPetw1DX^NyQ=EYa)_ih(Ffcds+3KagRgADIDKnF~1r!5rbT;D=t z>a$6i@kV)6Cc)%Zw#P7~qhsU9An0Fj@|f$?dv}Xz)2^dh)^C^h>chL?^8@fqL|EDv zc7c9(iSqr{*8yooMF46aDt9tDygz#rb&o6wdS~ccL^nW-+wMC{IYlG|hg~@LU zaQ1d>nB_pym+W-M35jX;Wfv)b0_h8iQGt+enRf4jR3DZ{&P#AUl1g8_{GF%n6ts5Y zT*)@aw;MayYum>g|Bc#NX8ttnmuks!xjoT`M7rlE$GpF4r=5FyTgT5$j}ya(FMj8=Vjx7&4@28j__t&)Q323C&aq5KUZO|wYY*~rpS9&Sy4lv&@R7wbJF_rz-^;>91Eh)CCp z*=!wD8&U@IGzJ*}J85q+@D}eBs(GW}^mJ@I#22WJGgOT@9l*W~0PYb6f0iV@0Hq^VS#g{o?y(lGL>IkVuO$U5TAjJa5adFRYkc0WIEOi1>d(Jnp$7WGy~U$r`ggeYO)~YSHwuFfVQL zMtf5;uwY@;@PfF=K7)b(ugBnynUJ-YBwHx9T;x#V7zik z%t)ZXG}I+afPQfJ&9Ck>Fabk424(8bJpUo^LeQM#`2p|~s9L`4X(s>Dne5NgQ%6F`VGJk~^H^)KvVNbp@q?ePGYd4P3w%Oz$; z@)}*`QtIK2pguziUS%s}+m>})=QIW2#4E`#)<+|6qO<7NAJ3n6r2IoxJ6PT6qZ9A@ zy6BC55G5qAs?3U4pi0M-7S~+-^K^xrB2okBt`93#8lkm@|QUJWfxscn5jI{Af|_3ay|oh<{dj- zfKSxD<6dp_E8F>kd5(t3_IdRgSjfx!an@dNglJ$`R2;T+&HG-g(Jkiyu;F!cRm_tV zm3fnsba|Z2!DGCn6KJ$)m=2MDC~u1Za$^mX4}kn(DtxCLX#i{v1jxfv@boq&MTl1EaFEh zT_2aug{I^;+qy{bNvFDS=*5NKRSeio^ zv5gc8RxVMj0;9LL+1`vo@dcS+W6T*F}`Chv6+q)kw zPVmV$=fRni@4R0Yu)gT)B}&G2Te8$Qb0iY3#Ph}@1+i1rI@x;bP)vP{)cXs{wci-I zy3C6xsN>mVO+T^5FuO|Mbp88K^_ik1vwJ4X43Yg|FU$0bn+ky7F8ju!;g}DD4O$#< zLgG}Y{G*c=_4q{K?B#n6liZ)u@913hg(u;My!o_w^dMXHp5F&1O#k5!L)f`Syvh#0 zid`ae%c-nRKY#w5dv%%7+ZEQyijvvP!47c@_4finuRGp|MDhd0ZR2M~8>Z zj@|Ur?h{;UgB$iO?zD9w&O+Obw*vO;oEshtb<$3$N8p@^=`4NByr-kQWgk+TB>e(+ zc#k2)5+P8#C4R{Fvmn;qlWF-Tg0Z1Ssd4KeMD=y~4>zt@H1Q)ip7GzsD>>d{B3{Ar zw|@_>=V)$K4zR7a5YDC6={GJU3$9_n}-U!IZg0`NvZ!n|o z2i8UC369U#A95*=^%WintE;%njOs=)J|5NmE9`MDK~rqTAMXrnz0;ogzVkfK=ldy$zyQhv{X<>|ID1NO>Ry`NZ~dsVr_0H2KBnQf zHYAbL8YWr}Vho>gr^E&wU~qzGkO}``6EZ04sAR@7 zMXkhv2t!_xCR1jLk?f!W%290aMl)G}4J_1$JuG%1_72gplT|*-#@^AsP>;B!#qi3vmy)AC^vmYyXk*L?qmHo{=QZs~^}fG=`3tBpne^x?>WdQ^%YqANPfd%2$r zT$%l}cIQl;y>2qneR4%&raUM?;RRohoQQhO65oT@$}9yq!QsOzOEq_81dny0NuibU z91ePRgC~xQhd)s|=VceyNPN*#))HI0M3qYWc9#>U5H9O^H(-O~>wjo>pwkS4nO%p% zBEXep7(~Py#hS{IW#o;G{`TPd);CB*^3_uh*R*r;GGAK6h>hj zY_@zUYUveH&T;)6K1{8roVx0H8)=^5C}GR58*vy*tGs876(ad$9)&ncLTectqk*oC z{B1yg20DF-zQ#HxyILA|CUb8Xw^RwgMN!S6QEU?^Is$TzXi7s*%_p&2x+Sh`1d6wk zYD=x!{sFyhs7ct>efK^3k5rDY+R&!!0Q1^ZBmJ{s`-{GErP}m@U)8P?7vvFp+1{Mn zg!GR>xkY;u#QLM_32ShCb4LKhZ?;aM$VFKTFKn2j=+l)(&^xITaRjoB$3~}%g zrLD5Ii7DLSKKZpV<42OoTNj`&x9=P?m-X?3uMvech^>q}sX#-5%?QOK_Mwbv_&Qb{ z6Os@eocm;>KHaL(&-0AyqQCh%$#3CInBC3*5E$NsD9BvGxj$tWSlj$HlmO@afeMyo zJQ7ODnZ#Jz3zl$(F}FtWdzn)+0}N)6DV1|#d#@_e0gX*9#z)>p%nrHP=^lS>{wdsn z2sB7}_j(X|Lp9gtKBJ~{_Q{*MSAsz)sTSwUC0pEj0hP$-y6qhD=W+gbNNRPg?@nEb zz9yPAwo?MlR-bAxeG?~ogwd$8ag%WT+EFTAb*1rJIAK-}$30;m1G{Ns(~m+7?G#5>zg=#)%&a;!1<5&rbP8OE z_QXdcCIC($9AWUYy#P>2v*Na^>xu)}qJyxa$rm)Wo_F16YWqQK<^Chcissg7-*y7^ z!nargfBUmOeRtX$y(=#@(~}>w@m52;&1bNkXbRX{W9cJrVskNK-q&7dYk{fA!@&V1 z?o<0&P3Ns+bck4rdLWwzm-ed()2rC2wnAH>W2AGBnpn1mkoEaseKSftIPWsY*Q%X+UOCr;RrXu@ z{fhdXou;47Mduuz1hk{CcttKzb|C`AxSCm^;3*=yJ%1j|==E6Wc6?t!Thv-jEYEmT z3{eh%XTyyO*9M1u;7d>GlKOy=*V8lFcO5@VbfbkAOz-N1NE5#(FaMr zS-AKR1-GM<(xo1h3yLGVOClsR>=lI|o!1RYQx*lG)aPOsbFa!XMY(jDhKt|-QhY+; za`kS+cAg@y37dFdNpz8+N|~7;U5goC;AGoUrkqZ~a}W#i^LW~@!lloOu^r@$QLc5 zZwIuhH*n{ckXROsb&<7irrG|y{zIEJB`pOb)2l;7!0gbmKl6@E&_IJmDAZ-<&kKj- z=~ppXxqa_^^MBvL4)d+Cl_q~c+j*5^nplW_-vCpN_lLdLSx22$O=bklV!!k(pHZFU zb%OEg0aS<*p%3`ocGk5Hzt>ND8GPPy%VdOS-TlF_;0&L4Byz`uIOlhkqSOf-?Lsmq`5hfOabBNr2XW_N{|sN7|TX zdJ~W1Gae0>>1RNq+U*Nqp;lp;mDt505Hddyo-1q2(@yx{xNHQdZ{VJ&1|N}QgF{+? zjsHEiO*k|Po8aXEyKOft;$0a_Ptll)KKoTHb?;c4a<(b-YaQoRjD?c0AA*NiekMQ? zB@USjm^{E|ygL>ICszX@pAI)Y+DmW97wr!$9g}W(c1%$l&7oxLBRdSNDU#bZI&|UN zY@v=Lx&A-ZH8wiyOPJF&VV}WZ6c@ms5rkLl-dwM)lbF$#}XpUpOF}Cd4!<1X7#TIU8ZQP)|Xl<6_Y88Zju77#fK>RC7T^#5@V6- z0)G$LKcHs`ut!+vOc=6d9Q89c^>b7mIC}3{os{F!R_4rz0C&QyMS?URA#`@Fdp_4y+?*y;_eOLQ;s zIGp|QfI*6Mfk+DHiZ3|J@<$A>_1E~(|7cqQ;O8J{C%wv_G!IpBdA}9TvhyC6HQ=@| z3nu5i?7VKL5+O~#bln^PZ0678^(A8|iRFg)ivpU=x7@qE^`W%ih6|m0c4zCEn>hT3 zGjCV!N$lPMNMa%#xv$Us)6D>yofQjs35ihGs}tSGx)X7%)N5yc_UdV~gg<4{$5IcL zl)ekXk)*^pP@p?s+8!S4Uf$R&K|@~@AiV}&l3^GR4jI9^k$=jyd4=-&SkbMqv9ovg zxDpp*qO+1))8-Jv)rGD@`X>-ii4EABvPf6O{JA^3j25cpjKg;?gOB84cd#QB>W4ml z5l4v^<}1C(i89@$N-xwiLS*MuVfE!__gmzfa5g}&)pyw+;%Bf;Ru43Byo1J6FT?o3p`Db36BlXW7^Lcxoev?E5?Ps2seycrY@DaD`;)+p9 zW_eSSU-3*=wPA5Z2RenKhIgqRe$BrW@$k#P9sDg9kNiiEnn0riAtc7!>KG^&f<@GN zyzD(r5jJiFd#;XUM^qc6!WE2TU2BMAgy}Xk8A(Di;MS~cn)%AahDYS83G&61jQJ*J z?#qq>>JNUm<${mcOW+fZY-TLJty)w0G?Niu9PYD0Cq0{t(El8jIKuuNG)pwZ!_;H- z(3k*v$COU+O-HroiNwic5{UN*rBW5n`TZ=@YQ{)cP)srV0TMj(~v8rTu!N1nhKh+6`_1opw`!iN||Jy z=>Qo;nM1JY{=+bOw)ue;^lh&cuNUvCq^i7HxK|PJq6b{nL#jR`DbeYPUirQcj+YO4i^)kG^u@M8#l|WDC6X|XHwy*DH@F&+Sn(x$f3{dVoaQfR-ciqguE0$7#S|pZr*g_GF&q9%FgW620rP+H76+th5vRZ= z=fq$!QnWTVUB0+7&wu#eh`>Fao>wN70Q39zzN@=wTurTiCo60e zhV4G86>XxiskNqPUdl$daVLI>??o%4PI=>1dA=O}k6wM86C4Qk=Y7Tit6nX;|7{-@ zPPZJ*mhjRx8}^bs*7l$gx;Q)hLfy$gm{&h(KLt9MRmMg9@Oo+uhbt=<6O%Bw<2!Tf z>eNlE6Mt=7btX8$*U%=brPfJ5Lv}|LGpHMD_&IIsbo7iA-^MI?t=_n~`;eX5sRT5S-;2jA`O83O2M)V6l*U z-^wSZ6*GBQdg@|t>m^es8miRoZa|G$ZSXg>qLeetq_kak5a|3}V8kzqn;;lBWzUtg_3_ZlQOuwh_J{(Xv;4SLvBa)1lUWi{PK#&3Yg7YyIh>z9w^Xmb;<2sj{mfs57mF_hgD> zg+4}YFM^{Jv@ASG(4a5^`anN_^Pj3iIe&nt&Sw}e+>@GB%*tWj zROx8`cHPf2@8=74OtK#1ijsZJw>U`mub^7*lUNB~xcr}sC1l}9z54L%+Y|GF^NJlI z_UB02Yj2o&oIqz}1I)WyF915#w0P&uJ|X}6UUKcCzYsl^_Ny{1>(@emoE(>x6;JoX zKjY=F;&dFY7Hp;Z@=#4YWMX^>zT-3&EA#;kc>lE5kjw}M1WRH&A;f?{V@pkKEP1Ju zZ3&d+qUDZb?k=N0{|Yk&HlY_iM=oA#3n`KACC}P*>J2tZ99X9=I?S&qir!?GV`83; zkB>?_w~3Lupo{Tl-qk^>Mw|}&`qr4I%`Q;EX(HH-mG6d8249uu_v(Th2+hT>j7M1( zog>~uDg0ZZWJJIfP=V0=-k+Pe)YUvKdY)yVs{H&rx{e_Bv))nn#%5WbgiP8x<+~nU(Oa<)pT4kZKm>k=pd|NOX1m5(5A(%0#B6VUV zIP=<7p8~Frrdr~>1C#z#=iizbP|wOMYX+friT(#hU%VFVhzn@SaUQ-+zxP+_&9>Lr zIt>pX_54$Yp8;H0ZXQX|onUQpbI2`K{i#4zm1uULoCCS!gtnJEwc-2rPFI?IA2H>t z=X^-XtKIILTL$a$ltZes&9mSP3p;;tfYuh4@~El)+r~4N-?$m4e?aTc)!oG3M693$ z37W05GGnC`b2o@+6|7^)YkCCJXOF}aYc=v)?}8GIwMBsSLEyg}bRWgyT5kG;n1BiG zN8&_%!p^vP8PS7HUK>kYWK;Ovr~q^MJ6KBohTV*HfwRVp(o!Cd5~$A%!a(3siU$7r zJmFg$TF&6+4->^~bo~%ON`ai|b{;eNthF~YUfnTk96Xeh^#rwMrSX_NyYcQ0_MjR7 zXi|@`}<2{Q}u{_<{}XD^5sc#vMyEj zrYZ=b=Pyd=3I)C~h0i*K8wHNJde6lyYfx4bH*(IX2uLRXv1cY@LJ(PewhB}f8@HYE zDp7Q!3XOv_cgF6N!V51Lh(d41vk1Del~RQ8$ftNL`T32$gd2hj>y2Sc$ue6;!^KTK z4G(|4;faEBqh1mZnMeHZOjd_9u((P6UTjoQF?qq^)HO>VT+7bV=tH#)r9-g-W=M_q ztt+}sb~S6|b|vjr_Z@a}|DiTF}X{MBLj>bt1Cq_hc;$f zo?MxZ66eEx4W_3)BlP!(sFw;6pneUa2aE%KVWn9nCp2!J%#f-aw;o`-aTqE!tp@$&gMOldWf*{pxIsiG+wvZ{|3pFh;u?>>VBbM_(c2^ZTK7RofWy_YH-`t`Oqz# z3E+!YJzo~mbbUQr6<_+n@mrVL2DkPfiIVfRxRp<5iH6T;`OPfR)b2b6em3jy$* z$w(}6@FVJTn_a6YaD0c{)$YSqCurr-m^Up>5>GX~v>T`pbsIKm!Z-YeH7ul9nAXOl zp;URz+?1d8B}XAY)u3xkhNNWf1D0<`vi6EBg2l@Zn@lH3khPeko()~`>AuhRqi^@I z(99&UKRzaB48bQZt{g{7;56G!xW3y{V*J^suaxJ`fUcT~IB;TSqcMF~mnhjq)DAoP zQ`(dys3$_`?T=G{GsT}oA#J8pV=XIp5*>1j~gw)Xis*c$vA$3-_uhyg=P;Ii;?u=j@G;wdnd_iXF7&0n+*K`pZ%5{CBddLA=3)bQULH@d z>JpBgB057f?RIaeEP)CoK9Rzvj!L*?xoW!~Fb3*%Yj*iJWizg5*{eV!_n>RM5nh)w zj|0#r?8!-v&c5PC5W5wX6UB>r6(0Y9vQN`@3%wM}kF3MU{gba-W0ulq$Ya5z=ESCslaMe_^x|R7-$Ly-s9T4NJZL zvn{o&|4B~^Ol3SqhaPleR^&;c27`{J2IsHkdG{RgJM10x7RlYKjWYFMTDeirH#`{_>N$aFxi1m*X#Ix;`6gS;s6O2mm+H*pWy=Xm4nNkey?m4Mr zJqOj)dV{pOnK%847Lilr3L}yO(<2M+Nu8mU2d>tMpSw$6-@IBVyy1r$#x zb5Sn{*-O=d5b`r!qSaz-5DIBmWHUAM6w`53#do6jGrLdr)wL3^$b>Wwz!ms%`iY{{@9J$3x-z7$I zd~s4TuvHP$ustIArf)`@7`7G_aZGEh^%lLlkCUcjD}ZFwQeSLDMXff)oV_S| zSN+&TT2?SV?2s`-se7yv;rMJk_v#{NUvt?;W=ftS%SV~1-@%lC4fLz$GTobxGRIaf zz}UQ7w`&sDWGT6<*kN~G*Z+oT+yX)Vw1WyNtxM4PU+73r<^H}IL9HJ`Fs!hBG)#K9mJ1(b18cR z^0>gSx|%4@V=gYCM2Vj%KLDP%Pw>4P&9@-Ja-ERzW6fs|I_}}Qx^THkO&X6|DQJDp zUxc$`Y>DPadqndh(nEqr?AbG~%jci*Y|UB+|p$V7Kg4+>Gr_;z`J?mGJ@<=}?((XA^Dbu`tVN#ANjX zF$|YoPjJY2Kb1T4{G)L#KaKYV#fDk}byJFuE# zhstioVyhPmLR`UrXkw~7hG%{~-v05tc*idZ4*f{x@};2>SZG<+)@Sk?TBo=rzFeTA zEZ_;`UqjGdU|*pO#@+qsTauk^qz&F?YIkBIlS*5~T3#I7A$Sq$$ zeXx0KB3v6v>dvZ(dyu0#kwbmfus<5?2QTK)S9)%v_PGe3Tq!&yh=S)Ma!}Gk4iICn zRNb+RmyfgrM^a-?2Xai$?UKM(+{@l$#b!x*SXi|>V#iHhqJin=`D59*vmcmBpQ_9u zo1xs?+&aUlGBWd5g?yz&C5ATND^hN&8y0YKD^Y<+b}{aoj<#jg`2#fI8%n*50PR~aFwNg9TW_P0lzY;!v-Ga^Ax=eL1 z1)bg9uU9w=ndqnA51Qk`CT;?0Ma#XzC6w zZW3d21KO@F$wT;FSox!B9IkfHQDmQ}+idn0o*_b>shem}wJ^AXzchM@RCgTt6*`L?M-xkVJzD$-lu*lz90-p7@_Ag4- z(P+`ksI>p)fN}bHxh8@sdiD7%t#%6}ZOhH@?bBxHos4#8+_YqD&h+%8O?)?VHlxNb zZ!VwZe?Xc1{=*KHKZBj|6-T%ah7LDdnnk@ zq|k)(Wii)MhNL@D78Ls5AFtTGVpGlf?MJ?$%@hjcouaGpehK*)zDwHbY2qoQ?Rpmn?eZjnB{T6OL5D%k+p8*T88aluAfZVZrB~N{{xxw) zw+%tL^{6Uj;XMhZVk`))Dso!Q32X8gR=yRxx~2Rmy8HwXxr- z`6qqFU&3g#=Fi+qqWS!Kqo-?I268>0>Uu!^b8j>6i|dn_@2wVjg|v@Dc?Wk7>@~*a zeoLJ)Ro7I1w6suUHVq)x;e2c9dQh;~zk?XA6UZq(&9sO=QOez%V}e-#D|$U5`+SYW zRy`0F3U|-?V|!Nh{O2XQ(H&3%NQY9^x@K!&)Su-%qybJ4)(JNtZY*sLFe%2aV!qh) zjUUdZFt-_jrKyKJU zWVvn^n_~?63QBgd+xYre$0fG)K?3_P)KB46G5Ple*=l0M!vs>Dc!E36?d|Cy+-b%@ zD2gYW$oZsV$nCn+G<+CH#k!tbY(@t-HM-vkgzKK{?nE4qX1K!o_y z5Q{G5eIUW?@&}kZbn9R0z5+z(VmCMCgIag4do5mI>agMNl!-w5dmjCd&cHLqR25P3~S@qHmR{mlDA62}6;;79dG3oJ&8c z+T}o7$AuhDT|tajuYau)|1ZTO{x8_^|Lr|Mxz3v*RI`@}(jiR{n<{EA6ijU6bd(K# zOaGUOqYSJ~FVIiVD#%#Q%Y+>XFUK~>p|Gs(xVo9A=9YZjEh46VoqkUiV`(=0CGe}c zU$%6dH(Awk9geDX%4vWpsGXN|9oT?G0D^_zvvma^L>aH$VoyPoqn6BI7k-7cG6@(--Vd2|yVW3?4YRMUd6(M+>4Zi!wN7GzG;^QSoyi%}36We= z#jwP0Swer;^Ioy#8&95f`UNO>DAB4tnpjG{Up$t}8AG(e`?dy9y15Snbea0=mqx60 zrG{4Zod%m!) zi|ESc?JdhQWfV{SkX9t(*2eOjwLzSm$rMge%YmJdP;qg6Ou++5Zcd;=f$`o1hK zuEdwYhG!o>@>ld_eYy!+wqlplwR3i6EWV`*`9LFDV3nXqRY<7;U+0H++A+!WY;d6% z&tyrjYlvC%Taof_=<|q6&jPl8+jcF{{a!0oO05niH@)K=CLpIq}ANdZ4M(IQ7GiBldvw*4w>t z%)dx;Cyn>f9g69tA>|IQfe@BjZc9wvro&-@4KXmND&L0@f$;d!=F3Fk#B=;7PC~E%ZGYYd!Ee#vEeh`7OUEZ$4pPisjv>_4nw=JLGM=(rf(7H|V)f+r3)Jh8z z@%p}?$^K||!tsu-6O~lzwT}t->7OT=*ZE$!!Ejz<+HIG^#v|+*>ru=p*Qzvza3H(s zjY-m_w(5&5Xk8tS^2<>A`6u)6n_ho0xfI(S&rZ8PwmPR$X!#Y~jK}yOZ?es*(+Ad| zgg#6lXiajdDgdA#2Q5rDI+FsvM*3R!h0#m(rrdL_G&g0gRu6oH4cwSzB^=`>i}k$1 zJIb8r=YnU=eC1iM{c(_d-@anpvUELYpCgd{%bNS^L<6PW_25#@sb9Ui%g*RZ1degB zY^sodH$319gtLhFxqV!hADUNc_;^;e_wEd*hT>!4JI{sNrQ+ac z!d}7B!DLioNacbgGTq}h(=IXccI%FmURF==wBhrEB-w8U>!%8ZZ^Qb3U*bDWkV_o> zv3y)YN3L3b*Zc8}=3f<2f8_7&?^_9ckI1Plgw^1qVMi(Fxe!Zp`|}ihZ2K&|_fLZ@ z9?UGsHL|==5pb=o&W)LK_5H6L@uKGa|aS z>p0EP*H-e!ISA$O6zqe!{H{=V;l>lexQsJ>cy{I9ACv^qmhMna;bVToC`E8}FK!UwWW}J$g`4U&64qIN!1;wNJ0cQw1g4I!E zst{Cci_XipjZa4l9~*8d>K(n|e3!=wbJ+osDTS>e$r9BUghL;JkE}uu@HgE9T{O)zJS;zv%YEr7;k)ai|*8}6q_)Ew{d;d8yP z4!hl?$hg)g%-x;dDL6Jf@;UJYU#^88FTX$WG6bjdD1OV*DKIG!n(@r1K(bdJEO&BJ zMx@L_91E&sCQB%kPvu9C(VbY_O^-Dg)wvhco0o{@d5%@MDenBe_aK(B{A*A*;HNcV|{1QiRY6fw(w zp+rhgr6-T5GSk&s4m66`RySHpEcFHB6w}iNPklSkw;=rU8~0<42uz=IUx;6hkt@nk ze60oorDNg9)Hx==ukeaBnx%<}BsqlE89O|8AsTP7X8f5(Jkk%A{;@=*k`oT`n>rU( zperoIGni_RpetgfQ*2G4hm(@u6sLM~3|_c~9BN~>jLJw-vq?peA)+fV+R>HRCrJtN z53-}1=hYOfaHbVCrLj5dWoW?*X#iDHOtTogV3JDYv@$0KADUxopU>Df_<%8U^u6_( zgqYBUqBqOa8;_jhN6qs-t-*2;RmL}m9WDHB_2-NAG5DsnjRO*e=IdR(9>4iwTbxiaDcCX8Dd(Ru#s5ZG8@3xnH;9v)gpu8Z@ z7W@<>RqtC_4=;*5p2?#T@eM(8qMnuyUx@t^ab;@21oB&j1`sL(Y@}TE(GED3_XW&7rpo#DFsI&K-ve zL+zV?3Us=?$gZ+EUvG3m^xbf|x=$TIb1@A5>DQPXFO`61X`rxkWGpF8dwGd%T4u(z5T1M z${i+RA@9A!$+_-T_9{$c!DX#UoCNs?gf4MKf??*!E2uYC;|HC9D3%{Xkj}7MjAnT$ zeiG=scA0;*U`h5~4YMu5n05&h4FwS+dRzVhaky1{5P}-ZbxqkOmp9_h`W{lXCm=4P zaz9>Tt$6@%IVxCMlII58@5wiML*okMpS1RS5BS)1!+Y;i4_Z~1s1Lip@H2#mtB(lb zD*0u*mNUoA_>><7UzI5uDh?BY)3okg1ILm0Ie$n#aOSPSFK!YcY@bIDInyk} z1@fupCV^6YMTewXsmOm^p&+i?gP1~fWm_9hy*QbbaF`qFk4YpD3t)wFcB`J1y8RD`=7K@A&xG{@kY-rMG9V4i+5$^=&u3f* zDDPp$GpX@vq4=$KDJTbiJjF5FdK|veYGeCZTgk<@oW93A0nU|R_ci0T^lx>W>E^N% z4$HTS=@l3ID~^$1JOW0ePm&vYKzwqWbRT?^^zf@6k6y&%gt=c0Nwxmu;_0LgQ!D1_ zmdZ03l7Gr#CK?dhGU+#mU2qdo-j8NOT*73jZ$OBnOK$eq*gCaGj67R~fM>knktHGu zY)U|eN(vL7`}3}wdn7J0UzrnCbhUGhZKk$}c`{uScU7RIVnfhCEc<;DcgnM>DvF4; zyL2;dIRxFv12h@Y{1w*y^T+bxZByS5tL8&mR^+3Zfu7tA;`72dRr@8J#%?QAYIq>H z>EVWhedYBVcn1AHy4)|`!2Fc&1zslQFwTfz)&{1Q7T-2k1mGwZ(389Ebu+9|p^Q^a zwFPhyg@jkq%(^hYB(JNqLK92&E>Mfndqn}BW!<|)I%46{&oXO=NI=_+;t`=g8)A^? z1zX?_m5I4DbP_8bzfVCz$O>1$lO+ym;a@< z)-}e^)q)oq0XiyOTF7fZr(QsYNB~-8+a3XlXy`o#=eumNMWQ<$%ZW2-+nhb@Qm3nR z_dn_i_+OOuEoohhR@g4cl3xXNA;3w{!+hSZ7BX=iLmRiqq z8*~Z$j|rur3kRdj(*|&7$D`_)krk?7X(Qe?n*C9-TMR};c-{+E3Nuu2hK(P- z_*^`na81WAyTp0}U9Qyj`xY5vUSNgHn3{Km4lMPzkC}X<@1QDFZ#%BmyfNyxbUQ0F z@lv~a?v#PzBQBA`*gp!6!le?ZRV8kkY_?S$(Y!?=-H)c=tyou*#-qCJqu`4DlXg2fO8(da_9I` z1x`FWH;n)(8 zKMG}h9(r*fMNe??+C8$xmS#QSeBn7iy)|qWx5SJP4gU4cY9($*Gq@z3tYE%*DrB)g zrVG1BxqY(4?9RxXeB3_nVl>^|4k?V^TaM|U+(P=O>#0Qe-lj^-^H;J%-!r%vHTXT& zrUY&oh|uJ{vopvJ;{5U2c|&6k^_G}j+@<;CJQMXQYR>(Te_i0DpX_cX#^M&T4BwKC&vwuK$zee!8 z0yhuIfe-@gfJyPz<_eX`*-cgVEU!$A;{@qR&i9y2k(?x*i_7VK*X+4)5NU6?ESjvmJ@B=RW|YcoAB2i8SPPiNeW5=j=oeqXcp@< zN8If*nW-0fwyb{WE;OYc?hIOBBN}3GGvNxW6i=Te`_GQpMagEzGdC~lm!m0a1k3RE zklQ|wx#g{1#xpplI~W}tmK7D2tfpAy3r!d4IouxBx=z}XMwCk&B^o0b24S|b?`FB; zIe!v%qF2Rsu0a;LG2g(<(6NnEk4p2x$K2%&!JFL65@J`tpNi!Jj(h>W5&+{)WnLZ> zn!MPrLfkF#fTQ!s8XX{ zhML5Q8mO%=zS?8rQ=YB^3(EUkw)&s=$~1g+`_sCL&eUyAZ53=5gef*t`W}lb<-cGR zE|_WwcvKJGCJ=5fT6pY{lnBeoOxxs$Y%iq9Q?C`8@Kv+%ytx3XvyLsH59Q?q431?! z;Ia|hwL|w&$*>d6mm{xcjKqE3zFZ=ds<4{_VpLug8v{tdjRt5-EbjV~ch&v!I``U~ zXA%#}^Z+XmrB*aG?w2(ydt=2mC}AT7HEdolyNq?$DL^iH-utBiB+sZ^?}C}K6UrIvecjEuQQlx=9dhs58X;I1e!=M&HloH{{{#C z9gDwX@wW#4H>-gM44{htM*`dbQ9J#=@BQDU=l%We{(YL@-|_eljK{}{SL9FQXVTpN z_BQ|acK_DE-x~N^1AlAaZw>sdfxk8Iw+8;!z~379TLXV<;BO85t%3igHSjrb)A=#A zg0#;+ATJH?$gRh;U$bqJ{A?h2AM(eyhF+a#%iliC9$3n~N3Z@W5{Sp8-FmThA`b?g0<%!)3!g3_9BiCiDj0OZavAgbi^knG_9HwYp;ehU~s?uAWWNaqEfv`#jv z2^qb>=j1_)|7u{ZZA8As+a>3meYVjHH;Uem z+S)wWsX(tI;51o|iq9#d{_aM5vb)29qPvfceyj?Y+)GmODV^DSG;Bq(b>L4%jUDFf zr_Al;Gi2I`4-PrinhzLRUHNbfbe9`gxf@TB+E=W-u*&GOc+ zexamKQ_C^01X-L7-U#6M2r3#6rr@-a_;Twp-GOdPECG1%(xYs9sizb+a#XrilkSs#7@$pv^_xc%uk{u*(a2R~e9@bC+TV}s4@ z(;)@WJxCg2@oR)K#n%Ylz_Bd^%p>CAW$EGMh=*jzQ%x<>#1W7ls&isQHXI9__TO_v zkp?Oczfk6>s2>xiA|8S+!2Ln9!0EJqcp9mXt-l6kQEKqg>HtsvIB@Wf_1ECIJfig> zK)VWl(q!^+Zc##A=aTR&Por$zkOKL$lu|iO!x9Fqs{sVMX@<@%;PdR%hUyy%3`}y?09WAdKa>9r=Mc_; diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 11f08cd2431..1bb8aa5d83c 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -1,7 +1,5 @@ use std::time::Duration; -#[cfg(feature = "image-proc")] -use matrix_sdk::attachment::{ImageFormat, ThumbnailFormat}; use matrix_sdk::{ attachment::{ AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, @@ -292,232 +290,3 @@ async fn test_room_attachment_send_mentions() { assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) } - -#[cfg(feature = "image-proc")] -const IMAGE_BYTES: &[u8] = include_bytes!("matrix-rusty.jpg"); - -#[cfg(feature = "image-proc")] -#[async_test] -async fn test_room_attachment_generate_thumbnail_original_format() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", - "info": { - "mimetype": "image/jpeg", - "thumbnail_info": { - "h": 600, - "w": 600, - "mimetype":"image/jpeg", - }, - "thumbnail_url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://localhost/AQwafuaFswefuhsfAFAgsw" - }))) - .expect(2) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let config = AttachmentConfig::new().generate_thumbnail(None, ThumbnailFormat::Original); - - let response = room - .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - -#[cfg(feature = "image-proc")] -#[async_test] -async fn test_room_attachment_generate_thumbnail_always_format() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "url": "mxc://localhost/original", - "info": { - "mimetype": "image/jpeg", - "thumbnail_info": { - "h": 600, - "w": 600, - "mimetype":"image/png", - }, - "thumbnail_url": "mxc://localhost/thumbnail", - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://localhost/original" - }))) - .expect(1) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/png")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://localhost/thumbnail" - }))) - .expect(1) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let config = - AttachmentConfig::new().generate_thumbnail(None, ThumbnailFormat::Always(ImageFormat::Png)); - - let response = room - .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - -#[cfg(feature = "image-proc")] -#[async_test] -async fn test_room_attachment_generate_thumbnail_not_fallback_format() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", - "info": { - "mimetype": "image/jpeg", - "thumbnail_info": { - "h": 600, - "w": 600, - "mimetype":"image/jpeg", - }, - "thumbnail_url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://localhost/AQwafuaFswefuhsfAFAgsw" - }))) - .expect(2) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let config = AttachmentConfig::new() - .generate_thumbnail(None, ThumbnailFormat::Fallback(ImageFormat::Png)); - - let response = room - .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - -#[cfg(feature = "image-proc")] -#[async_test] -async fn test_room_attachment_generate_thumbnail_bigger_than_image() { - use matrix_sdk_test::mocks::mock_encryption_state; - - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "url": "mxc://localhost/original", - "info": { - "mimetype": "image/jpeg", - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://localhost/original" - }))) - .expect(1) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let config = - AttachmentConfig::new().generate_thumbnail(Some((1400, 1400)), ThumbnailFormat::Original); - - let response = room - .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index b833c69f7a5..40c8db5df2a 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -69,7 +69,6 @@ enum FeatureSet { Markdown, Socks, SsoLogin, - ImageProc, } #[derive(Subcommand, PartialEq, Eq, PartialOrd, Ord)] @@ -222,7 +221,6 @@ fn run_feature_tests(cmd: Option) -> Result<()> { (FeatureSet::Markdown, "--features markdown,testing"), (FeatureSet::Socks, "--features socks,testing"), (FeatureSet::SsoLogin, "--features sso-login,testing"), - (FeatureSet::ImageProc, "--features image-proc,testing"), ]); let run = |arg_set: &str| { From 3d0423447c449f2c39256d4e2269ec0b89bb8b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 16 Oct 2024 10:31:00 +0200 Subject: [PATCH 287/979] fix(qrcode): Do not enable default features of image crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gets rid of dependencies for the different image formats. Signed-off-by: Kévin Commaille --- Cargo.lock | 451 +--------------------------- crates/matrix-sdk-qrcode/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 451 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01a2e0e8e53..be9c9a99775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,12 +72,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aligned-vec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" - [[package]] name = "allocator-api2" version = "0.2.18" @@ -180,23 +174,6 @@ dependencies = [ "syn", ] -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "arrayref" version = "0.3.8" @@ -391,29 +368,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" -[[package]] -name = "av1-grain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" -dependencies = [ - "arrayvec", -] - [[package]] name = "axum" version = "0.7.5" @@ -575,12 +529,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" version = "1.3.2" @@ -602,12 +550,6 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" -[[package]] -name = "bitstream-io" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c12d1856e42f0d817a835fe55853957c85c8c8a470114029143d3f12671446e" - [[package]] name = "blake3" version = "1.5.3" @@ -659,12 +601,6 @@ dependencies = [ "serde", ] -[[package]] -name = "built" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" - [[package]] name = "bumpalo" version = "3.16.0" @@ -768,20 +704,6 @@ name = "cc" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" -dependencies = [ - "jobserver", - "libc", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] [[package]] name = "cfg-if" @@ -933,12 +855,6 @@ dependencies = [ "tracing-error", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.1" @@ -1702,22 +1618,6 @@ dependencies = [ "url", ] -[[package]] -name = "exr" -version = "1.72.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" -dependencies = [ - "bit_field", - "flume", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - [[package]] name = "extension-trait" version = "1.0.2" @@ -1807,15 +1707,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "fdeflate" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" -dependencies = [ - "simd-adler32", -] - [[package]] name = "ff" version = "0.13.0" @@ -1854,15 +1745,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -2040,16 +1922,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gif" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gimli" version = "0.28.1" @@ -2429,29 +2301,7 @@ checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", "num-traits", - "png", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" -dependencies = [ - "byteorder-lite", - "quick-error 2.0.1", ] [[package]] @@ -2477,12 +2327,6 @@ dependencies = [ "bitmaps", ] -[[package]] -name = "imgref" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" - [[package]] name = "include_dir" version = "0.7.4" @@ -2590,17 +2434,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -2663,21 +2496,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" -dependencies = [ - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" - [[package]] name = "js-sys" version = "0.3.69" @@ -2768,29 +2586,12 @@ dependencies = [ "spin", ] -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -[[package]] -name = "libfuzzer-sys" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" -dependencies = [ - "arbitrary", - "cc", - "once_cell", -] - [[package]] name = "libm" version = "0.2.8" @@ -2850,15 +2651,6 @@ dependencies = [ "log", ] -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - [[package]] name = "lru" version = "0.12.3" @@ -3529,16 +3321,6 @@ dependencies = [ "wiremock", ] -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - [[package]] name = "memchr" version = "2.7.4" @@ -3589,7 +3371,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", - "simd-adler32", ] [[package]] @@ -3689,12 +3470,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3705,16 +3480,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -3738,17 +3503,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "num-format" version = "0.4.4" @@ -3779,17 +3533,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -4286,19 +4029,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "png" -version = "0.17.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -4398,25 +4128,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "profiling" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "proptest" version = "1.5.0" @@ -4478,15 +4189,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - [[package]] name = "qrcode" version = "0.14.1" @@ -4502,12 +4204,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quick-xml" version = "0.26.0" @@ -4640,56 +4336,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "rav1e" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" -dependencies = [ - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.12.1", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "once_cell", - "paste", - "profiling", - "rand", - "rand_chacha", - "simd_helpers", - "system-deps", - "thiserror", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error 2.0.1", - "rav1e", - "rayon", - "rgb", -] - [[package]] name = "rayon" version = "1.10.0" @@ -5194,7 +4840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", - "quick-error 1.2.3", + "quick-error", "tempfile", "wait-timeout", ] @@ -5556,21 +5202,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - [[package]] name = "similar" version = "2.6.0" @@ -5633,9 +5264,6 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] [[package]] name = "spki" @@ -5790,25 +5418,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml 0.8.15", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" - [[package]] name = "tempfile" version = "3.10.1" @@ -5871,17 +5480,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tiff" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" -dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", -] - [[package]] name = "time" version = "0.3.36" @@ -6508,17 +6106,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v_frame" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.0" @@ -6543,12 +6130,6 @@ dependencies = [ "time", ] -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.4" @@ -6761,12 +6342,6 @@ dependencies = [ "nom", ] -[[package]] -name = "weezl" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" - [[package]] name = "wildmatch" version = "2.3.4" @@ -7080,27 +6655,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" -dependencies = [ - "zune-core", -] diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index 80ae7c62eb2..264bfe0f777 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -26,7 +26,7 @@ thiserror = { workspace = true } vodozemac = { workspace = true } [dev-dependencies] -image = "0.25.1" +image = { version = "0.25.1", default-features = false } qrcode = { version = "0.14.0", default-features = false, features = ["image"] } [lints] From 79798a9de9b729a7bf6c26e110165d3b96e008e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 16 Oct 2024 10:11:30 +0200 Subject: [PATCH 288/979] refactor(oidc): allow passing a `Prompt` to get an OIDC url Changelog: `Client::url_for_oidc_login` is now `Client::url_for_oidc` with an additional `OidcPrompt` parameter. `abort_oidc_login` has been renamed to `abort_oidc_auth`. This allows clients to directly open the web page they want: the login one, the registration one, consent, etc. It should improve the UX in the registration flow since we can now skip the login one. --- bindings/matrix-sdk-ffi/src/client.rs | 58 ++++++++++++++++++++++++--- crates/matrix-sdk/src/oidc/mod.rs | 7 ++-- crates/matrix-sdk/src/oidc/tests.rs | 7 ++-- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 8e451b65b6e..960abbb4cb4 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -19,6 +19,7 @@ use matrix_sdk::{ registration::{ ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata, }, + requests::Prompt as SdkOidcPrompt, }, OidcAuthorizationData, OidcSession, }, @@ -360,13 +361,14 @@ impl Client { Ok(Arc::new(SsoHandler { client: Arc::clone(self), url })) } - /// Requests the URL needed for login in a web view using OIDC. Once the web + /// Requests the URL needed for opening a web view using OIDC. Once the web /// view has succeeded, call `login_with_oidc_callback` with the callback it /// returns. If a failure occurs and a callback isn't available, make sure - /// to call `abort_oidc_login` to inform the client of this. - pub async fn url_for_oidc_login( + /// to call `abort_oidc_auth` to inform the client of this. + pub async fn url_for_oidc( &self, oidc_configuration: &OidcConfiguration, + prompt: OidcPrompt, ) -> Result, OidcError> { let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?; let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file); @@ -387,14 +389,15 @@ impl Client { static_registrations, )?; - let data = self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await?; + let data = + self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?; Ok(Arc::new(data)) } /// Aborts an existing OIDC login operation that might have been cancelled, /// failed etc. - pub async fn abort_oidc_login(&self, authorization_data: Arc) { + pub async fn abort_oidc_auth(&self, authorization_data: Arc) { self.inner.oidc().abort_authorization(&authorization_data.state).await; } @@ -1731,3 +1734,48 @@ impl TryFrom for SdkSlidingSyncVersion { }) } } + +#[derive(uniffi::Enum)] +pub enum OidcPrompt { + /// The Authorization Server must not display any authentication or consent + /// user interface pages. + None, + + /// The Authorization Server should prompt the End-User for + /// reauthentication. + Login, + + /// The Authorization Server should prompt the End-User for consent before + /// returning information to the Client. + Consent, + + /// The Authorization Server should prompt the End-User to select a user + /// account. + /// + /// This enables an End-User who has multiple accounts at the Authorization + /// Server to select amongst the multiple accounts that they might have + /// current sessions for. + SelectAccount, + + /// The Authorization Server should prompt the End-User to create a user + /// account. + /// + /// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html). + Create, + + /// An unknown value. + Unknown { value: String }, +} + +impl From for SdkOidcPrompt { + fn from(value: OidcPrompt) -> Self { + match value { + OidcPrompt::None => Self::None, + OidcPrompt::Login => Self::Login, + OidcPrompt::Consent => Self::Consent, + OidcPrompt::SelectAccount => Self::SelectAccount, + OidcPrompt::Create => Self::Create, + OidcPrompt::Unknown { value } => Self::Unknown(value), + } + } +} diff --git a/crates/matrix-sdk/src/oidc/mod.rs b/crates/matrix-sdk/src/oidc/mod.rs index 4f2e7fc2d0b..ab686b23a96 100644 --- a/crates/matrix-sdk/src/oidc/mod.rs +++ b/crates/matrix-sdk/src/oidc/mod.rs @@ -443,10 +443,11 @@ impl Oidc { /// webview for a user to login to their account. Call /// [`Oidc::login_with_oidc_callback`] to finish the process when the /// webview is complete. - pub async fn url_for_oidc_login( + pub async fn url_for_oidc( &self, client_metadata: VerifiedClientMetadata, registrations: OidcRegistrations, + prompt: Prompt, ) -> Result { let issuer = match self.fetch_authentication_issuer().await { Ok(issuer) => issuer, @@ -470,7 +471,7 @@ impl Oidc { self.configure(issuer, client_metadata, registrations).await?; let mut data_builder = self.login(redirect_url.clone(), None)?; - data_builder = data_builder.prompt(vec![Prompt::Consent]); + data_builder = data_builder.prompt(vec![prompt]); let data = data_builder.build().await?; Ok(data) @@ -478,7 +479,7 @@ impl Oidc { /// A higher level wrapper around the methods to complete a login after the /// user has logged in through a webview. This method should be used in - /// tandem with [`Oidc::url_for_oidc_login`]. + /// tandem with [`Oidc::url_for_oidc`]. pub async fn login_with_oidc_callback( &self, authorization_data: &OidcAuthorizationData, diff --git a/crates/matrix-sdk/src/oidc/tests.rs b/crates/matrix-sdk/src/oidc/tests.rs index 5c6a7bc8ba1..0c40cff89cf 100644 --- a/crates/matrix-sdk/src/oidc/tests.rs +++ b/crates/matrix-sdk/src/oidc/tests.rs @@ -9,6 +9,7 @@ use mas_oidc_client::{ errors::ClientErrorCode, iana::oauth::OAuthClientAuthenticationMethod, registration::{ClientMetadata, VerifiedClientMetadata}, + requests::Prompt, }, }; use matrix_sdk_base::SessionMeta; @@ -124,7 +125,7 @@ async fn test_high_level_login() -> anyhow::Result<()> { // When getting the OIDC login URL. let authorization_data = - oidc.url_for_oidc_login(metadata.clone(), registrations).await.unwrap(); + oidc.url_for_oidc(metadata.clone(), registrations, Prompt::Login).await.unwrap(); // Then the client should be configured correctly. assert!(oidc.issuer().is_some()); @@ -146,7 +147,7 @@ async fn test_high_level_login_cancellation() -> anyhow::Result<()> { // Given a client ready to complete login. let (oidc, _server, metadata, registrations) = mock_environment().await.unwrap(); let authorization_data = - oidc.url_for_oidc_login(metadata.clone(), registrations).await.unwrap(); + oidc.url_for_oidc(metadata.clone(), registrations, Prompt::Login).await.unwrap(); assert!(oidc.issuer().is_some()); assert!(oidc.client_metadata().is_some()); @@ -170,7 +171,7 @@ async fn test_high_level_login_invalid_state() -> anyhow::Result<()> { // Given a client ready to complete login. let (oidc, _server, metadata, registrations) = mock_environment().await.unwrap(); let authorization_data = - oidc.url_for_oidc_login(metadata.clone(), registrations).await.unwrap(); + oidc.url_for_oidc(metadata.clone(), registrations, Prompt::Login).await.unwrap(); assert!(oidc.issuer().is_some()); assert!(oidc.client_metadata().is_some()); From efc2e2c4c8d8abe8a0ac49ca14252a76f67bac9e Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 16 Oct 2024 10:21:05 +0100 Subject: [PATCH 289/979] doc(contributing): Recommend --interactive for git rebase --autosquash Older versions of Git require --interactive when we supply --autosquash, and it's also probably a good idea generally. See https://stackoverflow.com/a/77663575/22610 for more info. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e3313cd9b5..0d35c9b33aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,7 +193,7 @@ requested. commits, the [autosquash] option can help with this. ```bash -git rebase main --autosquash +git rebase main --interactive --autosquash ``` [fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt From 77e52817811c97a5e60c61360782fe8b30239eb7 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 14:18:40 +0200 Subject: [PATCH 290/979] chore(timeline): instrument timeline tasks with their focus and internal prefix This would have avoided a few hours of debugging where we thought there was an issue with multiple timelines spawned at the same time, and then realized it was expected because of the existence of the pinned timeline in EX apps. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 19 +++++++++++++++---- crates/matrix-sdk-ui/src/timeline/mod.rs | 10 ++++++++++ .../tests/integration/timeline/echo.rs | 8 +++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 3553c2ecd71..2314a1afd3a 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -163,7 +163,7 @@ impl TimelineBuilder { let controller = TimelineController::new( room, focus.clone(), - internal_id_prefix, + internal_id_prefix.clone(), unable_to_decrypt_hook, is_room_encrypted, ) @@ -206,8 +206,13 @@ impl TimelineBuilder { let room_event_cache = room_event_cache.clone(); let inner = controller.clone(); - let span = - info_span!(parent: Span::none(), "room_update_handler", room_id = ?room.room_id()); + let span = info_span!( + parent: Span::none(), + "live_update_handler", + room_id = ?room.room_id(), + focus = focus.debug_string(), + prefix = internal_id_prefix + ); span.follows_from(Span::current()); async move { @@ -307,7 +312,13 @@ impl TimelineBuilder { timeline.handle_local_echo(echo).await; } - let span = info_span!(parent: Span::none(), "local_echo_handler", room_id = ?room.room_id()); + let span = info_span!( + parent: Span::none(), + "local_echo_handler", + room_id = ?room.room_id(), + focus = focus.debug_string(), + prefix = internal_id_prefix + ); span.follows_from(Span::current()); // React to future local echoes too. diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 0dc57b1e936..76c62e32f25 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -171,6 +171,16 @@ pub enum TimelineFocus { PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 }, } +impl TimelineFocus { + pub(super) fn debug_string(&self) -> String { + match self { + TimelineFocus::Live => "live".to_owned(), + TimelineFocus::Event { target, .. } => format!("permalink:{target}"), + TimelineFocus::PinnedEvents { .. } => "pinned-events".to_owned(), + } + } +} + impl Timeline { /// Create a new [`TimelineBuilder`] for the given room. pub fn builder(room: &Room) -> TimelineBuilder { diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs index 03047fa6a45..97366d6e9a6 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs @@ -60,7 +60,13 @@ async fn test_echo() { mock_encryption_state(&server, false).await; let room = client.get_room(room_id).unwrap(); - let timeline = Arc::new(room.timeline().await.unwrap()); + let timeline = Arc::new( + room.timeline_builder() + .with_internal_id_prefix("le_prefix".to_owned()) + .build() + .await + .unwrap(), + ); let (_, mut timeline_stream) = timeline.subscribe().await; Mock::given(method("PUT")) From 8b7494d17bc5e5d7d0fe54b8b7b6adf61ff9b56e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 15 Oct 2024 15:27:01 +0200 Subject: [PATCH 291/979] timeline: use a `TimelineItemId` to react to a timeline item Changelog: `Timeline::toggle_reaction` now identifies the item that's reacted to with a `TimelineEventItemId`. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 16 +++++----- .../src/timeline/controller/mod.rs | 8 ++--- crates/matrix-sdk-ui/src/timeline/mod.rs | 11 +++---- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 23 +++++++------- .../src/timeline/tests/reactions.rs | 28 ++++++++++------- crates/matrix-sdk-ui/src/timeline/util.rs | 30 +++++++++++++------ .../tests/integration/timeline/focus_event.rs | 2 +- .../tests/integration/timeline/reactions.rs | 28 ++++++++--------- .../src/tests/timeline.rs | 25 ++++++++-------- 9 files changed, 96 insertions(+), 75 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 7d3117c3b8c..66d686fcb49 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -495,7 +495,7 @@ impl Timeline { new_content: EditedContent, ) -> Result { self.inner - .edit_by_id(&(event_or_transaction_id.try_into()?), new_content.try_into()?) + .edit_by_id(&event_or_transaction_id.try_into()?, new_content.try_into()?) .await .map_err(Into::into) } @@ -530,19 +530,21 @@ impl Timeline { /// Toggle a reaction on an event. /// - /// The `unique_id` parameter is a string returned by - /// the `TimelineItem::unique_id()` method. As such, this method works both - /// on local echoes and remote items. - /// /// Adds or redacts a reaction based on the state of the reaction at the /// time it is called. /// + /// This method works both on local echoes and remote items. + /// /// When redacting a previous reaction, the redaction reason is not set. /// /// Ensures that only one reaction is sent at a time to avoid race /// conditions and spamming the homeserver with requests. - pub async fn toggle_reaction(&self, unique_id: String, key: String) -> Result<(), ClientError> { - self.inner.toggle_reaction(&unique_id, &key).await?; + pub async fn toggle_reaction( + &self, + item_id: EventOrTransactionId, + key: String, + ) -> Result<(), ClientError> { + self.inner.toggle_reaction(&item_id.try_into()?, &key).await?; Ok(()) } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index ba23ab6e5bb..94ff0944459 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -70,7 +70,7 @@ use crate::{ event_item::EventTimelineItemKind, pinned_events_loader::{PinnedEventsLoader, PinnedEventsLoaderError}, reactions::FullReactionKey, - util::rfind_event_by_uid, + util::rfind_event_by_item_id, TimelineEventFilterFn, }, unable_to_decrypt_hook::UtdHookManager, @@ -484,12 +484,12 @@ impl TimelineController

{ #[instrument(skip_all)] pub(super) async fn toggle_reaction_local( &self, - unique_id: &str, + item_id: &TimelineEventItemId, key: &str, ) -> Result { let mut state = self.state.write().await; - let Some((item_pos, item)) = rfind_event_by_uid(&state.items, unique_id) else { + let Some((item_pos, item)) = rfind_event_by_item_id(&state.items, item_id) else { warn!("Timeline item not found, can't add reaction"); return Err(Error::FailedToToggleReaction); }; @@ -502,7 +502,7 @@ impl TimelineController

{ .map(|reaction_info| reaction_info.status.clone()); let Some(prev_status) = prev_status else { - match &item.inner.kind { + match &item.kind { EventTimelineItemKind::Local(local) => { if let Some(send_handle) = local.send_handle.clone() { if send_handle diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 76c62e32f25..c364fe7cf86 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -557,9 +557,6 @@ impl Timeline { /// Toggle a reaction on an event. /// - /// The `unique_id` parameter is a string returned by - /// [`TimelineItem::unique_id()`]. - /// /// Adds or redacts a reaction based on the state of the reaction at the /// time it is called. /// @@ -567,8 +564,12 @@ impl Timeline { /// /// Ensures that only one reaction is sent at a time to avoid race /// conditions and spamming the homeserver with requests. - pub async fn toggle_reaction(&self, unique_id: &str, reaction_key: &str) -> Result<(), Error> { - self.controller.toggle_reaction_local(unique_id, reaction_key).await?; + pub async fn toggle_reaction( + &self, + item_id: &TimelineEventItemId, + reaction_key: &str, + ) -> Result<(), Error> { + self.controller.toggle_reaction_local(item_id, reaction_key).await?; Ok(()) } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 5c12916221f..46ae26f589b 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -60,7 +60,9 @@ use super::{ event_handler::TimelineEventKind, event_item::RemoteEventOrigin, traits::RoomDataProvider, - EventTimelineItem, Profile, TimelineController, TimelineFocus, TimelineItem, + util::rfind_event_by_item_id, + EventTimelineItem, Profile, TimelineController, TimelineEventItemId, TimelineFocus, + TimelineItem, }; use crate::{ timeline::pinned_events_loader::PinnedEventsRoom, unable_to_decrypt_hook::UtdHookManager, @@ -265,17 +267,16 @@ impl TestTimeline { self.controller.handle_read_receipts(ev_content).await; } - async fn toggle_reaction_local(&self, unique_id: &str, key: &str) -> Result<(), super::Error> { - if self.controller.toggle_reaction_local(unique_id, key).await? { + async fn toggle_reaction_local( + &self, + item_id: &TimelineEventItemId, + key: &str, + ) -> Result<(), super::Error> { + if self.controller.toggle_reaction_local(item_id, key).await? { // TODO(bnjbvr): hacky? - if let Some(event_id) = self - .controller - .items() - .await - .iter() - .rfind(|item| item.unique_id() == unique_id) - .and_then(|item| item.as_event()?.as_remote()) - .map(|event_item| event_item.event_id.clone()) + let items = self.controller.items().await; + if let Some(event_id) = rfind_event_by_item_id(&items, item_id) + .and_then(|(_pos, item)| item.event_id().map(ToOwned::to_owned)) { // Fake a local echo, for new reactions. self.handle_local_event( diff --git a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs index 2321563c107..bb0a8ab51da 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs @@ -29,7 +29,7 @@ use tokio::time::timeout; use crate::timeline::{ controller::TimelineEnd, event_item::RemoteEventOrigin, tests::TestTimeline, ReactionStatus, - TimelineItem, + TimelineEventItemId, TimelineItem, }; const REACTION_KEY: &str = "👍"; @@ -83,7 +83,11 @@ async fn test_add_reaction_on_non_existent_event() { let timeline = TestTimeline::new(); let mut stream = timeline.subscribe().await; - timeline.toggle_reaction_local("nonexisting_unique_id", REACTION_KEY).await.unwrap_err(); + let event_id = EventId::parse("$nonexisting_unique_id").unwrap(); + timeline + .toggle_reaction_local(&TimelineEventItemId::EventId(event_id), REACTION_KEY) + .await + .unwrap_err(); assert!(stream.next().now_or_never().is_none()); } @@ -92,10 +96,10 @@ async fn test_add_reaction_on_non_existent_event() { async fn test_add_reaction_success() { let timeline = TestTimeline::new(); let mut stream = timeline.subscribe().await; - let (msg_uid, event_id, item_pos) = send_first_message(&timeline, &mut stream).await; + let (item_id, event_id, item_pos) = send_first_message(&timeline, &mut stream).await; // If I toggle a reaction on an event which didn't have any… - timeline.toggle_reaction_local(&msg_uid, REACTION_KEY).await.unwrap(); + timeline.toggle_reaction_local(&item_id, REACTION_KEY).await.unwrap(); // The timeline item is updated, with a local echo for the reaction. assert_reaction_is_updated!(stream, &event_id, item_pos, false); @@ -123,7 +127,7 @@ async fn test_redact_reaction_success() { let f = &timeline.factory; let mut stream = timeline.subscribe().await; - let (msg_uid, event_id, item_pos) = send_first_message(&timeline, &mut stream).await; + let (item_id, event_id, item_pos) = send_first_message(&timeline, &mut stream).await; // A reaction is added by sync. let reaction_id = event_id!("$reaction_id"); @@ -135,7 +139,7 @@ async fn test_redact_reaction_success() { assert_reaction_is_updated!(stream, &event_id, item_pos, true); // Toggling the reaction locally… - timeline.toggle_reaction_local(&msg_uid, REACTION_KEY).await.unwrap(); + timeline.toggle_reaction_local(&item_id, REACTION_KEY).await.unwrap(); // Will immediately redact it on the item. let event = assert_item_update!(stream, &event_id, item_pos); @@ -166,12 +170,12 @@ async fn test_redact_reaction_success() { async fn test_reactions_store_timestamp() { let timeline = TestTimeline::new(); let mut stream = timeline.subscribe().await; - let (msg_uid, event_id, msg_pos) = send_first_message(&timeline, &mut stream).await; + let (item_id, event_id, msg_pos) = send_first_message(&timeline, &mut stream).await; // Creating a reaction adds a valid timestamp. let timestamp_before = MilliSecondsSinceUnixEpoch::now(); - timeline.toggle_reaction_local(&msg_uid, REACTION_KEY).await.unwrap(); + timeline.toggle_reaction_local(&item_id, REACTION_KEY).await.unwrap(); let event = assert_reaction_is_updated!(stream, &event_id, msg_pos, false); let reactions = event.reactions().get(&REACTION_KEY.to_owned()).unwrap(); @@ -216,15 +220,17 @@ async fn test_initial_reaction_timestamp_is_stored() { async fn send_first_message( timeline: &TestTimeline, stream: &mut (impl Stream>> + Unpin), -) -> (String, OwnedEventId, usize) { +) -> (TimelineEventItemId, OwnedEventId, usize) { timeline.handle_live_event(timeline.factory.text_msg("I want you to react").sender(&BOB)).await; let item = assert_next_matches!(*stream, VectorDiff::PushBack { value } => value); - let event_id = item.as_event().unwrap().as_remote().unwrap().event_id.clone(); + let event_item = item.as_event().unwrap(); + let item_id = event_item.identifier(); + let event_id = event_item.event_id().unwrap().to_owned(); let position = timeline.len().await - 1; let day_divider = assert_next_matches!(*stream, VectorDiff::PushFront { value } => value); assert!(day_divider.is_day_divider()); - (item.unique_id().to_owned(), event_id, position) + (item_id, event_id, position) } diff --git a/crates/matrix-sdk-ui/src/timeline/util.rs b/crates/matrix-sdk-ui/src/timeline/util.rs index d3e7d715622..91159f8654c 100644 --- a/crates/matrix-sdk-ui/src/timeline/util.rs +++ b/crates/matrix-sdk-ui/src/timeline/util.rs @@ -21,7 +21,8 @@ use ruma::{EventId, MilliSecondsSinceUnixEpoch}; #[cfg(doc)] use super::controller::TimelineMetadata; use super::{ - event_item::EventTimelineItemKind, EventTimelineItem, ReactionsByKeyBySender, TimelineItem, + event_item::EventTimelineItemKind, EventTimelineItem, ReactionsByKeyBySender, + TimelineEventItemId, TimelineItem, }; pub(super) struct EventTimelineItemWithId<'a> { @@ -80,26 +81,37 @@ pub(super) fn rfind_event_item( rfind_event_item_internal(items, |item_with_id| f(item_with_id.inner)) } -/// Find the timeline item that matches the given internal id, if any. +/// Find the timeline item that matches the given event id, if any. /// /// WARNING: Linear scan of the items, see documentation of /// [`rfind_event_item`]. -pub(super) fn rfind_event_by_uid<'a>( +pub(super) fn rfind_event_by_id<'a>( items: &'a Vector>, - internal_id: &'a str, + event_id: &EventId, ) -> Option<(usize, EventTimelineItemWithId<'a>)> { - rfind_event_item_internal(items, |item_with_id| item_with_id.internal_id == internal_id) + rfind_event_item(items, |it| it.event_id() == Some(event_id)) } -/// Find the timeline item that matches the given event id, if any. +/// Find the timeline item that matches the given item (event or transaction) +/// id, if any. /// /// WARNING: Linear scan of the items, see documentation of /// [`rfind_event_item`]. -pub(super) fn rfind_event_by_id<'a>( +pub(super) fn rfind_event_by_item_id<'a>( items: &'a Vector>, - event_id: &EventId, + item_id: &TimelineEventItemId, ) -> Option<(usize, EventTimelineItemWithId<'a>)> { - rfind_event_item(items, |it| it.event_id() == Some(event_id)) + match item_id { + TimelineEventItemId::TransactionId(txn_id) => { + rfind_event_item(items, |item| match &item.kind { + EventTimelineItemKind::Local(local) => local.transaction_id == *txn_id, + EventTimelineItemKind::Remote(remote) => { + remote.transaction_id.as_deref() == Some(txn_id) + } + }) + } + TimelineEventItemId::EventId(event_id) => rfind_event_by_id(items, event_id), + } } /// Result of comparing events position in the timeline. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs index 27a6056fa42..fa47850034e 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs @@ -320,7 +320,7 @@ async fn test_focused_timeline_local_echoes() { assert_pending!(timeline_stream); // Add a reaction to the focused event, which will cause a local echo to happen. - timeline.toggle_reaction(items[1].unique_id(), "✨").await.unwrap(); + timeline.toggle_reaction(&event_item.identifier(), "✨").await.unwrap(); // We immediately get the local echo for the reaction. let item = assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index: 1, value: item } => item); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs index 8202af31d40..671750aea23 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs @@ -75,7 +75,7 @@ async fn test_abort_before_being_sent() { assert_let!(Some(VectorDiff::PushBack { value: first }) = stream.next().await); let item = first.as_event().unwrap(); - let unique_id = first.unique_id(); + let item_id = item.identifier(); assert_eq!(item.content().as_message().unwrap().body(), "hello"); assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = stream.next().await); @@ -102,7 +102,7 @@ async fn test_abort_before_being_sent() { mock_redaction(event_id!("$3")).mount(&server).await; // We add the reaction… - timeline.toggle_reaction(unique_id, "👍").await.unwrap(); + timeline.toggle_reaction(&item_id, "👍").await.unwrap(); // First toggle (local echo). { @@ -119,7 +119,7 @@ async fn test_abort_before_being_sent() { } // We toggle another reaction at the same time… - timeline.toggle_reaction(unique_id, "🥰").await.unwrap(); + timeline.toggle_reaction(&item_id, "🥰").await.unwrap(); { assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); @@ -140,7 +140,7 @@ async fn test_abort_before_being_sent() { // Then we remove the first one; because it was being sent, it should lead to a // redaction event. - timeline.toggle_reaction(unique_id, "👍").await.unwrap(); + timeline.toggle_reaction(&item_id, "👍").await.unwrap(); { assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); @@ -157,7 +157,7 @@ async fn test_abort_before_being_sent() { // But because the first one was being sent, this one won't and the local echo // could be discarded. - timeline.toggle_reaction(unique_id, "🥰").await.unwrap(); + timeline.toggle_reaction(&item_id, "🥰").await.unwrap(); { assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); @@ -214,12 +214,11 @@ async fn test_redact_failed() { let _response = client.sync_once(Default::default()).await.unwrap(); server.reset().await; - let unique_id = assert_next_matches_with_timeout!(stream, VectorDiff::PushBack { value: item } => { - let unique_id = item.unique_id().to_owned(); + let item_id = assert_next_matches_with_timeout!(stream, VectorDiff::PushBack { value: item } => { let item = item.as_event().unwrap(); assert_eq!(item.content().as_message().unwrap().body(), "hello"); assert!(item.reactions().is_empty()); - unique_id + item.identifier() }); assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 0, value: item } => { @@ -242,7 +241,7 @@ async fn test_redact_failed() { .await; // We toggle the reaction, which fails with an error. - timeline.toggle_reaction(&unique_id, "😆").await.unwrap_err(); + timeline.toggle_reaction(&item_id, "😆").await.unwrap_err(); // The local echo is removed (assuming the redaction works)… assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { @@ -310,13 +309,12 @@ async fn test_local_reaction_to_local_echo() { let _ = timeline.send(RoomMessageEventContent::text_plain("lol").into()).await.unwrap(); // Receive a local echo. - let unique_id = assert_next_matches_with_timeout!(stream, VectorDiff::PushBack { value: item } => { - let unique_id = item.unique_id().to_owned(); + let item_id = assert_next_matches_with_timeout!(stream, VectorDiff::PushBack { value: item } => { let item = item.as_event().unwrap(); assert!(item.is_local_echo()); assert_eq!(item.content().as_message().unwrap().body(), "lol"); assert!(item.reactions().is_empty()); - unique_id + item.identifier() }); // Good ol' day divider. @@ -326,7 +324,7 @@ async fn test_local_reaction_to_local_echo() { // Add a reaction before the remote echo comes back. let key1 = "🤣"; - timeline.toggle_reaction(&unique_id, key1).await.unwrap(); + timeline.toggle_reaction(&item_id, key1).await.unwrap(); // The reaction is added to the local echo. assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { @@ -338,7 +336,7 @@ async fn test_local_reaction_to_local_echo() { // Add another reaction. let key2 = "😈"; - timeline.toggle_reaction(&unique_id, key2).await.unwrap(); + timeline.toggle_reaction(&item_id, key2).await.unwrap(); // Also comes as a local echo. assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { @@ -350,7 +348,7 @@ async fn test_local_reaction_to_local_echo() { // Remove second reaction. It's immediately removed, since it was a local echo, // and it wasn't being sent. - timeline.toggle_reaction(&unique_id, key2).await.unwrap(); + timeline.toggle_reaction(&item_id, key2).await.unwrap(); assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { let reactions = item.as_event().unwrap().reactions(); diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index bf8c670105f..4097ce04c46 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -96,17 +96,15 @@ async fn test_toggling_reaction() -> Result<()> { items.iter().find_map(|item| { let event = item.as_event()?; if !event.is_local_echo() && event.content().as_message()?.body().trim() == "hi!" { - event - .event_id() - .map(|event_id| (item.unique_id().to_owned(), event_id.to_owned())) + event.event_id().map(ToOwned::to_owned) } else { None } }) }; - if let Some(pair) = find_event_id(&items) { - return Ok(pair); + if let Some(event_id) = find_event_id(&items) { + return Ok(event_id); } warn!(?items, "Waiting for updates…"); @@ -114,8 +112,8 @@ async fn test_toggling_reaction() -> Result<()> { while let Some(diff) = stream.next().await { warn!(?diff, "received a diff"); diff.apply(&mut items); - if let Some(pair) = find_event_id(&items) { - return Ok(pair); + if let Some(event_id) = find_event_id(&items) { + return Ok(event_id); } } @@ -130,7 +128,7 @@ async fn test_toggling_reaction() -> Result<()> { debug!("Sending initial message…"); timeline.send(RoomMessageEventContent::text_plain("hi!").into()).await.unwrap(); - let (msg_uid, event_id) = timeout(Duration::from_secs(10), event_id_task) + let event_id = timeout(Duration::from_secs(10), event_id_task) .await .expect("timeout") .expect("failed to join tokio task") @@ -150,10 +148,13 @@ async fn test_toggling_reaction() -> Result<()> { diff.apply(&mut items); } - let message_position = items + let (message_position, item_id) = items .iter() .enumerate() - .find_map(|(i, item)| (item.as_event()?.event_id()? == event_id).then_some(i)) + .find_map(|(i, item)| { + let item = item.as_event()?; + (item.event_id()? == event_id).then_some((i, item.identifier())) + }) .expect("couldn't find the final position for the event id"); let reaction_key = "👍".to_owned(); @@ -163,7 +164,7 @@ async fn test_toggling_reaction() -> Result<()> { debug!("Starting the toggle reaction tests…"); // Add the reaction. - timeline.toggle_reaction(&msg_uid, &reaction_key).await.expect("toggling reaction"); + timeline.toggle_reaction(&item_id, &reaction_key).await.expect("toggling reaction"); // Local echo is added. { @@ -192,7 +193,7 @@ async fn test_toggling_reaction() -> Result<()> { // Redact the reaction. timeline - .toggle_reaction(&msg_uid, &reaction_key) + .toggle_reaction(&item_id, &reaction_key) .await .expect("toggling reaction the second time"); From 1552426961542a222ee2ff8c49eeb26a87d3129a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 15 Oct 2024 15:29:50 +0200 Subject: [PATCH 292/979] timeline: get rid of conversions from string to `TimelineEventItemId` I suppose these were useful at the FFI layer at some point, but they aren't anymore, so they could be removed. Changelog: Got rid of `From` for `TimelineEventItemId`. --- .../src/timeline/event_item/mod.rs | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) 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 62984c09d8d..67ee1299029 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -103,22 +103,6 @@ pub enum TimelineEventItemId { EventId(OwnedEventId), } -impl From for TimelineEventItemId { - fn from(value: String) -> Self { - value.as_str().into() - } -} - -impl From<&str> for TimelineEventItemId { - fn from(value: &str) -> Self { - if let Ok(event_id) = EventId::parse(value) { - TimelineEventItemId::EventId(event_id) - } else { - TimelineEventItemId::TransactionId(value.into()) - } - } -} - /// An handle that usually allows to perform an action on a timeline event. /// /// If the item represents a remote item, then the event id is usually @@ -749,7 +733,7 @@ mod tests { }; use super::{EventTimelineItem, Profile}; - use crate::timeline::{TimelineDetails, TimelineEventItemId}; + use crate::timeline::TimelineDetails; #[async_test] async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() { @@ -974,20 +958,6 @@ mod tests { ); } - #[test] - fn test_raw_event_id_into_timeline_event_item_id_gets_event_id() { - let raw_id = "$123:example.com"; - let id: TimelineEventItemId = raw_id.into(); - assert_matches!(id, TimelineEventItemId::EventId(_)); - } - - #[test] - fn test_raw_str_into_timeline_event_item_id_gets_transaction_id() { - let raw_id = "something something"; - let id: TimelineEventItemId = raw_id.into(); - assert_matches!(id, TimelineEventItemId::TransactionId(_)); - } - fn member_event( room_id: &RoomId, user_id: &UserId, From 8df5d655c01342ec80f012b75532f0de3710735d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 12:02:26 +0200 Subject: [PATCH 293/979] feat(multiverse): add support to toggle a reaction on the last message of a room --- labs/multiverse/src/main.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index d370bc4b928..e844449cec7 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -357,6 +357,41 @@ impl App { } } + async fn toggle_reaction_to_latest_msg(&mut self) { + let selected = self.get_selected_room_id(None); + + if let Some((sdk_timeline, items)) = selected.and_then(|room_id| { + self.timelines + .lock() + .unwrap() + .get(&room_id) + .map(|timeline| (timeline.timeline.clone(), timeline.items.clone())) + }) { + // Look for the latest (most recent) room message. + let item_id = { + let items = items.lock().unwrap(); + items.iter().rev().find_map(|it| { + it.as_event() + .and_then(|ev| ev.content().as_message().is_some().then(|| ev.identifier())) + }) + }; + + // If found, send a reaction. + if let Some(item_id) = item_id { + match sdk_timeline.toggle_reaction(&item_id, "🥰").await { + Ok(_) => { + self.set_status_message("reaction sent!".to_owned()); + } + Err(err) => self.set_status_message(format!("error when reacting: {err}")), + } + } else { + self.set_status_message("no item to react to".to_owned()); + } + } else { + self.set_status_message("missing timeline for room".to_owned()); + }; + } + /// Run a small back-pagination (expect a batch of 20 events, continue until /// we get 10 timeline items or hit the timeline start). fn back_paginate(&mut self) { @@ -484,6 +519,8 @@ impl App { }; } + Char('l') => self.toggle_reaction_to_latest_msg().await, + Char('r') => self.details_mode = DetailsMode::ReadReceipts, Char('t') => self.details_mode = DetailsMode::TimelineItems, Char('e') => self.details_mode = DetailsMode::Events, From 30f3a3c2e458eb0cb595ad8b8f02c9957077e98b Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 12:37:28 +0200 Subject: [PATCH 294/979] chore(timeline): fix instrumentation of `update_event_send_state` This would not report the `txn_id` field because of the `skip_all`. It's actually interesting to also get the error, so I'm only skipping self from now on. --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 94ff0944459..930facfa69b 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -735,7 +735,7 @@ impl TimelineController

{ /// /// If the corresponding local timeline item is missing, a warning is /// raised. - #[instrument(skip_all, fields(txn_id))] + #[instrument(skip(self))] pub(super) async fn update_event_send_state( &self, txn_id: &TransactionId, From 664f6d5f5abb9da4ecf157169603632c57636958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 9 Oct 2024 16:20:42 +0200 Subject: [PATCH 295/979] feat(knocking): add code to process knocked rooms separately during sync --- crates/matrix-sdk-base/src/client.rs | 83 +++++-- crates/matrix-sdk-base/src/debug.rs | 19 +- .../matrix-sdk-base/src/sliding_sync/mod.rs | 209 +++++++++++++++--- crates/matrix-sdk-base/src/sync.rs | 17 +- crates/matrix-sdk/src/sync.rs | 34 ++- crates/matrix-sdk/tests/integration/client.rs | 17 +- testing/matrix-sdk-test/src/test_json/sync.rs | 29 ++- 7 files changed, 348 insertions(+), 60 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index e39587455ab..2e3835a8cee 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -523,11 +523,22 @@ impl BaseClient { Ok(timeline) } + /// Handles the stripped state events in `invite_state`, modifying the + /// room's info and posting notifications as needed. + /// + /// * `room` - The [`Room`] to modify. + /// * `events` - The contents of `invite_state` in the form of list of pairs + /// of raw stripped state events with their deserialized counterpart. + /// * `push_rules` - The push rules for this room. + /// * `room_info` - The current room's info. + /// * `changes` - The accumulated list of changes to apply once the + /// processing is finished. + /// * `notifications` - Notifications to post for the current room. #[instrument(skip_all, fields(room_id = ?room_info.room_id))] pub(crate) async fn handle_invited_state( &self, room: &Room, - events: &[Raw], + events: &[(Raw, AnyStrippedStateEvent)], push_rules: &Ruleset, room_info: &mut RoomInfo, changes: &mut StateChanges, @@ -535,22 +546,12 @@ impl BaseClient { ) -> Result<()> { let mut state_events = BTreeMap::new(); - for raw_event in events { - match raw_event.deserialize() { - Ok(e) => { - room_info.handle_stripped_state_event(&e); - state_events - .entry(e.event_type()) - .or_insert_with(BTreeMap::new) - .insert(e.state_key().to_owned(), raw_event.clone()); - } - Err(err) => { - warn!( - room_id = ?room_info.room_id, - "Couldn't deserialize stripped state event: {err:?}", - ); - } - } + for (raw_event, event) in events { + room_info.handle_stripped_state_event(event); + state_events + .entry(event.event_type()) + .or_insert_with(BTreeMap::new) + .insert(event.state_key().to_owned(), raw_event.clone()); } changes.stripped_state.insert(room_info.room_id().to_owned(), state_events.clone()); @@ -1121,13 +1122,16 @@ impl BaseClient { self.room_info_notable_update_sender.clone(), ); + let invite_state = + Self::deserialize_stripped_state_events(&new_info.invite_state.events); + let mut room_info = room.clone_info(); room_info.mark_as_invited(); room_info.mark_state_fully_synced(); self.handle_invited_state( &room, - &new_info.invite_state.events, + &invite_state, &push_rules, &mut room_info, &mut changes, @@ -1140,6 +1144,34 @@ impl BaseClient { new_rooms.invite.insert(room_id, new_info); } + for (room_id, new_info) in response.rooms.knock { + let room = self.store.get_or_create_room( + &room_id, + RoomState::Knocked, + self.room_info_notable_update_sender.clone(), + ); + + let knock_state = Self::deserialize_stripped_state_events(&new_info.knock_state.events); + + let mut room_info = room.clone_info(); + room_info.mark_as_knocked(); + room_info.mark_state_fully_synced(); + + self.handle_invited_state( + &room, + &knock_state, + &push_rules, + &mut room_info, + &mut changes, + &mut notifications, + ) + .await?; + + changes.add_room(room_info); + + new_rooms.knocked.insert(room_id, new_info); + } + account_data_processor.apply(&mut changes, &self.store).await; changes.presence = response @@ -1582,6 +1614,21 @@ impl BaseClient { .collect() } + pub(crate) fn deserialize_stripped_state_events( + raw_events: &[Raw], + ) -> Vec<(Raw, AnyStrippedStateEvent)> { + raw_events + .iter() + .filter_map(|raw_event| match raw_event.deserialize() { + Ok(event) => Some((raw_event.clone(), event)), + Err(e) => { + warn!("Couldn't deserialize stripped state event: {e}"); + None + } + }) + .collect() + } + /// Returns a new receiver that gets future room info notable updates. /// /// Learn more by reading the [`RoomInfoNotableUpdate`] type. diff --git a/crates/matrix-sdk-base/src/debug.rs b/crates/matrix-sdk-base/src/debug.rs index d207eccef10..95035c1c4f7 100644 --- a/crates/matrix-sdk-base/src/debug.rs +++ b/crates/matrix-sdk-base/src/debug.rs @@ -17,7 +17,10 @@ use std::fmt; pub use matrix_sdk_common::debug::*; -use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw}; +use ruma::{ + api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom}, + serde::Raw, +}; /// A wrapper around a slice of `Raw` events that implements `Debug` in a way /// that only prints the event type of each item. @@ -46,6 +49,20 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> { } } +/// A wrapper around a knocked on room as found in `/sync` responses that +/// implements `Debug` in a way that only prints the event ID and event type for +/// the raw events contained in `knock_state`. +pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom); + +#[cfg(not(tarpaulin_include))] +impl<'a> fmt::Debug for DebugKnockedRoom<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KnockedRoom") + .field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events)) + .finish() + } +} + pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw]); #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 334336e3ba7..819d1662eec 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -27,10 +27,13 @@ use ruma::api::client::sync::sync_events::v5; #[cfg(feature = "e2e-encryption")] use ruma::events::AnyToDeviceEvent; use ruma::{ - api::client::sync::sync_events::v3::{self, InvitedRoom}, - events::{AnyRoomAccountDataEvent, AnySyncStateEvent}, + api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom}, + events::{ + room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent, + AnySyncStateEvent, + }, serde::Raw, - JsOption, OwnedRoomId, RoomId, UInt, + JsOption, OwnedRoomId, RoomId, UInt, UserId, }; use tracing::{debug, error, instrument, trace, warn}; @@ -47,6 +50,7 @@ use crate::{ normal::{RoomHero, RoomInfoNotableUpdateReasons}, RoomState, }, + ruma::assign, store::{ambiguity_map::AmbiguityCache, StateChanges, Store}, sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse}, Room, RoomInfo, @@ -167,13 +171,20 @@ impl BaseClient { let mut notifications = Default::default(); let mut rooms_account_data = extensions.account_data.rooms.clone(); + let user_id = self + .session_meta() + .expect("Sliding sync shouldn't run without an authenticated user.") + .user_id + .to_owned(); + for (room_id, response_room_data) in rooms { - let (room_info, joined_room, left_room, invited_room) = self + let (room_info, joined_room, left_room, invited_room, knocked_room) = self .process_sliding_sync_room( room_id, response_room_data, &mut rooms_account_data, &store, + &user_id, &account_data_processor, &mut changes, &mut room_info_notable_updates, @@ -196,6 +207,10 @@ impl BaseClient { if let Some(invited_room) = invited_room { new_rooms.invite.insert(room_id.clone(), invited_room); } + + if let Some(knocked_room) = knocked_room { + new_rooms.knocked.insert(room_id.clone(), knocked_room); + } } // Handle read receipts and typing notifications independently of the rooms: @@ -341,14 +356,20 @@ impl BaseClient { room_data: &http::response::Room, rooms_account_data: &mut BTreeMap>>, store: &Store, + user_id: &UserId, account_data_processor: &AccountDataProcessor, changes: &mut StateChanges, room_info_notable_updates: &mut BTreeMap, notifications: &mut BTreeMap>, ambiguity_cache: &mut AmbiguityCache, with_msc4186: bool, - ) -> Result<(RoomInfo, Option, Option, Option)> - { + ) -> Result<( + RoomInfo, + Option, + Option, + Option, + Option, + )> { // This method may change `room_data` (see the terrible hack describes below) // with `timestamp` and `invite_state. We don't want to change the `room_data` // from outside this method, hence `Cow` is perfectly suited here. @@ -402,14 +423,22 @@ impl BaseClient { } } + let stripped_state: Option, AnyStrippedStateEvent)>> = + room_data + .invite_state + .as_ref() + .map(|invite_state| Self::deserialize_stripped_state_events(invite_state)); + #[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.as_ref(), - &state_events, - store, - room_id, - room_info_notable_updates, - ); + let (mut room, mut room_info, invited_room, knocked_room) = self + .process_sliding_sync_room_membership( + &state_events, + stripped_state.as_ref(), + store, + user_id, + room_id, + room_info_notable_updates, + ); room_info.mark_state_partially_synced(); @@ -428,7 +457,8 @@ impl BaseClient { let push_rules = self.get_push_rules(account_data_processor).await?; - if let Some(invite_state) = &room_data.invite_state { + // This will be used for both invited and knocked rooms. + if let Some(invite_state) = &stripped_state { self.handle_invited_state( &room, invite_state, @@ -512,6 +542,7 @@ impl BaseClient { )), None, None, + None, )) } @@ -525,12 +556,12 @@ impl BaseClient { ambiguity_changes, )), None, + None, )), - RoomState::Invited => Ok((room_info, None, None, invited_room)), + RoomState::Invited => Ok((room_info, None, None, invited_room, None)), - // TODO: implement special logic for retrieving the knocked room info - RoomState::Knocked => Ok((room_info, None, None, None)), + RoomState::Knocked => Ok((room_info, None, None, None, knocked_room)), } } @@ -541,13 +572,14 @@ impl BaseClient { /// otherwise. https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#room-list-parameters fn process_sliding_sync_room_membership( &self, - room_data: &http::response::Room, state_events: &[AnySyncStateEvent], + stripped_state: Option<&Vec<(Raw, AnyStrippedStateEvent)>>, store: &Store, + user_id: &UserId, room_id: &RoomId, room_info_notable_updates: &mut BTreeMap, - ) -> (Room, RoomInfo, Option) { - if let Some(invite_state) = &room_data.invite_state { + ) -> (Room, RoomInfo, Option, Option) { + if let Some(stripped_state) = stripped_state { let room = store.get_or_create_room( room_id, RoomState::Invited, @@ -555,20 +587,34 @@ impl BaseClient { ); let mut room_info = room.clone_info(); - // We don't actually know what events are inside invite_state. In theory, they - // might not contain an m.room.member event, or they might set the - // membership to something other than invite. This would be very - // weird behaviour by the server, because invite_state is supposed - // to contain an m.room.member. We will call handle_invited_state, which will - // reflect any information found in the real events inside - // invite_state, but we default to considering this room invited - // simply because invite_state exists. This is needed in the normal - // case, because the sliding sync server tries to send minimal state, - // meaning that we normally actually just receive {"type": "m.room.member"} with - // no content at all. - room_info.mark_as_invited(); + // We need to find the membership event since it could be for either an invited + // or knocked room + let membership_event_content = stripped_state.iter().find_map(|(_, event)| { + if let AnyStrippedStateEvent::RoomMember(membership_event) = event { + if membership_event.state_key == user_id { + return Some(membership_event.content.clone()); + } + } + None + }); + + if let Some(membership_event_content) = membership_event_content { + if membership_event_content.membership == MembershipState::Knock { + // If we have a `Knock` membership state, set the room as such + room_info.mark_as_knocked(); + let raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect(); + let knock_state = assign!(v3::KnockState::default(), { events: raw_events }); + let knocked_room = + assign!(KnockedRoom::default(), { knock_state: knock_state }); + return (room, room_info, None, Some(knocked_room)); + } + } - (room, room_info, Some(InvitedRoom::from(v3::InviteState::from(invite_state.clone())))) + // Otherwise assume it's an invited room + room_info.mark_as_invited(); + let raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect::>(); + let invited_room = InvitedRoom::from(v3::InviteState::from(raw_events)); + (room, room_info, Some(invited_room), None) } else { let room = store.get_or_create_room( room_id, @@ -594,7 +640,7 @@ impl BaseClient { room_info_notable_updates, ); - (room, room_info, None) + (room, room_info, None, None) } } @@ -958,6 +1004,7 @@ mod tests { assert!(sync_resp.rooms.join.contains_key(room_id)); assert!(!sync_resp.rooms.leave.contains_key(room_id)); assert!(!sync_resp.rooms.invite.contains_key(room_id)); + assert!(!sync_resp.rooms.knocked.contains_key(room_id)); } #[async_test] @@ -1014,6 +1061,7 @@ mod tests { assert!(!sync_resp.rooms.join.contains_key(room_id)); assert!(!sync_resp.rooms.leave.contains_key(room_id)); assert!(sync_resp.rooms.invite.contains_key(room_id)); + assert!(!sync_resp.rooms.knocked.contains_key(room_id)); } #[async_test] @@ -1042,6 +1090,78 @@ mod tests { assert_eq!(client_room.compute_display_name().await.unwrap().to_string(), "The Name"); } + #[async_test] + async fn test_receiving_a_knocked_room_membership_event_creates_a_knocked_room() { + // Given a logged-in client, + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + let user_id = client.session_meta().unwrap().user_id.to_owned(); + + // When the room is properly set as knocked with the current user id as state + // key, + let mut room = http::response::Room::new(); + set_room_knocked(&mut room, &user_id); + + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // The room is knocked. + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!(client_room.state(), RoomState::Knocked); + } + + #[async_test] + async fn test_receiving_a_knocked_room_membership_event_with_wrong_state_key_creates_an_invited_room( + ) { + // Given a logged-in client, + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + let user_id = user_id!("@w:e.uk"); + + // When the room is set as knocked with a random user id as state key, + let mut room = http::response::Room::new(); + set_room_knocked(&mut room, user_id); + + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // The room is invited since the membership event doesn't belong to the current + // user. + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!(client_room.state(), RoomState::Invited); + } + + #[async_test] + async fn test_receiving_an_unknown_room_membership_event_in_invite_state_creates_an_invited_room( + ) { + // Given a logged-in client, + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + let user_id = client.session_meta().unwrap().user_id.to_owned(); + + // When the room has the wrong membership state in its invite_state + let mut room = http::response::Room::new(); + let event = Raw::new(&json!({ + "type": "m.room.member", + "sender": user_id, + "content": { + "is_direct": true, + "membership": "join", + }, + "state_key": user_id, + })) + .expect("Failed to make raw event") + .cast(); + room.invite_state = Some(vec![event]); + + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // The room is marked as invited. + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!(client_room.state(), RoomState::Invited); + } + #[async_test] async fn test_left_a_room_from_required_state_event() { // Given a logged-in client @@ -1072,6 +1192,7 @@ mod tests { assert!(!sync_resp.rooms.join.contains_key(room_id)); assert!(sync_resp.rooms.leave.contains_key(room_id)); assert!(!sync_resp.rooms.invite.contains_key(room_id)); + assert!(!sync_resp.rooms.knocked.contains_key(room_id)); } #[async_test] @@ -1113,6 +1234,7 @@ mod tests { assert!(!sync_resp.rooms.join.contains_key(room_id)); assert!(sync_resp.rooms.leave.contains_key(room_id)); assert!(!sync_resp.rooms.invite.contains_key(room_id)); + assert!(!sync_resp.rooms.knocked.contains_key(room_id)); } } @@ -2555,6 +2677,25 @@ mod tests { )); } + fn set_room_knocked(room: &mut http::response::Room, knocker: &UserId) { + // MSC3575 shows an almost-empty event to indicate that we are invited to a + // room. Just the type is supplied. + + let evt = Raw::new(&json!({ + "type": "m.room.member", + "sender": knocker, + "content": { + "is_direct": true, + "membership": "knock", + }, + "state_key": knocker, + })) + .expect("Failed to make raw event") + .cast(); + + room.invite_state = Some(vec![evt]); + } + fn set_room_joined(room: &mut http::response::Room, user_id: &UserId) { room.required_state.push(make_membership_event(user_id, MembershipState::Join)); } diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index d9015e72ea7..4d47cd904ce 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -19,7 +19,7 @@ use std::{collections::BTreeMap, fmt}; use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent}; use ruma::{ api::client::sync::sync_events::{ - v3::InvitedRoom as InvitedRoomUpdate, + v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom}, UnreadNotificationsCount as RumaUnreadNotificationsCount, }, events::{ @@ -33,7 +33,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use crate::{ - debug::{DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId}, + debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId}, deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent}, store::Store, }; @@ -77,6 +77,8 @@ pub struct RoomUpdates { pub join: BTreeMap, /// The rooms that the user has been invited to. pub invite: BTreeMap, + /// The rooms that the user has knocked on. + pub knocked: BTreeMap, } impl RoomUpdates { @@ -89,6 +91,7 @@ impl RoomUpdates { .keys() .chain(self.join.keys()) .chain(self.invite.keys()) + .chain(self.knocked.keys()) .filter_map(|room_id| store.room(room_id)) { let _ = room.compute_display_name().await; @@ -103,6 +106,7 @@ impl fmt::Debug for RoomUpdates { .field("leave", &self.leave) .field("join", &self.join) .field("invite", &DebugInvitedRoomUpdates(&self.invite)) + .field("knocked", &DebugKnockedRoomUpdates(&self.knocked)) .finish() } } @@ -250,6 +254,15 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> { } } +struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap); + +#[cfg(not(tarpaulin_include))] +impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish() + } +} + /// A notification triggered by a sync response. #[derive(Clone)] pub struct Notification { diff --git a/crates/matrix-sdk/src/sync.rs b/crates/matrix-sdk/src/sync.rs index a9f490aa976..5a824ebf845 100644 --- a/crates/matrix-sdk/src/sync.rs +++ b/crates/matrix-sdk/src/sync.rs @@ -22,11 +22,14 @@ use std::{ pub use matrix_sdk_base::sync::*; use matrix_sdk_base::{ - debug::{DebugInvitedRoom, DebugListOfRawEventsNoId}, + debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEventsNoId}, sync::SyncResponse as BaseSyncResponse, }; use ruma::{ - api::client::sync::sync_events::{self, v3::InvitedRoom}, + api::client::sync::sync_events::{ + self, + v3::{InvitedRoom, KnockedRoom}, + }, events::{presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyToDeviceEvent}, serde::Raw, time::Instant, @@ -100,6 +103,13 @@ pub enum RoomUpdate { /// Updates to the room. updates: InvitedRoom, }, + /// Updates to a room the user knocked on. + Knocked { + /// Room object with general information on the room. + room: Room, + /// Updates to the room. + updates: KnockedRoom, + }, } #[cfg(not(tarpaulin_include))] @@ -117,6 +127,11 @@ impl fmt::Debug for RoomUpdate { .field("room", room) .field("updates", &DebugInvitedRoom(updates)) .finish(), + Self::Knocked { room, updates } => f + .debug_struct("Knocked") + .field("room", room) + .field("updates", &DebugKnockedRoom(updates)) + .finish(), } } } @@ -225,6 +240,21 @@ impl Client { self.handle_sync_events(HandlerKind::StrippedState, Some(&room), invite_state).await?; } + for (room_id, room_info) in &rooms.knocked { + let Some(room) = self.get_room(room_id) else { + error!(?room_id, "Can't call event handler, room not found"); + continue; + }; + + self.send_room_update(room_id, || RoomUpdate::Knocked { + room: room.clone(), + updates: room_info.clone(), + }); + + let knock_state = &room_info.knock_state.events; + self.handle_sync_events(HandlerKind::StrippedState, Some(&room), knock_state).await?; + } + debug!("Ran event handlers in {:?}", now.elapsed()); let now = Instant::now(); diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 3dd739d751f..6e044c28567 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -15,7 +15,10 @@ use matrix_sdk_test::{ async_test, sync_state_event, test_json::{ self, - sync::{MIXED_INVITED_ROOM_ID, MIXED_JOINED_ROOM_ID, MIXED_LEFT_ROOM_ID, MIXED_SYNC}, + sync::{ + MIXED_INVITED_ROOM_ID, MIXED_JOINED_ROOM_ID, MIXED_KNOCKED_ROOM_ID, MIXED_LEFT_ROOM_ID, + MIXED_SYNC, + }, sync_events::PINNED_EVENTS, TAG, }, @@ -340,7 +343,7 @@ async fn test_subscribe_all_room_updates() { client.sync_once(sync_settings).await.unwrap(); let room_updates = rx.recv().now_or_never().unwrap().unwrap(); - assert_let!(RoomUpdates { leave, join, invite } = room_updates); + assert_let!(RoomUpdates { leave, join, invite, knocked } = room_updates); // Check the left room updates. { @@ -383,6 +386,16 @@ async fn test_subscribe_all_room_updates() { assert_eq!(room_id, *MIXED_INVITED_ROOM_ID); assert_eq!(update.invite_state.events.len(), 2); } + + // Check the knocked room updates. + { + assert_eq!(knocked.len(), 1); + + let (room_id, update) = knocked.iter().next().unwrap(); + + assert_eq!(room_id, *MIXED_KNOCKED_ROOM_ID); + assert_eq!(update.knock_state.events.len(), 2); + } } // Check that the `Room::is_encrypted()` is properly deduplicated, meaning we diff --git a/testing/matrix-sdk-test/src/test_json/sync.rs b/testing/matrix-sdk-test/src/test_json/sync.rs index 02a4ea12a83..8ad287bb292 100644 --- a/testing/matrix-sdk-test/src/test_json/sync.rs +++ b/testing/matrix-sdk-test/src/test_json/sync.rs @@ -1240,8 +1240,11 @@ pub static MIXED_LEFT_ROOM_ID: Lazy<&RoomId> = /// In the [`MIXED_SYNC`], the room id of the invited room. pub static MIXED_INVITED_ROOM_ID: Lazy<&RoomId> = Lazy::new(|| room_id!("!SVkFJHzfwvuaIEawgE:localhost")); +/// In the [`MIXED_SYNC`], the room id of the knocked room. +pub static MIXED_KNOCKED_ROOM_ID: Lazy<&RoomId> = + Lazy::new(|| room_id!("!SVkFJHzfwvuaIEawgF:localhost")); -/// A sync that contains updates to joined/invited/left rooms. +/// A sync that contains updates to joined/invited/knocked/left rooms. pub static MIXED_SYNC: Lazy = Lazy::new(|| { json!({ "account_data": { @@ -1357,6 +1360,30 @@ pub static MIXED_SYNC: Lazy = Lazy::new(|| { } } }, + "knock": { + *MIXED_KNOCKED_ROOM_ID: { + "knock_state": { + "events": [ + { + "sender": "@alice:example.com", + "type": "m.room.name", + "state_key": "", + "content": { + "name": "My Room Name" + } + }, + { + "sender": "@bob:example.com", + "type": "m.room.member", + "state_key": "@bob:example.com", + "content": { + "membership": "knock" + } + } + ] + } + } + }, "leave": { *MIXED_LEFT_ROOM_ID: { "timeline": { From 962a78ab1358fad312857627ab68e8f0da7c33a3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 9 Oct 2024 17:24:47 +0200 Subject: [PATCH 296/979] chore(timeline): always increment the unique id to avoid issues with stall IDs across timeline clears --- .../src/timeline/controller/state.rs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 41f4f163b16..47fd36aa155 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -623,11 +623,7 @@ impl TimelineStateTransaction<'_> { self.items.clear(); } - // Only clear the internal counter if there are no local echoes. Otherwise, we - // might end up reusing the same internal id for a local echo and - // another item. - let reset_internal_id = !has_local_echoes; - self.meta.clear(reset_internal_id); + self.meta.clear(); debug!(remaining_items = self.items.len(), "Timeline cleared"); } @@ -864,6 +860,13 @@ pub(in crate::timeline) struct TimelineMetadata { // **** DYNAMIC FIELDS **** /// The next internal identifier for timeline items, used for both local and /// remote echoes. + /// + /// This is never cleared, but always incremented, to avoid issues with + /// reusing a stale internal id across timeline clears. We don't expect + /// we can hit `u64::max_value()` realistically, but if this would + /// happen, we do a wrapping addition when incrementing this + /// id; the previous 0 value would have disappeared a long time ago, unless + /// the device has terabytes of RAM. next_internal_id: u64, /// List of all the events as received in the timeline, even the ones that @@ -929,10 +932,9 @@ impl TimelineMetadata { } } - pub(crate) fn clear(&mut self, reset_internal_id: bool) { - if reset_internal_id { - self.next_internal_id = 0; - } + pub(crate) fn clear(&mut self) { + // Note: we don't clear the next internal id to avoid bad cases of stale unique + // ids across timeline clears. self.all_events.clear(); self.reactions.clear(); self.pending_poll_events.clear(); @@ -977,7 +979,7 @@ impl TimelineMetadata { /// internal counter). fn next_internal_id(&mut self) -> String { let val = self.next_internal_id; - self.next_internal_id += 1; + self.next_internal_id = self.next_internal_id.wrapping_add(1); let prefix = self.internal_id_prefix.as_deref().unwrap_or(""); format!("{prefix}{val}") } From 4f49b237515b1c365aeea113a3302a311a435ee4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 9 Oct 2024 15:15:44 +0200 Subject: [PATCH 297/979] refactor(timeline): introduce `TimelineUniqueId` as an opaque type for the unique identifier We can now use this type instead of passing a string, which means there's no way to confuse oneself in methods like `toggle_reaction_local`. Changelog: Introduced `TimelineUniqueId`, returned by `TimelineItem::unique_id()` and serving as an opaque identifier to use in other methods modifying the timeline item (e.g. `toggle_reaction`). --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 23 +++++++++++++++++-- .../src/timeline/controller/mod.rs | 3 ++- .../src/timeline/controller/state.rs | 5 ++-- crates/matrix-sdk-ui/src/timeline/item.rs | 19 +++++++++++---- crates/matrix-sdk-ui/src/timeline/mod.rs | 2 +- .../matrix-sdk-ui/src/timeline/tests/basic.rs | 20 ++++++++-------- .../matrix-sdk-ui/src/timeline/tests/echo.rs | 6 ++--- crates/matrix-sdk-ui/src/timeline/util.rs | 10 ++++---- .../tests/integration/timeline/replies.rs | 8 +++---- 9 files changed, 64 insertions(+), 32 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 66d686fcb49..0dd994da43d 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -32,6 +32,7 @@ use matrix_sdk::{ }; use matrix_sdk_ui::timeline::{ EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails, + TimelineUniqueId as SdkTimelineUniqueId, }; use mime::Mime; use ruma::{ @@ -868,6 +869,23 @@ pub enum TimelineChange { Reset, } +#[derive(Clone, uniffi::Record)] +pub struct TimelineUniqueId { + id: String, +} + +impl From<&SdkTimelineUniqueId> for TimelineUniqueId { + fn from(value: &SdkTimelineUniqueId) -> Self { + Self { id: value.0.clone() } + } +} + +impl From<&TimelineUniqueId> for SdkTimelineUniqueId { + fn from(value: &TimelineUniqueId) -> Self { + Self(value.id.clone()) + } +} + #[repr(transparent)] #[derive(Clone, uniffi::Object)] pub struct TimelineItem(pub(crate) matrix_sdk_ui::timeline::TimelineItem); @@ -895,8 +913,9 @@ impl TimelineItem { } } - pub fn unique_id(&self) -> String { - self.0.unique_id().to_owned() + /// An opaque unique identifier for this timeline item. + pub fn unique_id(&self) -> TimelineUniqueId { + self.0.unique_id().into() } pub fn fmt_debug(&self) -> String { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 930facfa69b..6dbab04a5db 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -58,6 +58,7 @@ pub(super) use self::state::{ use super::{ event_handler::TimelineEventKind, event_item::{ReactionStatus, RemoteEventOrigin}, + item::TimelineUniqueId, traits::{Decryptor, RoomDataProvider}, util::{rfind_event_by_id, rfind_event_item, RelativePosition}, Error, EventSendState, EventTimelineItem, InReplyToDetails, Message, PaginationError, Profile, @@ -1541,7 +1542,7 @@ async fn fetch_replied_to_event( mut state: RwLockWriteGuard<'_, TimelineState>, index: usize, item: &EventTimelineItem, - internal_id: String, + internal_id: TimelineUniqueId, message: &Message, in_reply_to: &EventId, room: &Room, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 47fd36aa155..4f23f58a426 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -54,6 +54,7 @@ use crate::{ TimelineItemPosition, }, event_item::{PollState, RemoteEventOrigin, ResponseData}, + item::TimelineUniqueId, reactions::Reactions, read_receipts::ReadReceipts, traits::RoomDataProvider, @@ -977,11 +978,11 @@ impl TimelineMetadata { /// Returns the next internal id for a timeline item (and increment our /// internal counter). - fn next_internal_id(&mut self) -> String { + fn next_internal_id(&mut self) -> TimelineUniqueId { let val = self.next_internal_id; self.next_internal_id = self.next_internal_id.wrapping_add(1); let prefix = self.internal_id_prefix.as_deref().unwrap_or(""); - format!("{prefix}{val}") + TimelineUniqueId(format!("{prefix}{val}")) } /// Returns a new timeline item with a fresh internal id. diff --git a/crates/matrix-sdk-ui/src/timeline/item.rs b/crates/matrix-sdk-ui/src/timeline/item.rs index ba85a99efe4..8096da48c1a 100644 --- a/crates/matrix-sdk-ui/src/timeline/item.rs +++ b/crates/matrix-sdk-ui/src/timeline/item.rs @@ -18,6 +18,14 @@ use as_variant::as_variant; use super::{EventTimelineItem, VirtualTimelineItem}; +/// Opaque unique identifier for a timeline item. +/// +/// It is transferred whenever a timeline item is updated. This can be used as a +/// stable identifier for UI purposes, as well as operations on the event +/// represented by the item. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct TimelineUniqueId(pub String); + #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum TimelineItemKind { @@ -32,12 +40,15 @@ pub enum TimelineItemKind { #[derive(Clone, Debug)] pub struct TimelineItem { pub(crate) kind: TimelineItemKind, - pub(crate) internal_id: String, + pub(crate) internal_id: TimelineUniqueId, } impl TimelineItem { /// Create a new `TimelineItem` with the given kind and internal id. - pub(crate) fn new(kind: impl Into, internal_id: String) -> Arc { + pub(crate) fn new( + kind: impl Into, + internal_id: TimelineUniqueId, + ) -> Arc { Arc::new(TimelineItem { kind: kind.into(), internal_id }) } @@ -71,14 +82,14 @@ impl TimelineItem { /// dividers, identity isn't easy to define though and you might /// see a new ID getting generated for a day divider that you /// perceive to be "the same" as a previous one. - pub fn unique_id(&self) -> &str { + pub fn unique_id(&self) -> &TimelineUniqueId { &self.internal_id } pub(crate) fn read_marker() -> Arc { Arc::new(Self { kind: TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker), - internal_id: "__read_marker".to_owned(), + internal_id: TimelineUniqueId("__read_marker".to_owned()), }) } diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index c364fe7cf86..35adf7e875a 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -89,7 +89,7 @@ pub use self::{ Sticker, TimelineDetails, TimelineEventItemId, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, - item::{TimelineItem, TimelineItemKind}, + item::{TimelineItem, TimelineItemKind, TimelineUniqueId}, pagination::LiveBackPaginationStatus, traits::RoomExt, virtual_item::VirtualTimelineItem, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index d0149b7b3a3..bf613932fdd 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -330,16 +330,16 @@ async fn test_dedup_initial() { let event2 = &timeline_items[2]; let event3 = &timeline_items[3]; - // Make sure the order is right + // Make sure the order is right. assert_eq!(event1.as_event().unwrap().sender(), *ALICE); assert_eq!(event2.as_event().unwrap().sender(), *BOB); assert_eq!(event3.as_event().unwrap().sender(), *CAROL); - // Make sure we reused IDs when deduplicating events - assert_eq!(event1.unique_id(), "0"); - assert_eq!(event2.unique_id(), "1"); - assert_eq!(event3.unique_id(), "2"); - assert_eq!(timeline_items[0].unique_id(), "3"); + // Make sure we reused IDs when deduplicating events. + assert_eq!(event1.unique_id().0, "0"); + assert_eq!(event2.unique_id().0, "1"); + assert_eq!(event3.unique_id().0, "2"); + assert_eq!(timeline_items[0].unique_id().0, "3"); } #[async_test] @@ -360,19 +360,19 @@ async fn test_internal_id_prefix() { assert_eq!(timeline_items.len(), 4); assert!(timeline_items[0].is_day_divider()); - assert_eq!(timeline_items[0].unique_id(), "le_prefix_3"); + assert_eq!(timeline_items[0].unique_id().0, "le_prefix_3"); let event1 = &timeline_items[1]; assert_eq!(event1.as_event().unwrap().sender(), *ALICE); - assert_eq!(event1.unique_id(), "le_prefix_0"); + assert_eq!(event1.unique_id().0, "le_prefix_0"); let event2 = &timeline_items[2]; assert_eq!(event2.as_event().unwrap().sender(), *BOB); - assert_eq!(event2.unique_id(), "le_prefix_1"); + assert_eq!(event2.unique_id().0, "le_prefix_1"); let event3 = &timeline_items[3]; assert_eq!(event3.as_event().unwrap().sender(), *CAROL); - assert_eq!(event3.unique_id(), "le_prefix_2"); + assert_eq!(event3.unique_id().0, "le_prefix_2"); } #[async_test] diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index d8d8b56f6d8..fea31476c78 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -85,7 +85,7 @@ async fn test_remote_echo_full_trip() { event_item.send_state(), Some(EventSendState::SendingFailed { is_recoverable: true, .. }) ); - assert_eq!(item.unique_id(), id); + assert_eq!(*item.unique_id(), id); } // Scenario 3: The local event has been sent successfully to the server and an @@ -104,7 +104,7 @@ async fn test_remote_echo_full_trip() { let event_item = item.as_event().unwrap(); assert!(event_item.is_local_echo()); assert_matches!(event_item.send_state(), Some(EventSendState::Sent { .. })); - assert_eq!(item.unique_id(), id); + assert_eq!(*item.unique_id(), id); event_item.timestamp() }; @@ -125,7 +125,7 @@ async fn test_remote_echo_full_trip() { // The local echo is replaced with the remote echo. let item = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value); assert!(!item.as_event().unwrap().is_local_echo()); - assert_eq!(item.unique_id(), id); + assert_eq!(*item.unique_id(), id); } #[async_test] diff --git a/crates/matrix-sdk-ui/src/timeline/util.rs b/crates/matrix-sdk-ui/src/timeline/util.rs index 91159f8654c..3dd1302fa43 100644 --- a/crates/matrix-sdk-ui/src/timeline/util.rs +++ b/crates/matrix-sdk-ui/src/timeline/util.rs @@ -21,27 +21,27 @@ use ruma::{EventId, MilliSecondsSinceUnixEpoch}; #[cfg(doc)] use super::controller::TimelineMetadata; use super::{ - event_item::EventTimelineItemKind, EventTimelineItem, ReactionsByKeyBySender, - TimelineEventItemId, TimelineItem, + event_item::EventTimelineItemKind, item::TimelineUniqueId, EventTimelineItem, + ReactionsByKeyBySender, TimelineEventItemId, TimelineItem, }; pub(super) struct EventTimelineItemWithId<'a> { pub inner: &'a EventTimelineItem, /// Internal identifier generated by [`TimelineMetadata`]. - pub internal_id: &'a str, + pub internal_id: &'a TimelineUniqueId, } impl<'a> EventTimelineItemWithId<'a> { /// Create a clone of the underlying [`TimelineItem`] with the given kind. pub fn with_inner_kind(&self, kind: impl Into) -> Arc { - TimelineItem::new(self.inner.with_kind(kind), self.internal_id.to_owned()) + TimelineItem::new(self.inner.with_kind(kind), self.internal_id.clone()) } /// Create a clone of the underlying [`TimelineItem`] with the given /// reactions. pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Arc { let event_item = self.inner.with_reactions(reactions); - TimelineItem::new(event_item, self.internal_id.to_owned()) + TimelineItem::new(event_item, self.internal_id.clone()) } } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs index f53218d854c..08b9cd2a638 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs @@ -133,12 +133,12 @@ async fn test_in_reply_to_details() { assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Pending); - assert_eq!(third.unique_id(), unique_id); + assert_eq!(*third.unique_id(), unique_id); assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Error(_)); - assert_eq!(third.unique_id(), unique_id); + assert_eq!(*third.unique_id(), unique_id); // Set up fetching the replied-to event to succeed Mock::given(method("GET")) @@ -162,12 +162,12 @@ async fn test_in_reply_to_details() { assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Pending); - assert_eq!(third.unique_id(), unique_id); + assert_eq!(*third.unique_id(), unique_id); assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Ready(_)); - assert_eq!(third.unique_id(), unique_id); + assert_eq!(*third.unique_id(), unique_id); } #[async_test] From 2829b0730544bede35b69b8335d35adac938d2fb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 16 Oct 2024 12:17:47 +0100 Subject: [PATCH 298/979] doc: fix typo in contributing guide The convention is for changelog entries to be in the imperative, not the past tense. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d35c9b33aa..f56afd7618e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,7 @@ This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to stringify Ed25519 and thus present them to users. It's also commonly used when Ed25519 keys need to be inserted into JSON. -Changelog: Added the `Ed25519PublicKey::to_base64()` method which can be used to +Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to stringify the Ed25519 public key. ``` From 4a7f924161669f95ba8f8032bf11b7e5b8b72655 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 11 Oct 2024 12:29:03 +0100 Subject: [PATCH 299/979] timeline: tests for deserializing SyncTimelineEvent with unsigned events --- .../src/deserialized_responses.rs | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 6ef33dbfd20..f697f83389a 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -705,6 +705,8 @@ impl From for SyncTimelineEvent { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use assert_matches::assert_matches; use ruma::{ event_id, @@ -717,7 +719,8 @@ mod tests { use super::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, - TimelineEventKind, VerificationState, + TimelineEventKind, UnableToDecryptInfo, UnsignedDecryptionResult, UnsignedEventLocation, + VerificationState, }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; @@ -808,7 +811,12 @@ mod tests { }, verification_state: VerificationState::Verified, }, - unsigned_encryption_info: None, + unsigned_encryption_info: Some(BTreeMap::from([( + UnsignedEventLocation::RelationsReplace, + UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { + session_id: Some("xyz".to_owned()), + }), + )])), }), push_actions: Default::default(), }; @@ -840,6 +848,9 @@ mod tests { }, "verification_state": "Verified", }, + "unsigned_encryption_info": { + "RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}} + } } } }) @@ -881,5 +892,48 @@ mod tests { event.encryption_info().unwrap().algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { .. } ); + + // Test that the previous format, with an undecryptable unsigned event, can also + // be deserialized. + let serialized = json!({ + "event": { + "content": {"body": "secret", "msgtype": "m.text"}, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message", + }, + "encryption_info": { + "sender": "@sender:example.com", + "sender_device": null, + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified", + }, + "unsigned_encryption_info": { + "RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}} + } + }); + let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); + assert_matches!( + event.encryption_info().unwrap().algorithm_info, + AlgorithmInfo::MegolmV1AesSha2 { .. } + ); + assert_matches!(event.kind, TimelineEventKind::Decrypted(decrypted) => { + assert_matches!(decrypted.unsigned_encryption_info, Some(map) => { + assert_eq!(map.len(), 1); + let (location, result) = map.into_iter().next().unwrap(); + assert_eq!(location, UnsignedEventLocation::RelationsReplace); + assert_matches!(result, UnsignedDecryptionResult::UnableToDecrypt(utd_info) => { + assert_eq!(utd_info.session_id, Some("xyz".to_owned())); + }) + }); + }); } } From 427c59e26608b8806cdad4aa57c4fbed9f2517f3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 10 Oct 2024 15:43:26 +0100 Subject: [PATCH 300/979] crypto: add `UnableToDecryptReason` to `UnableToDecryptInfo` Add a field to store the reason that the decryption failed --- .../src/deserialized_responses.rs | 64 ++++++++++++++++++- crates/matrix-sdk-crypto/src/machine/mod.rs | 61 ++++++++++++++---- .../src/machine/tests/mod.rs | 8 ++- 3 files changed, 114 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index f697f83389a..f19d523eeae 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -628,6 +628,59 @@ pub struct UnableToDecryptInfo { /// `m.megolm.v1.aes-sha2` algorithm. #[serde(skip_serializing_if = "Option::is_none")] pub session_id: Option, + + /// Reason code for the decryption failure + #[serde(default = "unknown_utd_reason")] + pub reason: UnableToDecryptReason, +} + +fn unknown_utd_reason() -> UnableToDecryptReason { + UnableToDecryptReason::Unknown +} + +/// Reason code for a decryption failure +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum UnableToDecryptReason { + /// The reason for the decryption failure is unknown. This is only intended + /// for use when deserializing old UnableToDecryptInfo instances. + #[doc(hidden)] + Unknown, + + /// The `m.room.encrypted` event that should have been decrypted is + /// malformed in some way (e.g. unsupported algorithm, missing fields, + /// unknown megolm message type). + MalformedEncryptedEvent, + + /// Decryption failed because we're missing the megolm session that was used + /// to encrypt the event. + /// + /// TODO: support withheld codes? + MissingMegolmSession, + + /// Decryption failed because, while we have the megolm session that was + /// used to encrypt the message, it is ratcheted too far forward. + UnknownMegolmMessageIndex, + + /// We found the Megolm session, but were unable to decrypt the event using + /// that session for some reason (e.g. incorrect MAC). + /// + /// This represents all `vodozemac::megolm::DecryptionError`s, except + /// `UnknownMessageIndex`, which is represented as + /// `UnknownMegolmMessageIndex`. + MegolmDecryptionFailure, + + /// The event could not be deserialized after decryption. + PayloadDeserializationFailure, + + /// Decryption failed because of a mismatch between the identity keys of the + /// device we received the room key from and the identity keys recorded in + /// the plaintext of the room key to-device message. + MismatchedIdentityKeys, + + /// An encrypted message wasn't decrypted, because the sender's + /// cross-signing identity did not satisfy the requested + /// `TrustRequirement`. + SenderIdentityNotTrusted(VerificationLevel), } /// Deserialization helper for [`SyncTimelineEvent`], for the modern format. @@ -719,8 +772,8 @@ mod tests { use super::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, - TimelineEventKind, UnableToDecryptInfo, UnsignedDecryptionResult, UnsignedEventLocation, - VerificationState, + TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, + UnsignedEventLocation, VerificationState, }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; @@ -815,6 +868,7 @@ mod tests { UnsignedEventLocation::RelationsReplace, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { session_id: Some("xyz".to_owned()), + reason: UnableToDecryptReason::MalformedEncryptedEvent, }), )])), }), @@ -849,7 +903,10 @@ mod tests { "verification_state": "Verified", }, "unsigned_encryption_info": { - "RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}} + "RelationsReplace": {"UnableToDecrypt": { + "session_id": "xyz", + "reason": "MalformedEncryptedEvent", + }} } } } @@ -932,6 +989,7 @@ mod tests { assert_eq!(location, UnsignedEventLocation::RelationsReplace); assert_matches!(result, UnsignedDecryptionResult::UnableToDecrypt(utd_info) => { assert_eq!(utd_info.session_id, Some("xyz".to_owned())); + assert_eq!(utd_info.reason, UnableToDecryptReason::Unknown); }) }); }); diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 80b2c1d44d5..0e21e04cdc2 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -22,7 +22,8 @@ use itertools::Itertools; use matrix_sdk_common::{ deserialized_responses::{ AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, UnableToDecryptInfo, - UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, VerificationState, + UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, + VerificationState, }, BoxFuture, }; @@ -1902,18 +1903,13 @@ impl OlmMachine { *event = serde_json::to_value(decrypted_event.event).ok()?; Some(UnsignedDecryptionResult::Decrypted(decrypted_event.encryption_info)) } - Err(_) => { - let session_id = - raw_event.deserialize().ok().and_then(|ev| match ev.content.scheme { - RoomEventEncryptionScheme::MegolmV1AesSha2(s) => Some(s.session_id), - #[cfg(feature = "experimental-algorithms")] - RoomEventEncryptionScheme::MegolmV2AesSha2(s) => Some(s.session_id), - RoomEventEncryptionScheme::Unknown(_) => None, - }); - - Some(UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { - session_id, - })) + Err(err) => { + // For now, we throw away crypto store errors and just treat the unsigned event + // as unencrypted. Crypto store errors represent problems with the application + // rather than normal UTD errors, so they should probably be propagated + // rather than swallowed. + let utd_info = megolm_error_to_utd_info(&raw_event, err).ok()?; + Some(UnsignedDecryptionResult::UnableToDecrypt(utd_info)) } } }) @@ -2540,6 +2536,45 @@ pub struct EncryptionSyncChanges<'a> { pub next_batch_token: Option, } +/// Convert a [`MegolmError`] into an [`UnableToDecryptInfo`] or a +/// [`CryptoStoreError`]. +/// +/// Most `MegolmError` codes are converted into a suitable +/// `UnableToDecryptInfo`. The exception is [`MegolmError::Store`], which +/// represents a problem with our datastore rather than with the message itself, +/// and is therefore returned as a `CryptoStoreError`. +fn megolm_error_to_utd_info( + raw_event: &Raw, + error: MegolmError, +) -> Result { + use MegolmError::*; + let reason = match error { + EventError(_) => UnableToDecryptReason::MalformedEncryptedEvent, + Decode(_) => UnableToDecryptReason::MalformedEncryptedEvent, + MissingRoomKey(_) => UnableToDecryptReason::MissingMegolmSession, + Decryption(DecryptionError::UnknownMessageIndex(_, _)) => { + UnableToDecryptReason::UnknownMegolmMessageIndex + } + Decryption(_) => UnableToDecryptReason::MegolmDecryptionFailure, + JsonError(_) => UnableToDecryptReason::PayloadDeserializationFailure, + MismatchedIdentityKeys(_) => UnableToDecryptReason::MismatchedIdentityKeys, + SenderIdentityNotTrusted(level) => UnableToDecryptReason::SenderIdentityNotTrusted(level), + + // Pass through crypto store errors, which indicate a problem with our + // application, rather than a UTD. + Store(error) => Err(error)?, + }; + + let session_id = raw_event.deserialize().ok().and_then(|ev| match ev.content.scheme { + RoomEventEncryptionScheme::MegolmV1AesSha2(s) => Some(s.session_id), + #[cfg(feature = "experimental-algorithms")] + RoomEventEncryptionScheme::MegolmV2AesSha2(s) => Some(s.session_id), + RoomEventEncryptionScheme::Unknown(_) => None, + }); + + Ok(UnableToDecryptInfo { session_id, reason }) +} + #[cfg(test)] pub(crate) mod test_helpers; diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 4a0dd8e60f8..110e9872ac5 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -18,7 +18,7 @@ use assert_matches2::assert_matches; use futures_util::{pin_mut, FutureExt, StreamExt}; use itertools::Itertools; use matrix_sdk_common::deserialized_responses::{ - UnableToDecryptInfo, UnsignedDecryptionResult, UnsignedEventLocation, + UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, }; use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json}; use ruma::{ @@ -1355,7 +1355,8 @@ async fn test_unsigned_decryption() { assert_matches!( replace_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { - session_id: Some(second_room_key_session_id) + session_id: Some(second_room_key_session_id), + reason: UnableToDecryptReason::MissingMegolmSession, }) ); @@ -1460,7 +1461,8 @@ async fn test_unsigned_decryption() { assert_matches!( thread_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { - session_id: Some(third_room_key_session_id) + session_id: Some(third_room_key_session_id), + reason: UnableToDecryptReason::MissingMegolmSession, }) ); From 2820f5f3b4ee3cae5073a229bdc20759d4c52e96 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 4 Oct 2024 14:40:29 +0100 Subject: [PATCH 301/979] crypto: new method `OlmMachine::try_decrypt_room_event` --- crates/matrix-sdk-crypto/src/lib.rs | 12 ++++++++ crates/matrix-sdk-crypto/src/machine/mod.rs | 30 ++++++++++++++++++- .../src/machine/tests/mod.rs | 23 ++++++++------ 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index cfad6268a6d..b6c50526876 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -89,6 +89,7 @@ pub use identities::{ OwnUserIdentityData, UserDevices, UserIdentity, UserIdentityData, }; pub use machine::{CrossSigningBootstrapRequests, EncryptionSyncChanges, OlmMachine}; +use matrix_sdk_common::deserialized_responses::{DecryptedRoomEvent, UnableToDecryptInfo}; #[cfg(feature = "qrcode")] pub use matrix_sdk_qrcode; pub use olm::{Account, CrossSigningStatus, EncryptionSettings, Session}; @@ -142,3 +143,14 @@ pub struct DecryptionSettings { /// [`MegolmError::SenderIdentityNotTrusted`] will be returned. pub sender_device_trust_requirement: TrustRequirement, } + +/// The result of an attempt to decrypt a room event: either a successful +/// decryption, or information on a failure. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum RoomEventDecryptionResult { + /// A successfully-decrypted encrypted event. + Decrypted(DecryptedRoomEvent), + + /// We were unable to decrypt the event + UnableToDecrypt(UnableToDecryptInfo), +} diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 0e21e04cdc2..3624f4aa2cb 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -95,7 +95,7 @@ use crate::{ utilities::timestamp_to_iso8601, verification::{Verification, VerificationMachine, VerificationRequest}, CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, KeysQueryRequest, - LocalTrust, SignatureError, ToDeviceRequest, TrustRequirement, + LocalTrust, RoomEventDecryptionResult, SignatureError, ToDeviceRequest, TrustRequirement, }; /// State machine implementation of the Olm/Megolm encryption protocol used for @@ -1735,6 +1735,34 @@ impl OlmMachine { } } + /// Attempt to decrypt an event from a room timeline, returning information + /// on the failure if it fails. + /// + /// # Arguments + /// + /// * `event` - The event that should be decrypted. + /// + /// * `room_id` - The ID of the room where the event was sent to. + /// + /// # Returns + /// + /// The decrypted event, if it was successfully decrypted. Otherwise, + /// information on the failure, unless the failure was due to an + /// internal error, in which case, an `Err` result. + pub async fn try_decrypt_room_event( + &self, + raw_event: &Raw, + room_id: &RoomId, + decryption_settings: &DecryptionSettings, + ) -> Result { + match self.decrypt_room_event_inner(raw_event, room_id, true, decryption_settings).await { + Ok(decrypted) => Ok(RoomEventDecryptionResult::Decrypted(decrypted)), + Err(err) => Ok(RoomEventDecryptionResult::UnableToDecrypt(megolm_error_to_utd_info( + raw_event, err, + )?)), + } + } + /// Decrypt an event from a room timeline. /// /// # Arguments diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 110e9872ac5..21dabf2023a 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -14,7 +14,7 @@ use std::{collections::BTreeMap, iter, ops::Not, sync::Arc, time::Duration}; -use assert_matches2::assert_matches; +use assert_matches2::{assert_let, assert_matches}; use futures_util::{pin_mut, FutureExt, StreamExt}; use itertools::Itertools; use matrix_sdk_common::deserialized_responses::{ @@ -71,7 +71,7 @@ use crate::{ utilities::json_convert, verification::tests::bob_id, Account, DecryptionSettings, DeviceData, EncryptionSettings, MegolmError, OlmError, - OutgoingRequests, ToDeviceRequest, TrustRequirement, + OutgoingRequests, RoomEventDecryptionResult, ToDeviceRequest, TrustRequirement, }; mod decryption_verification_state; @@ -555,13 +555,11 @@ async fn test_megolm_encryption() { let decryption_settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; - let decrypted_event = bob - .decrypt_room_event(&event, room_id, &decryption_settings) - .await - .unwrap() - .event - .deserialize() - .unwrap(); + + let decryption_result = + bob.try_decrypt_room_event(&event, room_id, &decryption_settings).await.unwrap(); + assert_let!(RoomEventDecryptionResult::Decrypted(decrypted_event) = decryption_result); + let decrypted_event = decrypted_event.event.deserialize().unwrap(); if let AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original( OriginalMessageLikeEvent { sender, content, .. }, @@ -678,6 +676,13 @@ async fn test_withheld_unverified() { let err = decrypt_result.err().unwrap(); assert_matches!(err, MegolmError::MissingRoomKey(Some(WithheldCode::Unverified))); + + // Also check `try_decrypt_room_event`. + let decrypt_result = + bob.try_decrypt_room_event(&room_event, room_id, &decryption_settings).await.unwrap(); + assert_let!(RoomEventDecryptionResult::UnableToDecrypt(utd_info) = decrypt_result); + assert!(utd_info.session_id.is_some()); + assert_eq!(utd_info.reason, UnableToDecryptReason::MissingMegolmSession); } /// Test what happens when we feed an unencrypted event into the decryption From 87f89ec5617d0fbb16bd486f3cb484d23e3492a1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 10 Oct 2024 18:08:16 +0100 Subject: [PATCH 302/979] crypto: update changelog --- crates/matrix-sdk-crypto/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index bbcf222dbdb..dd3ac96a215 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,12 @@ Changes: +- Add new method `OlmMachine::try_decrypt_room_event`. + ([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116)) + +- Add reason code to `matrix_sdk_common::deserialized_responses::UnableToDecryptInfo`. + ([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116)) + - The `UserIdentity` struct has been renamed to `OtherUserIdentity` ([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036])) From a901506a536bf12b081eea6d6c70777d0fdcc565 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 17:48:31 +0200 Subject: [PATCH 303/979] fix(ffi): don't panic when joining after having cancelled a media upload Fixes #3573. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 0dd994da43d..87dbb859356 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::HashMap, fmt::Write as _, fs, sync::Arc}; +use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc}; use anyhow::{Context, Result}; use as_variant::as_variant; @@ -1205,11 +1205,28 @@ impl SendAttachmentJoinHandle { #[matrix_sdk_ffi_macros::export] impl SendAttachmentJoinHandle { + /// Wait until the attachment has been sent. + /// + /// If the sending had been cancelled, will return immediately. pub async fn join(&self) -> Result<(), RoomError> { - let join_hdl = self.join_hdl.clone(); - RUNTIME.spawn(async move { (&mut *join_hdl.lock().await).await.unwrap() }).await.unwrap() + let handle = self.join_hdl.clone(); + let mut locked_handle = handle.lock().await; + let join_result = (&mut *locked_handle).await; + match join_result { + Ok(res) => res, + Err(err) => { + if err.is_cancelled() { + return Ok(()); + } + error!("task panicked! resuming panic from here."); + panic::resume_unwind(err.into_panic()); + } + } } + /// Cancel the current sending task. + /// + /// A subsequent call to [`Self::join`] will return immediately. pub fn cancel(&self) { self.abort_hdl.abort(); } From c4dd2d192ea3116529ff62fc1e1f1caae741f6d1 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 15 Oct 2024 16:36:34 +0200 Subject: [PATCH 304/979] refactor(timeline): fuse `redact_by_id()` within `redact()` Changelog: `Timeline::redact_by_id` has been fused into `Timeline::redact`, which now takes a `TimelineEventItemId` as an identifier of the item (local or remote) to redact. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 9 +- crates/matrix-sdk-ui/src/timeline/error.rs | 5 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 76 ++------ .../tests/integration/timeline/mod.rs | 183 +----------------- 4 files changed, 35 insertions(+), 238 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 87dbb859356..ea4395ee590 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -610,10 +610,11 @@ impl Timeline { event_or_transaction_id: EventOrTransactionId, reason: Option, ) -> Result<(), ClientError> { - self.inner - .redact_by_id(&(event_or_transaction_id.try_into()?), reason.as_deref()) - .await - .map_err(Into::into) + if !self.inner.redact(&(event_or_transaction_id.try_into()?), reason.as_deref()).await? { + // TODO make it a hard error instead + warn!("Couldn't redact item"); + } + Ok(()) } /// Load the reply details for the given event id. diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index b1a89956474..bc881bc97a7 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -18,7 +18,6 @@ use matrix_sdk::{ send_queue::RoomSendQueueError, HttpError, }; -use ruma::OwnedTransactionId; use thiserror::Error; use crate::timeline::{pinned_events_loader::PinnedEventsLoaderError, TimelineEventItemId}; @@ -83,8 +82,8 @@ pub enum Error { #[derive(Error, Debug)] pub enum RedactError { /// Local event to redact wasn't found for transaction id - #[error("Local event to redact wasn't found for transaction {0}")] - LocalEventNotFound(OwnedTransactionId), + #[error("Event to redact wasn't found for item id {0:?}")] + ItemNotFound(TimelineEventItemId), /// An error happened while attempting to redact an event. #[error(transparent)] diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 35adf7e875a..4fee860a3d9 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -54,6 +54,7 @@ use ruma::{ }; use thiserror::Error; use tracing::{error, instrument, trace, warn}; +use util::rfind_event_by_item_id; use crate::timeline::pinned_events_loader::PinnedEventsRoom; @@ -602,41 +603,6 @@ impl Timeline { /// Redact an event given its [`TimelineEventItemId`] and an optional /// reason. /// - /// See [`Self::redact`] for more info. - pub async fn redact_by_id( - &self, - id: &TimelineEventItemId, - reason: Option<&str>, - ) -> Result<(), Error> { - let event_id = match id { - TimelineEventItemId::TransactionId(transaction_id) => { - let Some(item) = self.item_by_transaction_id(transaction_id).await else { - return Err(Error::RedactError(RedactError::LocalEventNotFound( - transaction_id.to_owned(), - ))); - }; - - match item.handle() { - TimelineItemHandle::Local(handle) => { - // If there is a local item that hasn't been sent yet, abort the upload - handle.abort().await.map_err(RoomSendQueueError::StorageError)?; - return Ok(()); - } - TimelineItemHandle::Remote(event_id) => event_id.to_owned(), - } - } - TimelineEventItemId::EventId(event_id) => event_id.to_owned(), - }; - self.room() - .redact(&event_id, reason, None) - .await - .map_err(|e| Error::RedactError(RedactError::HttpError(e)))?; - - Ok(()) - } - - /// Redact an event. - /// /// # Returns /// /// - Returns `Ok(true)` if the redact happened. @@ -646,33 +612,27 @@ impl Timeline { /// interacting with the sending queue. pub async fn redact( &self, - event: &EventTimelineItem, + item_id: &TimelineEventItemId, reason: Option<&str>, ) -> Result { - let event_id = match event.identifier() { - TimelineEventItemId::TransactionId(_) => { - // See if we have an up-to-date timeline item with that transaction id. - match event.handle() { - TimelineItemHandle::Remote(event_id) => event_id.to_owned(), - TimelineItemHandle::Local(handle) => { - return Ok(handle - .abort() - .await - .map_err(RoomSendQueueError::StorageError)?); - } - } - } - - TimelineEventItemId::EventId(event_id) => event_id, + let items = self.items().await; + let Some((_pos, event)) = rfind_event_by_item_id(&items, item_id) else { + return Err(Error::RedactError(RedactError::ItemNotFound(item_id.clone()))); }; - self.room() - .redact(&event_id, reason, None) - .await - .map_err(RedactError::HttpError) - .map_err(Error::RedactError)?; - - Ok(true) + match event.handle() { + TimelineItemHandle::Remote(event_id) => { + self.room() + .redact(event_id, reason, None) + .await + .map_err(RedactError::HttpError) + .map_err(Error::RedactError)?; + Ok(true) + } + TimelineItemHandle::Local(handle) => { + Ok(handle.abort().await.map_err(RoomSendQueueError::StorageError)?) + } + } } /// Fetch unavailable details about the event with the given ID. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 76fb281f9a8..f2d151e5bea 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -276,9 +276,8 @@ async fn test_redact_message() { // Redacting a remote event works. mock_redaction(event_id!("$42")).mount(&server).await; - let event_id = first.as_event().unwrap(); - - let did_redact = timeline.redact(event_id, Some("inapprops")).await.unwrap(); + let did_redact = + timeline.redact(&first.as_event().unwrap().identifier(), Some("inapprops")).await.unwrap(); assert!(did_redact); // Redacting a local event works. @@ -301,7 +300,7 @@ async fn test_redact_message() { assert_matches!(second.send_state(), Some(EventSendState::SendingFailed { .. })); // Let's redact the local echo. - let did_redact = timeline.redact(second, None).await.unwrap(); + let did_redact = timeline.redact(&second.identifier(), None).await.unwrap(); assert!(did_redact); // Observe local echo being removed. @@ -371,151 +370,11 @@ async fn test_redact_local_sent_message() { mock_redaction(event.event_id().unwrap()).expect(1).mount(&server).await; // Let's redact the local echo with the remote handle. - timeline.redact(event, None).await.unwrap(); + timeline.redact(&event.identifier(), None).await.unwrap(); } #[async_test] -async fn test_redact_by_id_message() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - let (_, mut timeline_stream) = timeline.subscribe().await; - - let factory = EventFactory::new(); - factory.set_next_ts(MilliSecondsSinceUnixEpoch::now().get().into()); - - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event( - factory.sender(user_id!("@a:b.com")).text_msg("buy my bitcoins bro"), - ), - ); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); - assert_eq!( - first.as_event().unwrap().content().as_message().unwrap().body(), - "buy my bitcoins bro" - ); - - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); - - // Redacting a remote event works. - mock_redaction(event_id!("$42")).mount(&server).await; - - let event = first.as_event().unwrap(); - - timeline.redact_by_id(&event.identifier(), Some("inapprops")).await.unwrap(); - - // Redacting a local event works. - timeline - .send(RoomMessageEventContent::text_plain("i will disappear soon").into()) - .await - .unwrap(); - - assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); - - let second = second.as_event().unwrap(); - assert_matches!(second.send_state(), Some(EventSendState::NotSentYet)); - - // We haven't set a route for sending events, so this will fail. - assert_let!(Some(VectorDiff::Set { index, value: second }) = timeline_stream.next().await); - assert_eq!(index, 2); - - let second = second.as_event().unwrap(); - assert!(second.is_local_echo()); - assert_matches!(second.send_state(), Some(EventSendState::SendingFailed { .. })); - - // Let's redact the local echo. - timeline.redact_by_id(&second.identifier(), None).await.unwrap(); - - // Observe local echo being removed. - assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); -} - -#[async_test] -async fn test_redact_by_local_sent_message() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - let (_, mut timeline_stream) = timeline.subscribe().await; - - // Mock event sending. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({ "event_id": "$wWgymRfo7ri1uQx0NXO40vLJ" })), - ) - .expect(1) - .mount(&server) - .await; - - // Send the event so it's added to the send queue as a local event. - timeline - .send(RoomMessageEventContent::text_plain("i will disappear soon").into()) - .await - .unwrap(); - - // Assert the local event is in the timeline now and is not sent yet. - assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); - let event = item.as_event().unwrap(); - assert!(event.is_local_echo()); - assert_matches!(event.send_state(), Some(EventSendState::NotSentYet)); - - // As well as a day divider. - assert_let_timeout!( - Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next() - ); - assert!(day_divider.is_day_divider()); - - // We receive an update in the timeline from the send queue. - assert_let_timeout!(Some(VectorDiff::Set { index, value: item }) = timeline_stream.next()); - assert_eq!(index, 1); - - // Check the event is sent but still considered local. - let event = item.as_event().unwrap(); - assert!(event.is_local_echo()); - assert_matches!(event.send_state(), Some(EventSendState::Sent { .. })); - - // Mock the redaction response for the event we just sent. Ensure it's called - // once. - mock_redaction(event.event_id().unwrap()).expect(1).mount(&server).await; - - // Let's redact the local echo with the remote handle. - timeline.redact_by_id(&event.identifier(), None).await.unwrap(); -} - -#[async_test] -async fn test_redact_by_id_message_with_no_remote_message_present() { +async fn test_redact_nonexisting_item() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; @@ -534,36 +393,14 @@ async fn test_redact_by_id_message_with_no_remote_message_present() { let timeline = room.timeline().await.unwrap(); let error = timeline - .redact_by_id(&TimelineEventItemId::EventId(owned_event_id!("$123:example.com")), None) + .redact(&TimelineEventItemId::EventId(owned_event_id!("$123:example.com")), None) .await .err(); - assert_matches!(error, Some(Error::RedactError(RedactError::HttpError(_)))) -} + assert_matches!(error, Some(Error::RedactError(RedactError::ItemNotFound(_)))); -#[async_test] -async fn test_redact_by_id_message_with_no_local_message_present() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - - let error = timeline - .redact_by_id(&TimelineEventItemId::TransactionId("something".into()), None) - .await - .err(); - assert_matches!(error, Some(Error::RedactError(RedactError::LocalEventNotFound(_)))) + let error = + timeline.redact(&TimelineEventItemId::TransactionId("something".into()), None).await.err(); + assert_matches!(error, Some(Error::RedactError(RedactError::ItemNotFound(_)))); } #[async_test] From 81bebcf69251c04dd1570dec3bffc39008506ebe Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 15 Oct 2024 17:05:28 +0200 Subject: [PATCH 305/979] refactor(timeline): fuse `edit_by_id()` within `edit()` In particular, this means that trying to edit an event that's not present anymore in a timeline (e.g. after a timeline reset) will fail, while it worked before. Changelog: `Timeline::edit_by_id` has been fused into `Timeline::edit`, which now takes a `TimelineEventItemId` as the identifier for the local or remote item to edit. This also means that editing an event that's not in the timeline anymore will now fail. Callers should manually create the edit event's content, and then send it via the send queue; which the FFI function `Room::edit` does. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 2 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 127 +++----- .../tests/integration/timeline/edit.rs | 286 ++---------------- .../src/tests/timeline.rs | 2 +- 4 files changed, 65 insertions(+), 352 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index ea4395ee590..dc9842c7b8a 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -496,7 +496,7 @@ impl Timeline { new_content: EditedContent, ) -> Result { self.inner - .edit_by_id(&event_or_transaction_id.try_into()?, new_content.try_into()?) + .edit(&event_or_transaction_id.try_into()?, new_content.try_into()?) .await .map_err(Into::into) } diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 4fee860a3d9..29825e5b200 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -18,7 +18,7 @@ use std::{path::PathBuf, pin::Pin, sync::Arc, task::Poll}; -use event_item::{extract_room_msg_edit_content, EventTimelineItemKind, TimelineItemHandle}; +use event_item::{extract_room_msg_edit_content, TimelineItemHandle}; use eyeball_im::VectorDiff; use futures_core::Stream; use imbl::Vector; @@ -239,17 +239,6 @@ impl Timeline { self.controller.retry_event_decryption(self.room(), None).await; } - /// Get the current timeline item for the given [`TimelineEventItemId`], if - /// any. - async fn event_by_timeline_id(&self, id: &TimelineEventItemId) -> Option { - match id { - TimelineEventItemId::EventId(event_id) => self.item_by_event_id(event_id).await, - TimelineEventItemId::TransactionId(transaction_id) => { - self.item_by_transaction_id(transaction_id).await - } - } - } - /// Get the current timeline item for the given event ID, if any. /// /// Will return a remote event, *or* a local echo that has been sent but not @@ -456,38 +445,8 @@ impl Timeline { }) } - /// Returns a local or remote timeline item identified by this transaction - /// id. - async fn item_by_transaction_id(&self, txn_id: &TransactionId) -> Option { - let items = self.controller.items().await; - - let (_, found) = rfind_event_item(&items, |item| match &item.kind { - EventTimelineItemKind::Local(local) => local.transaction_id == txn_id, - EventTimelineItemKind::Remote(remote) => { - remote.transaction_id.as_deref() == Some(txn_id) - } - })?; - - Some(found.clone()) - } - /// Edit an event given its [`TimelineEventItemId`] and some new content. /// - /// See [`Self::edit`] for more information. - pub async fn edit_by_id( - &self, - id: &TimelineEventItemId, - new_content: EditedContent, - ) -> Result { - let Some(event) = self.event_by_timeline_id(id).await else { - return Err(Error::EventNotInTimeline(id.clone())); - }; - - self.edit(&event, new_content).await - } - - /// Edit an event. - /// /// Only supports events for which [`EventTimelineItem::is_editable()`] /// returns `true`. /// @@ -501,59 +460,49 @@ impl Timeline { #[instrument(skip(self, new_content))] pub async fn edit( &self, - item: &EventTimelineItem, + item_id: &TimelineEventItemId, new_content: EditedContent, ) -> Result { - let event_id = match item.identifier() { - TimelineEventItemId::TransactionId(txn_id) => { - // See if we have an up-to-date timeline item with that transaction id. - if let Some(item) = self.item_by_transaction_id(&txn_id).await { - match item.handle() { - TimelineItemHandle::Remote(event_id) => event_id.to_owned(), - TimelineItemHandle::Local(handle) => { - // Relations are filled by the editing code itself. - let new_content: AnyMessageLikeEventContent = match new_content { - EditedContent::RoomMessage(message) => { - if matches!(item.content, TimelineItemContent::Message(_)) { - AnyMessageLikeEventContent::RoomMessage(message.into()) - } else { - warn!("New content (m.room.message) doesn't match previous event content."); - return Ok(false); - } - } - EditedContent::PollStart { new_content, .. } => { - if matches!(item.content, TimelineItemContent::Poll(_)) { - AnyMessageLikeEventContent::UnstablePollStart( - UnstablePollStartEventContent::New( - NewUnstablePollStartEventContent::new(new_content), - ), - ) - } else { - warn!("New content (poll start) doesn't match previous event content."); - return Ok(false); - } - } - }; - return Ok(handle - .edit(new_content) - .await - .map_err(RoomSendQueueError::StorageError)?); - } - } - } else { - warn!("Couldn't find the local echo anymore, nor a matching remote echo"); - return Ok(false); - } - } - - TimelineEventItemId::EventId(event_id) => event_id, + let items = self.items().await; + let Some((_pos, item)) = rfind_event_by_item_id(&items, item_id) else { + return Err(Error::EventNotInTimeline(item_id.clone())); }; - let content = self.room().make_edit_event(&event_id, new_content).await?; + match item.handle() { + TimelineItemHandle::Remote(event_id) => { + let content = self.room().make_edit_event(event_id, new_content).await?; + self.send(content).await?; + Ok(true) + } - self.send(content).await?; + TimelineItemHandle::Local(handle) => { + // Relations are filled by the editing code itself. + let new_content: AnyMessageLikeEventContent = match new_content { + EditedContent::RoomMessage(message) => { + if matches!(item.content, TimelineItemContent::Message(_)) { + AnyMessageLikeEventContent::RoomMessage(message.into()) + } else { + warn!("New content (m.room.message) doesn't match previous event content."); + return Ok(false); + } + } + EditedContent::PollStart { new_content, .. } => { + if matches!(item.content, TimelineItemContent::Poll(_)) { + AnyMessageLikeEventContent::UnstablePollStart( + UnstablePollStartEventContent::New( + NewUnstablePollStartEventContent::new(new_content), + ), + ) + } else { + warn!("New content (poll start) doesn't match previous event content."); + return Ok(false); + } + } + }; - Ok(true) + Ok(handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)?) + } + } } /// Toggle a reaction on an event. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index a4fed5da95c..f3c22913253 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -238,7 +238,7 @@ async fn test_edit_local_echo() { // Let's edit the local echo. let did_edit = timeline .edit( - item, + &item.identifier(), EditedContent::RoomMessage(RoomMessageEventContent::text_plain("hello, world").into()), ) .await @@ -325,7 +325,7 @@ async fn test_send_edit() { timeline .edit( - &hello_world_item, + &hello_world_item.identifier(), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( "Hello, Room!", )), @@ -412,7 +412,7 @@ async fn test_send_reply_edit() { let edited = timeline .edit( - &reply_item, + &reply_item.identifier(), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( "Hello, Room!", )), @@ -523,7 +523,7 @@ async fn test_edit_to_replied_updates_reply() { // If I edit the first message,… let edited = timeline .edit( - &replied_to_item, + &replied_to_item.identifier(), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( "hello world", )), @@ -638,7 +638,7 @@ async fn test_send_edit_poll() { UnstablePollStartContentBlock::new("Edited Test".to_owned(), edited_poll_answers); timeline .edit( - &poll_event, + &poll_event.identifier(), EditedContent::PollStart { fallback_text: "poll_fallback_text".to_owned(), new_content: edited_poll, @@ -719,42 +719,21 @@ async fn test_send_edit_when_timeline_is_clear() { yield_now().await; assert_next_matches!(timeline_stream, VectorDiff::Clear); - mock_encryption_state(&server, false).await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .respond_with( - ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$edit_event" })), - ) - .expect(1) - .mount(&server) - .await; - - // Since we assume we can't use the timeline item directly in this use case, the - // API will fetch the event from the server directly so we need to mock the - // response. - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(raw_original_event.json())) - .expect(1) - .named("event_1") - .mount(&server) - .await; - - timeline - .edit( - &hello_world_item, - EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( - "Hello, Room!", - )), - ) - .await - .unwrap(); - - // Since verifying the content would mean mocking the sliding sync response with - // what we are already expecting, because this test would require to paginate - // again the timeline, testing the content change would not be meaningful. - // Use an integration test for the full case. + // Sending the edit will fail, since the edited event isn't in the timeline + // anymore. + assert_matches!( + timeline + .edit( + &hello_world_item.identifier(), + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + "Hello, Room!", + )), + ) + .await, + Err(Error::EventNotInTimeline(TimelineEventItemId::EventId(event_id))) => { + assert_eq!(hello_world_item.event_id().unwrap(), event_id); + } + ); // The response to the mocked endpoint does not generate further timeline // updates, so just wait for a bit before verifying that the endpoint was @@ -843,7 +822,7 @@ async fn test_edit_local_echo_with_unsupported_content() { }; // Let's edit the local echo (message) with an unsupported type (poll start). - let did_edit = timeline.edit(item, poll_start_content).await.unwrap(); + let did_edit = timeline.edit(&item.identifier(), poll_start_content).await.unwrap(); // We couldn't edit the local echo, since their content types didn't match assert!(!did_edit); @@ -863,7 +842,7 @@ async fn test_edit_local_echo_with_unsupported_content() { // Let's edit the local echo (poll start) with an unsupported type (message). let did_edit = timeline .edit( - item, + &item.identifier(), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( "edited", )), @@ -1226,87 +1205,7 @@ async fn test_pending_poll_edit() { } #[async_test] -async fn test_send_edit_by_event_id() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - let (_, mut timeline_stream) = - timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; - - let f = EventFactory::new(); - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event( - f.text_msg("Hello, World!") - .sender(client.user_id().unwrap()) - .event_id(event_id!("$original_event")), - ), - ); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - let hello_world_item = - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); - let hello_world_message = hello_world_item.content().as_message().unwrap(); - assert!(!hello_world_message.is_edited()); - assert!(hello_world_item.is_editable()); - - mock_encryption_state(&server, false).await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .respond_with( - ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$edit_event" })), - ) - .expect(1) - .mount(&server) - .await; - - timeline - .edit_by_id( - &hello_world_item.identifier(), - EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( - "Hello, Room!", - )), - ) - .await - .unwrap(); - - // Let the send queue handle the event. - yield_now().await; - - let edit_item = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => value); - - // The event itself is already known to the server. We don't currently have - // a separate edit send state. - assert_matches!(edit_item.send_state(), None); - let edit_message = edit_item.content().as_message().unwrap(); - assert_eq!(edit_message.body(), "Hello, Room!"); - assert!(edit_message.is_edited()); - - // The response to the mocked endpoint does not generate further timeline - // updates, so just wait for a bit before verifying that the endpoint was - // called. - sleep(Duration::from_millis(200)).await; - - server.verify().await; -} - -#[async_test] -async fn test_send_edit_by_non_existing_event_id() { +async fn test_send_edit_non_existing_item() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); @@ -1326,7 +1225,7 @@ async fn test_send_edit_by_non_existing_event_id() { mock_encryption_state(&server, false).await; let error = timeline - .edit_by_id( + .edit( &TimelineEventItemId::EventId(owned_event_id!("$123:example.com")), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( "Hello, Room!", @@ -1336,144 +1235,9 @@ async fn test_send_edit_by_non_existing_event_id() { .err() .unwrap(); assert_matches!(error, Error::EventNotInTimeline(_)); -} - -#[async_test] -async fn test_edit_local_echo_by_id() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - let (_, mut timeline_stream) = timeline.subscribe().await; - - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - let mounted_send = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(413).set_body_json(json!({ - "errcode": "M_TOO_LARGE", - }))) - .expect(1) - .mount_as_scoped(&server) - .await; - - // Redacting a local event works. - timeline.send(RoomMessageEventContent::text_plain("hello, just you").into()).await.unwrap(); - - assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await); - - let internal_id = item.unique_id(); - - let item = item.as_event().unwrap(); - assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); - - // We haven't set a route for sending events, so this will fail. - - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); - - let item = item.as_event().unwrap(); - assert!(item.is_local_echo()); - assert!(item.is_editable()); - - assert_matches!( - item.send_state(), - Some(EventSendState::SendingFailed { is_recoverable: false, .. }) - ); - - assert!(timeline_stream.next().now_or_never().is_none()); - - // Set up the success response before editing, since edit causes an immediate - // retry (the room's send queue is not blocked, since the one event it couldn't - // send failed in an unrecoverable way). - drop(mounted_send); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$1" }))) - .expect(1) - .mount(&server) - .await; - - // Let's edit the local echo. - let did_edit = timeline - .edit_by_id( - &item.identifier(), - EditedContent::RoomMessage(RoomMessageEventContent::text_plain("hello, world").into()), - ) - .await - .unwrap(); - - // We could edit the local echo, since it was in the failed state. - assert!(did_edit); - - // Observe local echo being replaced. - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); - - assert_eq!(item.unique_id(), internal_id); - - let item = item.as_event().unwrap(); - assert!(item.is_local_echo()); - - // The send state has been reset. - assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - - let edit_message = item.content().as_message().unwrap(); - assert_eq!(edit_message.body(), "hello, world"); - - // Observe the event being sent, and replacing the local echo. - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); - - let item = item.as_event().unwrap(); - assert!(item.is_local_echo()); - - let edit_message = item.content().as_message().unwrap(); - assert_eq!(edit_message.body(), "hello, world"); - - // No new updates. - assert!(timeline_stream.next().now_or_never().is_none()); -} - -#[async_test] -async fn test_send_edit_by_non_existing_local_id() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - - let room = client.get_room(room_id).unwrap(); - let timeline = room.timeline().await.unwrap(); - - mock_encryption_state(&server, false).await; let error = timeline - .edit_by_id( + .edit( &TimelineEventItemId::TransactionId("something".into()), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( "Hello, Room!", diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 4097ce04c46..039d593873f 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -291,7 +291,7 @@ async fn test_stale_local_echo_time_abort_edit() { // Now do a crime: try to edit the local echo. let did_edit = timeline .edit( - &local_echo, + &local_echo.identifier(), EditedContent::RoomMessage(RoomMessageEventContent::text_plain("bonjour").into()), ) .await From 59fce90943e0a4e2d77c600db50efcff9f1fbfc8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 14:37:25 +0200 Subject: [PATCH 306/979] chore(ffi): revert to using a room method to edit if a remote event couldn't be found in the timeline This maintains functionality we had prior to the previous commit: if an event's missing from the timeline (e.g. timeline's been cleared after a gappy sync response), then still allow editing it. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index dc9842c7b8a..f571ba158c8 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -31,7 +31,7 @@ use matrix_sdk::{ Error, }; use matrix_sdk_ui::timeline::{ - EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails, + self, EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails, TimelineUniqueId as SdkTimelineUniqueId, }; use mime::Mime; @@ -494,11 +494,30 @@ impl Timeline { &self, event_or_transaction_id: EventOrTransactionId, new_content: EditedContent, - ) -> Result { - self.inner - .edit(&event_or_transaction_id.try_into()?, new_content.try_into()?) + ) -> Result<(), ClientError> { + match self + .inner + .edit(&event_or_transaction_id.clone().try_into()?, new_content.clone().try_into()?) .await - .map_err(Into::into) + { + Ok(true) => Ok(()), + Ok(false) | Err(timeline::Error::EventNotInTimeline(_)) => { + // If we couldn't edit, assume it was an (remote) event that wasn't in the + // timeline, and try to edit it via the room itself. + let event_id = match event_or_transaction_id { + EventOrTransactionId::EventId { event_id } => EventId::parse(event_id)?, + EventOrTransactionId::TransactionId { .. } => { + warn!("trying to apply an edit to a local echo that doesn't exist in this timeline, aborting"); + return Ok(()); + } + }; + let room = self.inner.room(); + let edit_event = room.make_edit_event(&event_id, new_content.try_into()?).await?; + room.send_queue().send(edit_event).await?; + Ok(()) + } + Err(err) => Err(err)?, + } } pub async fn send_location( From a5f1769e28c261751f0fe9c2416d3cb9fac63513 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 17 Oct 2024 12:00:02 +0200 Subject: [PATCH 307/979] refactor(timeline): return an invalid local echo state error if a local echo disappeared I think this can't happen, but the send queue can return an error if a local echo identified by a transaction id doesn't exist anymore in the database. The only reason the latter could happen is because the local echo has been sent, in which case an update to the timeline would be dispatched, and the timeline item would have morphed into a remote echo in the meantime. So it's really rare that this would happen, and the `Timeline::redact()` method doesn't have to return a boolean to indicate success in general. Changelog: `Timeline::redact()` doesn't return a boolean; previously, it would only return false if the internal state was invalid, so a new error `RedactError::InvalidLocalEchoState` has been introduced to represent that. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 6 +---- crates/matrix-sdk-ui/src/timeline/error.rs | 6 ++++- crates/matrix-sdk-ui/src/timeline/mod.rs | 25 ++++++------------- .../tests/integration/timeline/mod.rs | 7 ++---- 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index f571ba158c8..ca8503f01be 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -629,11 +629,7 @@ impl Timeline { event_or_transaction_id: EventOrTransactionId, reason: Option, ) -> Result<(), ClientError> { - if !self.inner.redact(&(event_or_transaction_id.try_into()?), reason.as_deref()).await? { - // TODO make it a hard error instead - warn!("Couldn't redact item"); - } - Ok(()) + Ok(self.inner.redact(&(event_or_transaction_id.try_into()?), reason.as_deref()).await?) } /// Load the reply details for the given event id. diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index bc881bc97a7..fc3d0fdf611 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -76,7 +76,7 @@ pub enum Error { /// An error happened while attempting to redact an event. #[error(transparent)] - RedactError(RedactError), + RedactError(#[from] RedactError), } #[derive(Error, Debug)] @@ -88,6 +88,10 @@ pub enum RedactError { /// An error happened while attempting to redact an event. #[error(transparent)] HttpError(#[from] HttpError), + + /// The local echo we tried to abort has been lost. + #[error("Invalid state: the local echo we tried to abort has been lost.")] + InvalidLocalEchoState, } #[derive(Error, Debug)] diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 29825e5b200..1a3aa497e6e 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -551,37 +551,28 @@ impl Timeline { /// Redact an event given its [`TimelineEventItemId`] and an optional /// reason. - /// - /// # Returns - /// - /// - Returns `Ok(true)` if the redact happened. - /// - Returns `Ok(false)` if the redact targets an item that has no local - /// nor matching remote item. - /// - Returns an error if there was an issue sending the redaction event, or - /// interacting with the sending queue. pub async fn redact( &self, item_id: &TimelineEventItemId, reason: Option<&str>, - ) -> Result { + ) -> Result<(), Error> { let items = self.items().await; let Some((_pos, event)) = rfind_event_by_item_id(&items, item_id) else { - return Err(Error::RedactError(RedactError::ItemNotFound(item_id.clone()))); + return Err(RedactError::ItemNotFound(item_id.clone()).into()); }; match event.handle() { TimelineItemHandle::Remote(event_id) => { - self.room() - .redact(event_id, reason, None) - .await - .map_err(RedactError::HttpError) - .map_err(Error::RedactError)?; - Ok(true) + self.room().redact(event_id, reason, None).await.map_err(RedactError::HttpError)?; } TimelineItemHandle::Local(handle) => { - Ok(handle.abort().await.map_err(RoomSendQueueError::StorageError)?) + if !handle.abort().await.map_err(RoomSendQueueError::StorageError)? { + return Err(RedactError::InvalidLocalEchoState.into()); + } } } + + Ok(()) } /// Fetch unavailable details about the event with the given ID. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index f2d151e5bea..e9d28a84e3a 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -276,9 +276,7 @@ async fn test_redact_message() { // Redacting a remote event works. mock_redaction(event_id!("$42")).mount(&server).await; - let did_redact = - timeline.redact(&first.as_event().unwrap().identifier(), Some("inapprops")).await.unwrap(); - assert!(did_redact); + timeline.redact(&first.as_event().unwrap().identifier(), Some("inapprops")).await.unwrap(); // Redacting a local event works. timeline @@ -300,8 +298,7 @@ async fn test_redact_message() { assert_matches!(second.send_state(), Some(EventSendState::SendingFailed { .. })); // Let's redact the local echo. - let did_redact = timeline.redact(&second.identifier(), None).await.unwrap(); - assert!(did_redact); + timeline.redact(&second.identifier(), None).await.unwrap(); // Observe local echo being removed. assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); From 821fa8fa995377594e4611a21593a44660867292 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 17 Oct 2024 12:15:45 +0200 Subject: [PATCH 308/979] refactor(timeline): don't return a bool in `Timeline::edit` See previous commit for explanations. This makes for a simpler API anyways. Changelog: `Timeline::edit` doesn't return a bool anymore to indicate it couldn't manage the edit in some cases, but will return errors indicating what the cause of the error is. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 4 +- crates/matrix-sdk-ui/src/timeline/error.rs | 16 +++++++- crates/matrix-sdk-ui/src/timeline/mod.rs | 38 +++++++++++-------- .../tests/integration/timeline/edit.rs | 26 ++++++------- .../src/tests/timeline.rs | 6 +-- 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index ca8503f01be..cc7a3b285df 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -500,8 +500,8 @@ impl Timeline { .edit(&event_or_transaction_id.clone().try_into()?, new_content.clone().try_into()?) .await { - Ok(true) => Ok(()), - Ok(false) | Err(timeline::Error::EventNotInTimeline(_)) => { + Ok(()) => Ok(()), + Err(timeline::Error::EventNotInTimeline(_)) => { // If we couldn't edit, assume it was an (remote) event that wasn't in the // timeline, and try to edit it via the room itself. let event_id = match event_or_transaction_id { diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index fc3d0fdf611..53149ee4a83 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -14,7 +14,6 @@ use matrix_sdk::{ event_cache::{paginator::PaginatorError, EventCacheError}, - room::edit::EditError, send_queue::RoomSendQueueError, HttpError, }; @@ -79,6 +78,21 @@ pub enum Error { RedactError(#[from] RedactError), } +#[derive(Error, Debug)] +pub enum EditError { + /// The content types have changed. + #[error("the new content type ({new}) doesn't match that of the previous content ({original}")] + ContentMismatch { original: String, new: String }, + + /// The local echo we tried to edit has been lost. + #[error("Invalid state: the local echo we tried to abort has been lost.")] + InvalidLocalEchoState, + + /// An error happened at a lower level. + #[error(transparent)] + RoomError(#[from] matrix_sdk::room::edit::EditError), +} + #[derive(Error, Debug)] pub enum RedactError { /// Local event to redact wasn't found for transaction id diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 1a3aa497e6e..812dc0ae8f6 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -449,20 +449,12 @@ impl Timeline { /// /// Only supports events for which [`EventTimelineItem::is_editable()`] /// returns `true`. - /// - /// # Returns - /// - /// - Returns `Ok(true)` if the edit was added to the send queue. - /// - Returns `Ok(false)` if the edit targets an item that has no local nor - /// matching remote item. - /// - Returns an error if there was an issue sending the redaction event, or - /// interacting with the sending queue. #[instrument(skip(self, new_content))] pub async fn edit( &self, item_id: &TimelineEventItemId, new_content: EditedContent, - ) -> Result { + ) -> Result<(), Error> { let items = self.items().await; let Some((_pos, item)) = rfind_event_by_item_id(&items, item_id) else { return Err(Error::EventNotInTimeline(item_id.clone())); @@ -470,9 +462,13 @@ impl Timeline { match item.handle() { TimelineItemHandle::Remote(event_id) => { - let content = self.room().make_edit_event(event_id, new_content).await?; + let content = self + .room() + .make_edit_event(event_id, new_content) + .await + .map_err(EditError::RoomError)?; self.send(content).await?; - Ok(true) + Ok(()) } TimelineItemHandle::Local(handle) => { @@ -482,8 +478,11 @@ impl Timeline { if matches!(item.content, TimelineItemContent::Message(_)) { AnyMessageLikeEventContent::RoomMessage(message.into()) } else { - warn!("New content (m.room.message) doesn't match previous event content."); - return Ok(false); + return Err(EditError::ContentMismatch { + original: item.content.debug_string().to_owned(), + new: "a message".to_owned(), + } + .into()); } } EditedContent::PollStart { new_content, .. } => { @@ -494,13 +493,20 @@ impl Timeline { ), ) } else { - warn!("New content (poll start) doesn't match previous event content."); - return Ok(false); + return Err(EditError::ContentMismatch { + original: item.content.debug_string().to_owned(), + new: "a poll".to_owned(), + } + .into()); } } }; - Ok(handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)?) + if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? { + return Err(EditError::InvalidLocalEchoState.into()); + } + + Ok(()) } } } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index f3c22913253..3d848cc2116 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -30,7 +30,8 @@ use matrix_sdk_test::{ }; use matrix_sdk_ui::{ timeline::{ - Error, EventSendState, RoomExt, TimelineDetails, TimelineEventItemId, TimelineItemContent, + EditError, Error, EventSendState, RoomExt, TimelineDetails, TimelineEventItemId, + TimelineItemContent, }, Timeline, }; @@ -235,8 +236,8 @@ async fn test_edit_local_echo() { .mount(&server) .await; - // Let's edit the local echo. - let did_edit = timeline + // Editing the local echo works, since it was in the failed state. + timeline .edit( &item.identifier(), EditedContent::RoomMessage(RoomMessageEventContent::text_plain("hello, world").into()), @@ -244,9 +245,6 @@ async fn test_edit_local_echo() { .await .unwrap(); - // We could edit the local echo, since it was in the failed state. - assert!(did_edit); - // Observe local echo being replaced. assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); @@ -410,7 +408,7 @@ async fn test_send_reply_edit() { .mount(&server) .await; - let edited = timeline + timeline .edit( &reply_item.identifier(), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( @@ -419,7 +417,6 @@ async fn test_send_reply_edit() { ) .await .unwrap(); - assert!(edited); // Let the send queue handle the event. yield_now().await; @@ -521,7 +518,7 @@ async fn test_edit_to_replied_updates_reply() { .await; // If I edit the first message,… - let edited = timeline + timeline .edit( &replied_to_item.identifier(), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( @@ -530,7 +527,6 @@ async fn test_edit_to_replied_updates_reply() { ) .await .unwrap(); - assert!(edited); yield_now().await; // let the send queue handle the edit. @@ -822,10 +818,10 @@ async fn test_edit_local_echo_with_unsupported_content() { }; // Let's edit the local echo (message) with an unsupported type (poll start). - let did_edit = timeline.edit(&item.identifier(), poll_start_content).await.unwrap(); + let edit_err = timeline.edit(&item.identifier(), poll_start_content).await.unwrap_err(); // We couldn't edit the local echo, since their content types didn't match - assert!(!did_edit); + assert_matches!(edit_err, Error::EditError(EditError::ContentMismatch { .. })); timeline .send(AnyMessageLikeEventContent::UnstablePollStart(UnstablePollStartEventContent::New( @@ -840,7 +836,7 @@ async fn test_edit_local_echo_with_unsupported_content() { assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); // Let's edit the local echo (poll start) with an unsupported type (message). - let did_edit = timeline + let edit_err = timeline .edit( &item.identifier(), EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( @@ -848,10 +844,10 @@ async fn test_edit_local_echo_with_unsupported_content() { )), ) .await - .unwrap(); + .unwrap_err(); // We couldn't edit the local echo, since their content types didn't match - assert!(!did_edit); + assert_matches!(edit_err, Error::EditError(EditError::ContentMismatch { .. })); } struct PendingEditHelper { diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 039d593873f..531ca7a1d21 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -289,7 +289,8 @@ async fn test_stale_local_echo_time_abort_edit() { } // Now do a crime: try to edit the local echo. - let did_edit = timeline + // The edit works on the local echo and applies to the remote echo \o/. + timeline .edit( &local_echo.identifier(), EditedContent::RoomMessage(RoomMessageEventContent::text_plain("bonjour").into()), @@ -297,9 +298,6 @@ async fn test_stale_local_echo_time_abort_edit() { .await .unwrap(); - // The edit works on the local echo and applies to the remote echo \o/. - assert!(did_edit); - let vector_diff = timeout(Duration::from_secs(5), stream.next()).await.unwrap().unwrap(); let remote_echo = assert_matches!(vector_diff, VectorDiff::Set { index: 0, value } => value); assert!(!remote_echo.is_local_echo()); From 59c47fb22d27b69c9735b1eb76b887946a8fc46e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 17 Oct 2024 10:45:51 +0200 Subject: [PATCH 309/979] fix(ffi): don't panic when running into an unknown membership state Fixes #1254. --- bindings/matrix-sdk-ffi/src/error.rs | 5 +++ bindings/matrix-sdk-ffi/src/event.rs | 2 +- bindings/matrix-sdk-ffi/src/room.rs | 11 ++++-- bindings/matrix-sdk-ffi/src/room_info.rs | 5 ++- bindings/matrix-sdk-ffi/src/room_member.rs | 43 ++++++++++++++-------- 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index ba0198b4f0c..cdfc07a690a 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -217,3 +217,8 @@ impl From for NotificationSettingsError { Self::Generic { msg: e.to_string() } } } + +/// Something has not been implemented yet. +#[derive(thiserror::Error, Debug)] +#[error("not implemented yet")] +pub struct NotYetImplemented; diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index b5b98d792c8..8771693ab61 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -105,7 +105,7 @@ impl TryFrom for StateEventContent { let original_content = get_state_event_original_content(content)?; StateEventContent::RoomMemberContent { user_id: state_key, - membership_state: original_content.membership.into(), + membership_state: original_content.membership.try_into()?, } } AnySyncStateEvent::RoomName(_) => StateEventContent::RoomName, diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 8ecbe787623..d10aea0e4ba 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -163,7 +163,12 @@ impl Room { /// the user who invited the logged-in user to a room. pub async fn inviter(&self) -> Option { if self.inner.state() == RoomState::Invited { - self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into()) + self.inner + .invite_details() + .await + .ok() + .and_then(|a| a.inviter) + .and_then(|m| m.try_into().ok()) } else { None } @@ -273,7 +278,7 @@ impl Room { pub async fn member(&self, user_id: String) -> Result { let user_id = UserId::parse(&*user_id).context("Invalid user id.")?; let member = self.inner.get_member(&user_id).await?.context("User not found")?; - Ok(member.into()) + Ok(member.try_into().context("Unknown state membership")?) } pub async fn member_avatar_url(&self, user_id: String) -> Result, ClientError> { @@ -952,7 +957,7 @@ impl RoomMembersIterator { fn next_chunk(&self, chunk_size: u32) -> Option> { self.chunk_iterator .next(chunk_size) - .map(|members| members.into_iter().map(|m| m.into()).collect()) + .map(|members| members.into_iter().filter_map(|m| m.try_into().ok()).collect()) } } diff --git a/bindings/matrix-sdk-ffi/src/room_info.rs b/bindings/matrix-sdk-ffi/src/room_info.rs index bbc84cfe97e..3f04eed962b 100644 --- a/bindings/matrix-sdk-ffi/src/room_info.rs +++ b/bindings/matrix-sdk-ffi/src/room_info.rs @@ -90,7 +90,10 @@ impl RoomInfo { .await .ok() .and_then(|details| details.inviter) - .map(Into::into), + .map(TryInto::try_into) + .transpose() + .ok() + .flatten(), _ => None, }, heroes: room.heroes().into_iter().map(Into::into).collect(), diff --git a/bindings/matrix-sdk-ffi/src/room_member.rs b/bindings/matrix-sdk-ffi/src/room_member.rs index 96aed25a00f..03dc0b91655 100644 --- a/bindings/matrix-sdk-ffi/src/room_member.rs +++ b/bindings/matrix-sdk-ffi/src/room_member.rs @@ -1,7 +1,7 @@ use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole}; use ruma::UserId; -use crate::error::ClientError; +use crate::error::{ClientError, NotYetImplemented}; #[derive(Clone, uniffi::Enum)] pub enum MembershipState { @@ -21,23 +21,32 @@ pub enum MembershipState { Leave, } -impl From for MembershipState { - fn from(m: matrix_sdk::ruma::events::room::member::MembershipState) -> Self { +impl TryFrom for MembershipState { + type Error = NotYetImplemented; + + fn try_from( + m: matrix_sdk::ruma::events::room::member::MembershipState, + ) -> Result { match m { - matrix_sdk::ruma::events::room::member::MembershipState::Ban => MembershipState::Ban, + matrix_sdk::ruma::events::room::member::MembershipState::Ban => { + Ok(MembershipState::Ban) + } matrix_sdk::ruma::events::room::member::MembershipState::Invite => { - MembershipState::Invite + Ok(MembershipState::Invite) + } + matrix_sdk::ruma::events::room::member::MembershipState::Join => { + Ok(MembershipState::Join) } - matrix_sdk::ruma::events::room::member::MembershipState::Join => MembershipState::Join, matrix_sdk::ruma::events::room::member::MembershipState::Knock => { - MembershipState::Knock + Ok(MembershipState::Knock) } matrix_sdk::ruma::events::room::member::MembershipState::Leave => { - MembershipState::Leave + Ok(MembershipState::Leave) + } + _ => { + tracing::warn!("Other membership state change not yet implemented"); + Err(NotYetImplemented) } - _ => unimplemented!( - "Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254" - ), } } } @@ -74,18 +83,20 @@ pub struct RoomMember { pub suggested_role_for_power_level: RoomMemberRole, } -impl From for RoomMember { - fn from(m: SdkRoomMember) -> Self { - RoomMember { +impl TryFrom for RoomMember { + type Error = NotYetImplemented; + + fn try_from(m: SdkRoomMember) -> Result { + Ok(RoomMember { user_id: m.user_id().to_string(), display_name: m.display_name().map(|s| s.to_owned()), avatar_url: m.avatar_url().map(|a| a.to_string()), - membership: m.membership().clone().into(), + membership: m.membership().clone().try_into()?, is_name_ambiguous: m.name_ambiguous(), power_level: m.power_level(), normalized_power_level: m.normalized_power_level(), is_ignored: m.is_ignored(), suggested_role_for_power_level: m.suggested_role_for_power_level(), - } + }) } } From bdfe64179bd0b5ef5598774ac510d1539efc2184 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 17 Oct 2024 11:46:22 +0200 Subject: [PATCH 310/979] feat(ffi): support custom membership state value in `MembershipState` --- bindings/matrix-sdk-ffi/src/room_member.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/room_member.rs b/bindings/matrix-sdk-ffi/src/room_member.rs index 03dc0b91655..36e40743f62 100644 --- a/bindings/matrix-sdk-ffi/src/room_member.rs +++ b/bindings/matrix-sdk-ffi/src/room_member.rs @@ -19,6 +19,9 @@ pub enum MembershipState { /// The user has left. Leave, + + /// A custom membership state value. + Custom { value: String }, } impl TryFrom for MembershipState { @@ -43,6 +46,9 @@ impl TryFrom for Member matrix_sdk::ruma::events::room::member::MembershipState::Leave => { Ok(MembershipState::Leave) } + matrix_sdk::ruma::events::room::member::MembershipState::_Custom(_) => { + Ok(MembershipState::Custom { value: m.to_string() }) + } _ => { tracing::warn!("Other membership state change not yet implemented"); Err(NotYetImplemented) From c7708d6154f2c56165494280e4c14cfdf7226a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 17 Oct 2024 13:52:15 +0200 Subject: [PATCH 311/979] feature(ffi): Add optional `CreateRoomParameters::join_rule_override` This allows clients to set custom join rules for a room, as would be needed for the knock-only rooms, or restricted rooms (those that can only be joined if the user is part of some other room or space). --- bindings/matrix-sdk-ffi/src/client.rs | 112 ++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 960abbb4cb4..1c972151e9d 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -55,7 +55,8 @@ use ruma::{ }, events::{ ignored_user_list::IgnoredUserListEventContent, - room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType, + room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent}, + GlobalAccountDataEventType, }, push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat}, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName, @@ -645,7 +646,7 @@ impl Client { } pub async fn create_room(&self, request: CreateRoomParameters) -> Result { - let response = self.inner.create_room(request.into()).await?; + let response = self.inner.create_room(request.try_into()?).await?; Ok(String::from(response.room_id())) } @@ -1316,10 +1317,14 @@ pub struct CreateRoomParameters { pub avatar: Option, #[uniffi(default = None)] pub power_level_content_override: Option, + #[uniffi(default = None)] + pub join_rule_override: Option, } -impl From for create_room::v3::Request { - fn from(value: CreateRoomParameters) -> create_room::v3::Request { +impl TryFrom for create_room::v3::Request { + type Error = ClientError; + + fn try_from(value: CreateRoomParameters) -> Result { let mut request = create_room::v3::Request::new(); request.name = value.name; request.topic = value.topic; @@ -1353,6 +1358,12 @@ impl From for create_room::v3::Request { content.url = Some(url.into()); initial_state.push(InitialStateEvent::new(content).to_raw_any()); } + + if let Some(join_rule_override) = value.join_rule_override { + let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?); + initial_state.push(InitialStateEvent::new(content).to_raw_any()); + } + request.initial_state = initial_state; if let Some(power_levels) = value.power_level_content_override { @@ -1361,12 +1372,14 @@ impl From for create_room::v3::Request { request.power_level_content_override = Some(power_levels); } Err(e) => { - error!("Failed to serialize power levels, error: {e}"); + return Err(ClientError::Generic { + msg: format!("Failed to serialize power levels, error: {e}"), + }) } } } - request + Ok(request) } } @@ -1779,3 +1792,90 @@ impl From for SdkOidcPrompt { } } } + +/// The rule used for users wishing to join this room. +#[derive(uniffi::Enum)] +pub enum JoinRule { + /// Anyone can join the room without any prior action. + Public, + + /// A user who wishes to join the room must first receive an invite to the + /// room from someone already inside of the room. + Invite, + + /// Users can join the room if they are invited, or they can request an + /// invite to the room. + /// + /// They can be allowed (invited) or denied (kicked/banned) access. + Knock, + + /// Reserved but not yet implemented by the Matrix specification. + Private, + + /// Users can join the room if they are invited, or if they meet any of the + /// conditions described in a set of [`AllowRule`]s. + Restricted { rules: Vec }, + + /// Users can join the room if they are invited, or if they meet any of the + /// conditions described in a set of [`AllowRule`]s, or they can request + /// an invite to the room. + KnockRestricted { rules: Vec }, +} + +/// An allow rule which defines a condition that allows joining a room. +#[derive(uniffi::Enum)] +pub enum AllowRule { + /// Only a member of the `room_id` Room can join the one this rule is used + /// in. + RoomMembership { room_id: String }, +} + +impl TryFrom for ruma::events::room::join_rules::JoinRule { + type Error = ClientError; + + fn try_from(value: JoinRule) -> Result { + match value { + JoinRule::Public => Ok(Self::Public), + JoinRule::Invite => Ok(Self::Invite), + JoinRule::Knock => Ok(Self::Knock), + JoinRule::Private => Ok(Self::Private), + JoinRule::Restricted { rules } => { + let rules = allow_rules_from(rules)?; + Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules))) + } + JoinRule::KnockRestricted { rules } => { + let rules = allow_rules_from(rules)?; + Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules))) + } + } + } +} + +fn allow_rules_from( + value: Vec, +) -> Result, ClientError> { + let mut ret = Vec::with_capacity(value.len()); + for rule in value { + let rule: Result = rule.try_into(); + match rule { + Ok(rule) => ret.push(rule), + Err(error) => return Err(error), + } + } + Ok(ret) +} + +impl TryFrom for ruma::events::room::join_rules::AllowRule { + type Error = ClientError; + + fn try_from(value: AllowRule) -> Result { + match value { + AllowRule::RoomMembership { room_id } => { + let room_id = RoomId::parse(room_id)?; + Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new( + room_id, + ))) + } + } + } +} From 2ea114d988cc0fbdc46b137356a6474027d16203 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 15:37:49 +0200 Subject: [PATCH 312/979] chore(media): reduce indent level of upload_thumbnail by one with let-else --- crates/matrix-sdk/src/encryption/mod.rs | 22 +++++++------- crates/matrix-sdk/src/media.rs | 38 ++++++++++++------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 1438db50fcd..f07ab0b7154 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -543,24 +543,24 @@ impl Client { content_type: &mime::Mime, send_progress: SharedObservable, ) -> Result<(Option, Option>)> { - if let Some(thumbnail) = thumbnail { - let mut cursor = Cursor::new(thumbnail.data); + let Some(thumbnail) = thumbnail else { + return Ok((None, None)); + }; - let file = self - .prepare_encrypted_file(content_type, &mut cursor) - .with_send_progress_observable(send_progress) - .await?; + let mut cursor = Cursor::new(thumbnail.data); - #[rustfmt::skip] + let file = self + .prepare_encrypted_file(content_type, &mut cursor) + .with_send_progress_observable(send_progress) + .await?; + + #[rustfmt::skip] let thumbnail_info = assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) }); - Ok((Some(MediaSource::Encrypted(Box::new(file))), Some(Box::new(thumbnail_info)))) - } else { - Ok((None, None)) - } + Ok((Some(MediaSource::Encrypted(Box::new(file))), Some(Box::new(thumbnail_info)))) } /// Claim one-time keys creating new Olm sessions. diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 29e9f7b4055..42e33da9c50 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -590,25 +590,25 @@ impl Media { thumbnail: Option, send_progress: SharedObservable, ) -> Result<(Option, Option>)> { - if let Some(thumbnail) = thumbnail { - let response = self - .upload(&thumbnail.content_type, thumbnail.data) - .with_send_progress_observable(send_progress) - .await?; - let url = response.content_uri; - - let thumbnail_info = assign!( - thumbnail.info - .as_ref() - .map(|info| ThumbnailInfo::from(info.clone())) - .unwrap_or_default(), - { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } - ); - - Ok((Some(MediaSource::Plain(url)), Some(Box::new(thumbnail_info)))) - } else { - Ok((None, None)) - } + let Some(thumbnail) = thumbnail else { + return Ok((None, None)); + }; + + let response = self + .upload(&thumbnail.content_type, thumbnail.data) + .with_send_progress_observable(send_progress) + .await?; + let url = response.content_uri; + + let thumbnail_info = assign!( + thumbnail.info + .as_ref() + .map(|info| ThumbnailInfo::from(info.clone())) + .unwrap_or_default(), + { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } + ); + + Ok((Some(MediaSource::Plain(url)), Some(Box::new(thumbnail_info)))) } } From 56edc9d00f21aa625250868b19a2fe351d3d49a3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 15:41:39 +0200 Subject: [PATCH 313/979] chore(media): rename all event content values to `content` --- crates/matrix-sdk/src/encryption/mod.rs | 7 +++-- crates/matrix-sdk/src/media.rs | 42 +++++++++++-------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index f07ab0b7154..4b33b687c5e 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -502,14 +502,16 @@ impl Client { }); MessageType::Image(content) } + mime::AUDIO => { - let audio_message_event_content = AudioMessageEventContent::encrypted(body, file); + let content = AudioMessageEventContent::encrypted(body, file); MessageType::Audio(crate::media::update_audio_message_event( - audio_message_event_content, + content, content_type, config.info, )) } + mime::VIDEO => { let info = assign!(config.info.map(VideoInfo::from).unwrap_or_default(), { mimetype: Some(content_type.as_ref().to_owned()), @@ -523,6 +525,7 @@ impl Client { }); MessageType::Video(content) } + _ => { let info = assign!(config.info.map(FileInfo::from).unwrap_or_default(), { mimetype: Some(content_type.as_ref().to_owned()), diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 42e33da9c50..7b6def34f3c 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -542,45 +542,41 @@ impl Media { thumbnail_source, thumbnail_info, }); - let mut image_message_event_content = - ImageMessageEventContent::plain(body, url).info(Box::new(info)); - image_message_event_content.filename = filename; - image_message_event_content.formatted = config.formatted_caption; - MessageType::Image(image_message_event_content) + let mut content = ImageMessageEventContent::plain(body, url).info(Box::new(info)); + content.filename = filename; + content.formatted = config.formatted_caption; + MessageType::Image(content) } + mime::AUDIO => { - let mut audio_message_event_content = AudioMessageEventContent::plain(body, url); - audio_message_event_content.filename = filename; - audio_message_event_content.formatted = config.formatted_caption; - MessageType::Audio(update_audio_message_event( - audio_message_event_content, - content_type, - config.info, - )) + let mut content = AudioMessageEventContent::plain(body, url); + content.filename = filename; + content.formatted = config.formatted_caption; + MessageType::Audio(update_audio_message_event(content, content_type, config.info)) } + mime::VIDEO => { let info = assign!(config.info.map(VideoInfo::from).unwrap_or_default(), { mimetype: Some(content_type.as_ref().to_owned()), thumbnail_source, thumbnail_info }); - let mut video_message_event_content = - VideoMessageEventContent::plain(body, url).info(Box::new(info)); - video_message_event_content.filename = filename; - video_message_event_content.formatted = config.formatted_caption; - MessageType::Video(video_message_event_content) + let mut content = VideoMessageEventContent::plain(body, url).info(Box::new(info)); + content.filename = filename; + content.formatted = config.formatted_caption; + MessageType::Video(content) } + _ => { let info = assign!(config.info.map(FileInfo::from).unwrap_or_default(), { mimetype: Some(content_type.as_ref().to_owned()), thumbnail_source, thumbnail_info }); - let mut file_message_event_content = - FileMessageEventContent::plain(body, url).info(Box::new(info)); - file_message_event_content.filename = filename; - file_message_event_content.formatted = config.formatted_caption; - MessageType::File(file_message_event_content) + let mut content = FileMessageEventContent::plain(body, url).info(Box::new(info)); + content.filename = filename; + content.formatted = config.formatted_caption; + MessageType::File(content) } }) } From d8de12561b462ab9b3d84332fd4021ceecf3c0e4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 16:38:14 +0200 Subject: [PATCH 314/979] refactor(media): regroup preparation of the media message after uploading the content The tails of the prepare_attachment_message and prepare_encrypted_attachment_message were almost the same, with the one different that they were using different ctors for the `EventContent` types. In fact, all these `EventContent` types also expose a plain `new` function that can take in either an encrypted or a plain media source, so we can commonize the code there. --- crates/matrix-sdk/src/encryption/mod.rs | 82 +++--------------- crates/matrix-sdk/src/media.rs | 76 +++------------- crates/matrix-sdk/src/room/mod.rs | 110 +++++++++++++++++++++--- 3 files changed, 120 insertions(+), 148 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 4b33b687c5e..7a71f0f53a4 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -47,13 +47,7 @@ use ruma::{ uiaa::{AuthData, UiaaInfo}, }, assign, - events::room::{ - message::{ - AudioMessageEventContent, FileInfo, FileMessageEventContent, ImageMessageEventContent, - MessageType, VideoInfo, VideoMessageEventContent, - }, - ImageInfo, MediaSource, ThumbnailInfo, - }, + events::room::{MediaSource, ThumbnailInfo}, DeviceId, OwnedDeviceId, OwnedUserId, TransactionId, UserId, }; use serde::Deserialize; @@ -72,7 +66,7 @@ use self::{ verification::{SasVerification, Verification, VerificationRequest}, }; use crate::{ - attachment::{AttachmentConfig, Thumbnail}, + attachment::Thumbnail, client::{ClientInner, WeakClient}, error::HttpResult, store_locks::CrossProcessStoreLockGuard, @@ -457,18 +451,17 @@ impl Client { PrepareEncryptedFile::new(self, content_type, reader) } - /// Encrypt and upload the file to be read from `reader` and construct an - /// attachment message. + /// Encrypt and upload the file and thumbnails, and return the source + /// information. pub(crate) async fn prepare_encrypted_attachment_message( &self, - filename: &str, content_type: &mime::Mime, data: Vec, - config: AttachmentConfig, + thumbnail: Option, send_progress: SharedObservable, - ) -> Result { + ) -> Result<(MediaSource, Option, Option>)> { let upload_thumbnail = - self.upload_encrypted_thumbnail(config.thumbnail, content_type, send_progress.clone()); + self.upload_encrypted_thumbnail(thumbnail, content_type, send_progress.clone()); let upload_attachment = async { let mut cursor = Cursor::new(data); @@ -480,66 +473,11 @@ impl Client { let ((thumbnail_source, thumbnail_info), file) = try_join(upload_thumbnail, upload_attachment).await?; - // if config.caption is set, use it as body, and filename as the file name - // otherwise, body is the filename, and the filename is not set - // https://github.com/tulir/matrix-spec-proposals/blob/body-as-caption/proposals/2530-body-as-caption.md - let (body, filename) = match config.caption { - Some(caption) => (caption, Some(filename.to_owned())), - None => (filename.to_owned(), None), - }; - - Ok(match content_type.type_() { - mime::IMAGE => { - let info = assign!(config.info.map(ImageInfo::from).unwrap_or_default(), { - mimetype: Some(content_type.as_ref().to_owned()), - thumbnail_source, - thumbnail_info - }); - let content = assign!(ImageMessageEventContent::encrypted(body, file), { - info: Some(Box::new(info)), - formatted: config.formatted_caption, - filename - }); - MessageType::Image(content) - } - - mime::AUDIO => { - let content = AudioMessageEventContent::encrypted(body, file); - MessageType::Audio(crate::media::update_audio_message_event( - content, - content_type, - config.info, - )) - } - - mime::VIDEO => { - let info = assign!(config.info.map(VideoInfo::from).unwrap_or_default(), { - mimetype: Some(content_type.as_ref().to_owned()), - thumbnail_source, - thumbnail_info - }); - let content = assign!(VideoMessageEventContent::encrypted(body, file), { - info: Some(Box::new(info)), - formatted: config.formatted_caption, - filename - }); - MessageType::Video(content) - } - - _ => { - let info = assign!(config.info.map(FileInfo::from).unwrap_or_default(), { - mimetype: Some(content_type.as_ref().to_owned()), - thumbnail_source, - thumbnail_info - }); - let content = assign!(FileMessageEventContent::encrypted(body, file), { - info: Some(Box::new(info)) - }); - MessageType::File(content) - } - }) + Ok((MediaSource::Encrypted(Box::new(file)), thumbnail_source, thumbnail_info)) } + /// Uploads an encrypted thumbnail to the media repository, and returns + /// its source and extra information. async fn upload_encrypted_thumbnail( &self, thumbnail: Option, diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 7b6def34f3c..8ad1686f216 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -33,11 +33,10 @@ use ruma::{ assign, events::room::{ message::{ - AudioInfo, AudioMessageEventContent, FileInfo, FileMessageEventContent, - ImageMessageEventContent, MessageType, UnstableAudioDetailsContentBlock, - UnstableVoiceContentBlock, VideoInfo, VideoMessageEventContent, + AudioInfo, AudioMessageEventContent, UnstableAudioDetailsContentBlock, + UnstableVoiceContentBlock, }, - ImageInfo, MediaSource, ThumbnailInfo, + MediaSource, ThumbnailInfo, }, MxcUri, }; @@ -47,7 +46,7 @@ use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir}; use tokio::{fs::File as TokioFile, io::AsyncWriteExt}; use crate::{ - attachment::{AttachmentConfig, AttachmentInfo, Thumbnail}, + attachment::{AttachmentInfo, Thumbnail}, futures::SendRequest, Client, Result, TransmissionProgress, }; @@ -503,17 +502,15 @@ impl Media { Ok(()) } - /// Upload the file bytes in `data` and construct an attachment - /// message. + /// Upload the file bytes in `data` and return the source information. pub(crate) async fn prepare_attachment_message( &self, - filename: &str, content_type: &Mime, data: Vec, - config: AttachmentConfig, + thumbnail: Option, send_progress: SharedObservable, - ) -> Result { - let upload_thumbnail = self.upload_thumbnail(config.thumbnail, send_progress.clone()); + ) -> Result<(MediaSource, Option, Option>)> { + let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone()); let upload_attachment = async move { self.upload(content_type, data) @@ -525,62 +522,11 @@ impl Media { let ((thumbnail_source, thumbnail_info), response) = try_join(upload_thumbnail, upload_attachment).await?; - // if config.caption is set, use it as body, and filename as the file name - // otherwise, body is the filename, and the filename is not set - // https://github.com/tulir/matrix-spec-proposals/blob/body-as-caption/proposals/2530-body-as-caption.md - let (body, filename) = match config.caption { - Some(caption) => (caption, Some(filename.to_owned())), - None => (filename.to_owned(), None), - }; - - let url = response.content_uri; - - Ok(match content_type.type_() { - mime::IMAGE => { - let info = assign!(config.info.map(ImageInfo::from).unwrap_or_default(), { - mimetype: Some(content_type.as_ref().to_owned()), - thumbnail_source, - thumbnail_info, - }); - let mut content = ImageMessageEventContent::plain(body, url).info(Box::new(info)); - content.filename = filename; - content.formatted = config.formatted_caption; - MessageType::Image(content) - } - - mime::AUDIO => { - let mut content = AudioMessageEventContent::plain(body, url); - content.filename = filename; - content.formatted = config.formatted_caption; - MessageType::Audio(update_audio_message_event(content, content_type, config.info)) - } - - mime::VIDEO => { - let info = assign!(config.info.map(VideoInfo::from).unwrap_or_default(), { - mimetype: Some(content_type.as_ref().to_owned()), - thumbnail_source, - thumbnail_info - }); - let mut content = VideoMessageEventContent::plain(body, url).info(Box::new(info)); - content.filename = filename; - content.formatted = config.formatted_caption; - MessageType::Video(content) - } - - _ => { - let info = assign!(config.info.map(FileInfo::from).unwrap_or_default(), { - mimetype: Some(content_type.as_ref().to_owned()), - thumbnail_source, - thumbnail_info - }); - let mut content = FileMessageEventContent::plain(body, url).info(Box::new(info)); - content.filename = filename; - content.formatted = config.formatted_caption; - MessageType::File(content) - } - }) + Ok((MediaSource::Plain(response.content_uri), thumbnail_source, thumbnail_info)) } + /// Uploads an unencrypted thumbnail to the media repository, and returns + /// its source and extra information. async fn upload_thumbnail( &self, thumbnail: Option, diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index cdcc8bfad93..f44a625a974 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -69,12 +69,16 @@ use ruma::{ avatar::{self, RoomAvatarEventContent}, encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility, - message::RoomMessageEventContent, + message::{ + AudioMessageEventContent, FileInfo, FileMessageEventContent, + ImageMessageEventContent, MessageType, RoomMessageEventContent, VideoInfo, + VideoMessageEventContent, + }, name::RoomNameEventContent, power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, server_acl::RoomServerAclEventContent, topic::RoomTopicEventContent, - MediaSource, + ImageInfo, MediaSource, ThumbnailInfo, }, space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, tag::{TagInfo, TagName}, @@ -1920,43 +1924,127 @@ impl Room { let mentions = config.mentions.take(); #[cfg(feature = "e2e-encryption")] - let content = if self.is_encrypted().await? { + let (media_source, thumbnail_source, thumbnail_info) = if self.is_encrypted().await? { self.client .prepare_encrypted_attachment_message( - filename, content_type, data, - config, + config.thumbnail.take(), send_progress, ) .await? } else { self.client .media() - .prepare_attachment_message(filename, content_type, data, config, send_progress) + .prepare_attachment_message( + content_type, + data, + config.thumbnail.take(), + send_progress, + ) .await? }; #[cfg(not(feature = "e2e-encryption"))] - let content = self + let (media_source, thumbnail_source, thumbnail_info) = self .client .media() - .prepare_attachment_message(filename, content_type, data, config, send_progress) + .prepare_attachment_message(content_type, data, config.thumbnail.take(), send_progress) .await?; - let mut message = RoomMessageEventContent::new(content); + let msg_type = self.make_attachment_message( + content_type, + media_source, + thumbnail_source, + thumbnail_info, + filename, + config, + ); + + let mut content = RoomMessageEventContent::new(msg_type); if let Some(mentions) = mentions { - message = message.add_mentions(mentions); + content = content.add_mentions(mentions); } - let mut fut = self.send(message); + let mut fut = self.send(content); if let Some(txn_id) = &txn_id { fut = fut.with_transaction_id(txn_id); } fut.await } + /// Creates the inner [`MessageType`] for an already-uploaded media file + /// provided by its source. + pub(crate) fn make_attachment_message( + &self, + content_type: &Mime, + source: MediaSource, + thumbnail_source: Option, + thumbnail_info: Option>, + filename: &str, + config: AttachmentConfig, + ) -> MessageType { + // if config.caption is set, use it as body, and filename as the file name + // otherwise, body is the filename, and the filename is not set. + // https://github.com/tulir/matrix-spec-proposals/blob/body-as-caption/proposals/2530-body-as-caption.md + let (body, filename) = match config.caption { + Some(caption) => (caption, Some(filename.to_owned())), + None => (filename.to_owned(), None), + }; + + match content_type.type_() { + mime::IMAGE => { + let info = assign!(config.info.map(ImageInfo::from).unwrap_or_default(), { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_source, + thumbnail_info + }); + let content = assign!(ImageMessageEventContent::new(body, source), { + info: Some(Box::new(info)), + formatted: config.formatted_caption, + filename + }); + MessageType::Image(content) + } + + mime::AUDIO => { + let content = AudioMessageEventContent::new(body, source); + MessageType::Audio(crate::media::update_audio_message_event( + content, + content_type, + config.info, + )) + } + + mime::VIDEO => { + let info = assign!(config.info.map(VideoInfo::from).unwrap_or_default(), { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_source, + thumbnail_info + }); + let content = assign!(VideoMessageEventContent::new(body, source), { + info: Some(Box::new(info)), + formatted: config.formatted_caption, + filename + }); + MessageType::Video(content) + } + + _ => { + let info = assign!(config.info.map(FileInfo::from).unwrap_or_default(), { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_source, + thumbnail_info + }); + let content = assign!(FileMessageEventContent::new(body, source), { + info: Some(Box::new(info)) + }); + MessageType::File(content) + } + } + } + /// Update the power levels of a select set of users of this room. /// /// Issue a `power_levels` state event request to the server, changing the From 7089ff51c428cce175d0898ab58a1012d90a2f8d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 16:40:56 +0200 Subject: [PATCH 315/979] refactor(room): take the transaction id by ownership in `with_transaction_id` This allows letting the caller whether they need to clone it or not, and avoids a spurious clone in one call site. --- crates/matrix-sdk/src/encryption/mod.rs | 2 +- crates/matrix-sdk/src/room/futures.rs | 4 ++-- crates/matrix-sdk/src/room/mod.rs | 4 ++-- crates/matrix-sdk/tests/integration/encryption/backups.rs | 4 ++-- crates/matrix-sdk/tests/integration/room/joined.rs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 7a71f0f53a4..9f82e706966 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -562,7 +562,7 @@ impl Client { request: &RoomMessageRequest, ) -> Result { let content = request.content.clone(); - let txn_id = &request.txn_id; + let txn_id = request.txn_id.clone(); let room_id = &request.room_id; self.get_room(room_id) diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index 494499d87fd..396d15cd2e4 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -72,8 +72,8 @@ impl<'a> SendMessageLikeEvent<'a> { /// corresponding [`SyncMessageLikeEvent`], but only for the *sending* /// device. Other devices will not see it. This is then used to ignore /// events sent by our own device and/or to implement local echo. - pub fn with_transaction_id(mut self, txn_id: &TransactionId) -> Self { - self.transaction_id = Some(txn_id.to_owned()); + pub fn with_transaction_id(mut self, txn_id: OwnedTransactionId) -> Self { + self.transaction_id = Some(txn_id); self } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index f44a625a974..09ba7212fae 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1715,7 +1715,7 @@ impl Room { /// let txn_id = TransactionId::new(); /// /// if let Some(room) = client.get_room(&room_id) { - /// room.send(content).with_transaction_id(&txn_id).await?; + /// room.send(content).with_transaction_id(txn_id).await?; /// } /// /// // Custom events work too: @@ -1968,7 +1968,7 @@ impl Room { } let mut fut = self.send(content); - if let Some(txn_id) = &txn_id { + if let Some(txn_id) = txn_id { fut = fut.with_transaction_id(txn_id); } fut.await diff --git a/crates/matrix-sdk/tests/integration/encryption/backups.rs b/crates/matrix-sdk/tests/integration/encryption/backups.rs index 26244204a7c..6a335763ac4 100644 --- a/crates/matrix-sdk/tests/integration/encryption/backups.rs +++ b/crates/matrix-sdk/tests/integration/encryption/backups.rs @@ -687,7 +687,7 @@ async fn test_incremental_upload_of_keys() -> Result<()> { // backup let content = RoomMessageEventContent::text_plain("Hello world"); let txn_id = TransactionId::new(); - let _ = alice_room.send(content).with_transaction_id(&txn_id).await?; + let _ = alice_room.send(content).with_transaction_id(txn_id).await?; Mock::given(method("GET")) .and(path("/_matrix/client/r0/sync")) @@ -773,7 +773,7 @@ async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { // backup let content = RoomMessageEventContent::text_plain("Hello world"); let txn_id = TransactionId::new(); - let _ = alice_room.send(content).with_transaction_id(&txn_id).await?; + let _ = alice_room.send(content).with_transaction_id(txn_id).await?; // Set up sliding sync. let sliding = client diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 96b240a8535..497df17e03c 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -331,7 +331,7 @@ async fn test_room_message_send() { let content = RoomMessageEventContent::text_plain("Hello world"); let txn_id = TransactionId::new(); - let response = room.send(content).with_transaction_id(&txn_id).await.unwrap(); + let response = room.send(content).with_transaction_id(txn_id).await.unwrap(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) } From 3b33f3779fb486c7952e3e5ff5551ac68110179a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 16:45:33 +0200 Subject: [PATCH 316/979] chore(media): rename upload methods to make their intents clearer --- crates/matrix-sdk/src/encryption/mod.rs | 2 +- crates/matrix-sdk/src/media.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 9f82e706966..00b1b10d586 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -453,7 +453,7 @@ impl Client { /// Encrypt and upload the file and thumbnails, and return the source /// information. - pub(crate) async fn prepare_encrypted_attachment_message( + pub(crate) async fn upload_encrypted_media_and_thumbnail( &self, content_type: &mime::Mime, data: Vec, diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 8ad1686f216..d54d3659a69 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -503,7 +503,7 @@ impl Media { } /// Upload the file bytes in `data` and return the source information. - pub(crate) async fn prepare_attachment_message( + pub(crate) async fn upload_plain_media_and_thumbnail( &self, content_type: &Mime, data: Vec, diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 09ba7212fae..974ad241eba 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1926,7 +1926,7 @@ impl Room { #[cfg(feature = "e2e-encryption")] let (media_source, thumbnail_source, thumbnail_info) = if self.is_encrypted().await? { self.client - .prepare_encrypted_attachment_message( + .upload_encrypted_media_and_thumbnail( content_type, data, config.thumbnail.take(), @@ -1936,7 +1936,7 @@ impl Room { } else { self.client .media() - .prepare_attachment_message( + .upload_plain_media_and_thumbnail( content_type, data, config.thumbnail.take(), @@ -1949,7 +1949,12 @@ impl Room { let (media_source, thumbnail_source, thumbnail_info) = self .client .media() - .prepare_attachment_message(content_type, data, config.thumbnail.take(), send_progress) + .upload_plain_media_and_thumbnail( + content_type, + data, + config.thumbnail.take(), + send_progress, + ) .await?; let msg_type = self.make_attachment_message( @@ -1976,7 +1981,7 @@ impl Room { /// Creates the inner [`MessageType`] for an already-uploaded media file /// provided by its source. - pub(crate) fn make_attachment_message( + fn make_attachment_message( &self, content_type: &Mime, source: MediaSource, From dc4c6b4d739db1fc2d5855333395c78934a29a47 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 16:50:47 +0200 Subject: [PATCH 317/979] refactor(media): inline `update_audio_message_event` into its unique caller The name wasn't very descriptive, and it's tweaking the content, so let's do that in place, instead of deferring to another method somewhere else in the codebase. --- crates/matrix-sdk/src/media.rs | 32 ++----------------------------- crates/matrix-sdk/src/room/mod.rs | 31 +++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index d54d3659a69..02fd602f0da 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -31,13 +31,7 @@ use ruma::{ MatrixVersion, }, assign, - events::room::{ - message::{ - AudioInfo, AudioMessageEventContent, UnstableAudioDetailsContentBlock, - UnstableVoiceContentBlock, - }, - MediaSource, ThumbnailInfo, - }, + events::room::{MediaSource, ThumbnailInfo}, MxcUri, }; #[cfg(not(target_arch = "wasm32"))] @@ -45,11 +39,7 @@ use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir}; #[cfg(not(target_arch = "wasm32"))] use tokio::{fs::File as TokioFile, io::AsyncWriteExt}; -use crate::{ - attachment::{AttachmentInfo, Thumbnail}, - futures::SendRequest, - Client, Result, TransmissionProgress, -}; +use crate::{attachment::Thumbnail, futures::SendRequest, Client, Result, TransmissionProgress}; /// A conservative upload speed of 1Mbps const DEFAULT_UPLOAD_SPEED: u64 = 125_000; @@ -553,21 +543,3 @@ impl Media { Ok((Some(MediaSource::Plain(url)), Some(Box::new(thumbnail_info)))) } } - -pub(crate) fn update_audio_message_event( - mut audio_message_event_content: AudioMessageEventContent, - content_type: &Mime, - info: Option, -) -> AudioMessageEventContent { - if let Some(AttachmentInfo::Voice { audio_info, waveform: Some(waveform_vec) }) = &info { - if let Some(duration) = audio_info.duration { - let waveform = waveform_vec.iter().map(|v| (*v).into()).collect(); - audio_message_event_content.audio = - Some(UnstableAudioDetailsContentBlock::new(duration, waveform)); - } - audio_message_event_content.voice = Some(UnstableVoiceContentBlock::new()); - } - - let audio_info = assign!(info.map(AudioInfo::from).unwrap_or_default(), {mimetype: Some(content_type.as_ref().to_owned()), }); - audio_message_event_content.info(Box::new(audio_info)) -} diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 974ad241eba..a0bf0ced59b 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -70,8 +70,9 @@ use ruma::{ encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility, message::{ - AudioMessageEventContent, FileInfo, FileMessageEventContent, - ImageMessageEventContent, MessageType, RoomMessageEventContent, VideoInfo, + AudioInfo, AudioMessageEventContent, FileInfo, FileMessageEventContent, + ImageMessageEventContent, MessageType, RoomMessageEventContent, + UnstableAudioDetailsContentBlock, UnstableVoiceContentBlock, VideoInfo, VideoMessageEventContent, }, name::RoomNameEventContent, @@ -108,7 +109,7 @@ pub use self::{ #[cfg(doc)] use crate::event_cache::EventCache; use crate::{ - attachment::AttachmentConfig, + attachment::{AttachmentConfig, AttachmentInfo}, client::WeakClient, config::RequestConfig, error::{BeaconError, WrongRoomState}, @@ -2014,12 +2015,24 @@ impl Room { } mime::AUDIO => { - let content = AudioMessageEventContent::new(body, source); - MessageType::Audio(crate::media::update_audio_message_event( - content, - content_type, - config.info, - )) + let mut content = AudioMessageEventContent::new(body, source); + + if let Some(AttachmentInfo::Voice { audio_info, waveform: Some(waveform_vec) }) = + &config.info + { + if let Some(duration) = audio_info.duration { + let waveform = waveform_vec.iter().map(|v| (*v).into()).collect(); + content.audio = + Some(UnstableAudioDetailsContentBlock::new(duration, waveform)); + } + content.voice = Some(UnstableVoiceContentBlock::new()); + } + + let mut audio_info = config.info.map(AudioInfo::from).unwrap_or_default(); + audio_info.mimetype = Some(content_type.as_ref().to_owned()); + let content = content.info(Box::new(audio_info)); + + MessageType::Audio(content) } mime::VIDEO => { From 1ce5160846365451cf5cfe93000a73e67eeb1299 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 17:03:36 +0200 Subject: [PATCH 318/979] refactor(ffi): introduce a `parse_mime` function to avoid code repetition --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 70 ++++++++++++--------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index cc7a3b285df..361b95dc013 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -143,6 +143,11 @@ impl Timeline { } } +fn parse_mime(mimetype: Option) -> Result { + let mime_str = mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; + mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType) +} + #[matrix_sdk_ffi_macros::export] impl Timeline { pub async fn add_listener(&self, listener: Box) -> Arc { @@ -264,11 +269,6 @@ impl Timeline { progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let mime_str = - image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; - let mime_type = - mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let base_image_info = BaseImageInfo::try_from(&image_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -285,7 +285,13 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, mime_type, attachment_config, progress_watcher).await + self.send_attachment( + url, + parse_mime(image_info.mimetype)?, + attachment_config, + progress_watcher, + ) + .await })) } @@ -299,11 +305,6 @@ impl Timeline { progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let mime_str = - video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; - let mime_type = - mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -320,7 +321,13 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, mime_type, attachment_config, progress_watcher).await + self.send_attachment( + url, + parse_mime(video_info.mimetype)?, + attachment_config, + progress_watcher, + ) + .await })) } @@ -333,11 +340,6 @@ impl Timeline { progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let mime_str = - audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; - let mime_type = - mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -347,7 +349,13 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, mime_type, attachment_config, progress_watcher).await + self.send_attachment( + url, + parse_mime(audio_info.mimetype)?, + attachment_config, + progress_watcher, + ) + .await })) } @@ -361,11 +369,6 @@ impl Timeline { progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let mime_str = - audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; - let mime_type = - mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -376,7 +379,13 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, mime_type, attachment_config, progress_watcher).await + self.send_attachment( + url, + parse_mime(audio_info.mimetype)?, + attachment_config, + progress_watcher, + ) + .await })) } @@ -387,18 +396,19 @@ impl Timeline { progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let mime_str = - file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; - let mime_type = - mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let base_file_info: BaseFileInfo = BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?; let attachment_info = AttachmentInfo::File(base_file_info); let attachment_config = AttachmentConfig::new().info(attachment_info); - self.send_attachment(url, mime_type, attachment_config, progress_watcher).await + self.send_attachment( + url, + parse_mime(file_info.mimetype)?, + attachment_config, + progress_watcher, + ) + .await })) } From 41d392f89935809b6cb8298759fc9316bd9153c8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 17:14:26 +0200 Subject: [PATCH 319/979] refactor(ffi): commonize creation of the attachment with a thumbnail --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 94 ++++++++++----------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 361b95dc013..f3294f7a2a2 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -98,29 +98,6 @@ impl Timeline { unsafe { Arc::from_raw(Arc::into_raw(inner) as _) } } - fn build_thumbnail_info( - &self, - thumbnail_url: String, - thumbnail_info: ThumbnailInfo, - ) -> Result { - let thumbnail_data = - fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?; - - let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info) - .map_err(|_| RoomError::InvalidAttachmentData)?; - - let mime_str = - thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; - let mime_type = - mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - - Ok(Thumbnail { - data: thumbnail_data, - content_type: mime_type, - info: Some(base_thumbnail_info), - }) - } - async fn send_attachment( &self, filename: String, @@ -148,6 +125,41 @@ fn parse_mime(mimetype: Option) -> Result { mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType) } +fn build_thumbnail_info( + thumbnail_url: Option, + thumbnail_info: Option, +) -> Result { + match (thumbnail_url, thumbnail_info) { + (None, None) => Ok(AttachmentConfig::new()), + + (Some(thumbnail_url), Some(thumbnail_info)) => { + let thumbnail_data = + fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?; + + let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info) + .map_err(|_| RoomError::InvalidAttachmentData)?; + + let mime_str = + thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; + let mime_type = + mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; + + let thumbnail = Thumbnail { + data: thumbnail_data, + content_type: mime_type, + info: Some(base_thumbnail_info), + }; + + Ok(AttachmentConfig::with_thumbnail(thumbnail)) + } + + _ => { + warn!("Ignoring thumbnail because either the thumbnail URL or info isn't defined"); + Ok(AttachmentConfig::new()) + } + } +} + #[matrix_sdk_ffi_macros::export] impl Timeline { pub async fn add_listener(&self, listener: Box) -> Arc { @@ -271,19 +283,12 @@ impl Timeline { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_image_info = BaseImageInfo::try_from(&image_info) .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::Image(base_image_info); - let attachment_config = match (thumbnail_url, image_info.thumbnail_info) { - (Some(thumbnail_url), Some(thumbnail_image_info)) => { - let thumbnail = - self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?; - AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info) - } - _ => AttachmentConfig::new().info(attachment_info), - } - .caption(caption) - .formatted_caption(formatted_caption.map(Into::into)); + let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)? + .info(attachment_info) + .caption(caption) + .formatted_caption(formatted_caption.map(Into::into)); self.send_attachment( url, @@ -307,19 +312,12 @@ impl Timeline { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::Video(base_video_info); - let attachment_config = match (thumbnail_url, video_info.thumbnail_info) { - (Some(thumbnail_url), Some(thumbnail_image_info)) => { - let thumbnail = - self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?; - AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info) - } - _ => AttachmentConfig::new().info(attachment_info), - } - .caption(caption) - .formatted_caption(formatted_caption.map(Into::into)); + let attachment_config = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)? + .info(attachment_info) + .caption(caption) + .formatted_caption(formatted_caption.map(Into::into)); self.send_attachment( url, @@ -342,8 +340,8 @@ impl Timeline { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::Audio(base_audio_info); + let attachment_config = AttachmentConfig::new() .info(attachment_info) .caption(caption) @@ -371,9 +369,9 @@ impl Timeline { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) }; + let attachment_config = AttachmentConfig::new() .info(attachment_info) .caption(caption) @@ -398,8 +396,8 @@ impl Timeline { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_file_info: BaseFileInfo = BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::File(base_file_info); + let attachment_config = AttachmentConfig::new().info(attachment_info); self.send_attachment( From 65ed4f3f2263e373729d7563a499dfe6f97afb00 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 17:15:52 +0200 Subject: [PATCH 320/979] refactor(ffi): push further and inline `parse_mime` into the same caller --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 55 ++++++--------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index f3294f7a2a2..e7ce10c17c0 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -101,10 +101,14 @@ impl Timeline { async fn send_attachment( &self, filename: String, - mime_type: Mime, + mime_type: Option, attachment_config: AttachmentConfig, progress_watcher: Option>, ) -> Result<(), RoomError> { + let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; + let mime_type = + mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; + let request = self.inner.send_attachment(filename, mime_type, attachment_config); if let Some(progress_watcher) = progress_watcher { let mut subscriber = request.subscribe_to_send_progress(); @@ -120,11 +124,6 @@ impl Timeline { } } -fn parse_mime(mimetype: Option) -> Result { - let mime_str = mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; - mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType) -} - fn build_thumbnail_info( thumbnail_url: Option, thumbnail_info: Option, @@ -290,13 +289,8 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - parse_mime(image_info.mimetype)?, - attachment_config, - progress_watcher, - ) - .await + self.send_attachment(url, image_info.mimetype, attachment_config, progress_watcher) + .await })) } @@ -319,13 +313,8 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - parse_mime(video_info.mimetype)?, - attachment_config, - progress_watcher, - ) - .await + self.send_attachment(url, video_info.mimetype, attachment_config, progress_watcher) + .await })) } @@ -347,13 +336,8 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - parse_mime(audio_info.mimetype)?, - attachment_config, - progress_watcher, - ) - .await + self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) + .await })) } @@ -377,13 +361,8 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - parse_mime(audio_info.mimetype)?, - attachment_config, - progress_watcher, - ) - .await + self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) + .await })) } @@ -400,13 +379,7 @@ impl Timeline { let attachment_config = AttachmentConfig::new().info(attachment_info); - self.send_attachment( - url, - parse_mime(file_info.mimetype)?, - attachment_config, - progress_watcher, - ) - .await + self.send_attachment(url, file_info.mimetype, attachment_config, progress_watcher).await })) } From 89183a3d4bb9a8b2e114e1e9ccf77aa207a82919 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 17:21:39 +0200 Subject: [PATCH 321/979] doc(timeline): rejigger a doc comment around sending attachments --- crates/matrix-sdk-ui/src/timeline/mod.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 812dc0ae8f6..4e79dd12054 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -529,22 +529,21 @@ impl Timeline { Ok(()) } - /// Sends an attachment to the room. It does not currently support local - /// echoes + /// Sends an attachment to the room. + /// + /// It does not currently support local echoes. /// /// If the encryption feature is enabled, this method will transparently /// encrypt the room message if the room is encrypted. /// /// # Arguments /// - /// * `path` - The path of the file to be sent + /// * `path` - The path of the file to be sent. /// - /// * `mime_type` - The attachment's mime type + /// * `mime_type` - The attachment's mime type. /// /// * `config` - An attachment configuration object containing details about - /// the attachment - /// - /// like a thumbnail, its size, duration etc. + /// the attachment like a thumbnail, its size, duration etc. #[instrument(skip_all)] pub fn send_attachment( &self, From 08152bd9fc80fddc5ba25547901dca635c5ec1ee Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 16 Oct 2024 17:31:18 +0200 Subject: [PATCH 322/979] refactor(sdk)!: rename `PrepareEncryptedFile` et al. to `UploadEncryptedFile` Changelog: Renamed `PrepareEncryptedFile` and `Client::prepare_encrypted_file` to `UploadEncryptedFile` and `Client::upload_encrypted_file`. --- crates/matrix-sdk/src/encryption/futures.rs | 8 ++++---- crates/matrix-sdk/src/encryption/mod.rs | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/futures.rs b/crates/matrix-sdk/src/encryption/futures.rs index 30ef5da23cc..a31c84851f5 100644 --- a/crates/matrix-sdk/src/encryption/futures.rs +++ b/crates/matrix-sdk/src/encryption/futures.rs @@ -27,16 +27,16 @@ use ruma::events::room::{EncryptedFile, EncryptedFileInit}; use crate::{Client, Result, TransmissionProgress}; -/// Future returned by [`Client::prepare_encrypted_file`]. +/// Future returned by [`Client::upload_encrypted_file`]. #[allow(missing_debug_implementations)] -pub struct PrepareEncryptedFile<'a, R: ?Sized> { +pub struct UploadEncryptedFile<'a, R: ?Sized> { client: &'a Client, content_type: &'a mime::Mime, reader: &'a mut R, send_progress: SharedObservable, } -impl<'a, R: ?Sized> PrepareEncryptedFile<'a, R> { +impl<'a, R: ?Sized> UploadEncryptedFile<'a, R> { pub(crate) fn new(client: &'a Client, content_type: &'a mime::Mime, reader: &'a mut R) -> Self { Self { client, content_type, reader, send_progress: Default::default() } } @@ -63,7 +63,7 @@ impl<'a, R: ?Sized> PrepareEncryptedFile<'a, R> { } } -impl<'a, R> IntoFuture for PrepareEncryptedFile<'a, R> +impl<'a, R> IntoFuture for UploadEncryptedFile<'a, R> where R: Read + Send + ?Sized + 'a, { diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 00b1b10d586..0466716b3e6 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -58,7 +58,7 @@ use vodozemac::Curve25519PublicKey; use self::{ backups::{types::BackupClientState, Backups}, - futures::PrepareEncryptedFile, + futures::UploadEncryptedFile, identities::{Device, DeviceUpdates, IdentityUpdates, UserDevices, UserIdentity}, recovery::{Recovery, RecoveryState}, secret_storage::SecretStorage, @@ -438,17 +438,17 @@ impl Client { /// # let client = Client::new(homeserver).await?; /// # let room = client.get_room(&room_id!("!test:example.com")).unwrap(); /// let mut reader = std::io::Cursor::new(b"Hello, world!"); - /// let encrypted_file = client.prepare_encrypted_file(&mime::TEXT_PLAIN, &mut reader).await?; + /// let encrypted_file = client.upload_encrypted_file(&mime::TEXT_PLAIN, &mut reader).await?; /// /// room.send(CustomEventContent { encrypted_file }).await?; /// # anyhow::Ok(()) }; /// ``` - pub fn prepare_encrypted_file<'a, R: Read + ?Sized + 'a>( + pub fn upload_encrypted_file<'a, R: Read + ?Sized + 'a>( &'a self, content_type: &'a mime::Mime, reader: &'a mut R, - ) -> PrepareEncryptedFile<'a, R> { - PrepareEncryptedFile::new(self, content_type, reader) + ) -> UploadEncryptedFile<'a, R> { + UploadEncryptedFile::new(self, content_type, reader) } /// Encrypt and upload the file and thumbnails, and return the source @@ -465,7 +465,7 @@ impl Client { let upload_attachment = async { let mut cursor = Cursor::new(data); - self.prepare_encrypted_file(content_type, &mut cursor) + self.upload_encrypted_file(content_type, &mut cursor) .with_send_progress_observable(send_progress) .await }; @@ -491,7 +491,7 @@ impl Client { let mut cursor = Cursor::new(thumbnail.data); let file = self - .prepare_encrypted_file(content_type, &mut cursor) + .upload_encrypted_file(content_type, &mut cursor) .with_send_progress_observable(send_progress) .await?; From 350a26cee992a021b29c696d6ab7d9f245c9e92d Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 15 Oct 2024 16:11:20 +0100 Subject: [PATCH 323/979] refactor(crypto): Extract a test helper function for simulating verification --- .../matrix-sdk-crypto/src/identities/user.rs | 169 ++++++++++++++---- crates/matrix-sdk-crypto/src/lib.rs | 4 +- .../group_sessions/share_strategy.rs | 57 +++--- 3 files changed, 163 insertions(+), 67 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 96c7e5677d5..02c080a413b 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -1189,7 +1189,15 @@ where #[cfg(any(test, feature = "testing"))] #[allow(dead_code)] pub(crate) mod testing { - use ruma::{api::client::keys::get_keys::v3::Response as KeyQueryResponse, user_id}; + use matrix_sdk_test::ruma_response_from_json; + use ruma::{ + api::client::keys::{ + get_keys::v3::Response as KeyQueryResponse, + upload_signatures::v3::Request as SignatureUploadRequest, + }, + user_id, UserId, + }; + use serde_json::json; use super::{OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData}; #[cfg(test)] @@ -1266,6 +1274,117 @@ pub(crate) mod testing { OtherUserIdentityData::new(master_key.try_into().unwrap(), self_signing.try_into().unwrap()) .unwrap() } + + /// When we want to test identities that are verified, we need to simulate + /// the verification process. This function supports that by simulating + /// what happens when a successful verification dance happens and + /// providing the /keys/query response we would get when that happened. + /// + /// signature_upload_request will be the result of calling + /// [`super::OtherUserIdentity::verify`]. + /// + /// # Example + /// + /// ```ignore + /// let signature_upload_request = their_identity.verify().await.unwrap(); + /// + /// let msk_json = json!({ + /// "their_user_id": { + /// "keys": { "ed25519:blah": "blah" } + /// "signatures": { + /// "their_user_id": { "ed25519:blah": "blah", ... } + /// } + /// "usage": [ "master" ], + /// "user_id": "their_user_id" + /// } + /// }); + /// + /// let ssk_json = json!({ + /// "their_user_id": { + /// "keys": { "ed25519:blah": "blah" }, + /// "signatures": { + /// "their_user_id": { "ed25519:blah": "blah" } + /// }, + /// "usage": [ "self_signing" ], + /// "user_id": "their_user_id" + /// } + /// }) + /// + /// let response = simulate_key_query_response_for_verification( + /// signature_upload_request, + /// my_identity, + /// my_user_id, + /// their_user_id, + /// msk_json, + /// ssk_json + /// ).await; + /// + /// olm_machine + /// .mark_request_as_sent( + /// &TransactionId::new(), + /// crate::IncomingResponse::KeysQuery(&kq_response), + /// ) + /// .await + /// .unwrap(); + /// ``` + pub fn simulate_key_query_response_for_verification( + signature_upload_request: SignatureUploadRequest, + my_identity: OwnUserIdentity, + my_user_id: &UserId, + their_user_id: &UserId, + msk_json: serde_json::Value, + ssk_json: serde_json::Value, + ) -> KeyQueryResponse { + // Find the signed key inside the SignatureUploadRequest + let cross_signing_key: CrossSigningKey = serde_json::from_str( + signature_upload_request + .signed_keys + .get(their_user_id) + .expect("Signature upload request should contain a key for their user ID") + .iter() + .next() + .expect("There should be a key in the signature upload request") + .1 + .get(), + ) + .expect("Should not fail to deserialize the key"); + + // Find their master key that we want to update inside their msk JSON + let mut their_msk: CrossSigningKey = serde_json::from_value( + msk_json.get(their_user_id.as_str()).expect("msk should contain their user ID").clone(), + ) + .expect("Should not fail to deserialize msk"); + + // Find our own user signing key + let my_user_signing_key_id = my_identity + .user_signing_key() + .keys() + .iter() + .next() + .expect("There should be a user signing key") + .0; + + // Add the signature from the SignatureUploadRequest to their master key, under + // our user ID + their_msk.signatures.add_signature( + my_user_id.to_owned(), + my_user_signing_key_id.to_owned(), + cross_signing_key + .signatures + .get_signature(my_user_id, my_user_signing_key_id) + .expect("There should be a signature for our user"), + ); + + // Create a JSON response as if the verification has happened + ruma_response_from_json(&json!({ + "device_keys": {}, // Don't need devices here, even though they would exist + "failures": {}, + "master_keys": { + their_user_id: their_msk, + }, + "self_signing_keys": ssk_json, + })) + } } #[cfg(test)] @@ -1273,11 +1392,8 @@ pub(crate) mod tests { use std::{collections::HashMap, sync::Arc}; use assert_matches::assert_matches; - use matrix_sdk_test::{async_test, ruma_response_from_json, test_json}; - use ruma::{ - api::client::keys::get_keys::v3::Response as KeyQueryResponse, device_id, user_id, - TransactionId, - }; + use matrix_sdk_test::{async_test, test_json}; + use ruma::{device_id, user_id, TransactionId}; use serde_json::{json, Value}; use tokio::sync::Mutex; @@ -1288,7 +1404,12 @@ pub(crate) mod tests { }; use crate::{ identities::{ - manager::testing::own_key_query, user::OtherUserIdentityDataSerializer, Device, + manager::testing::own_key_query, + user::{ + testing::simulate_key_query_response_for_verification, + OtherUserIdentityDataSerializer, + }, + Device, }, olm::{Account, PrivateCrossSigningIdentity}, store::{CryptoStoreWrapper, MemoryStore}, @@ -1610,7 +1731,6 @@ pub(crate) mod tests { machine.bootstrap_cross_signing(false).await.unwrap(); let my_id = machine.get_identity(my_user_id, None).await.unwrap().unwrap().own().unwrap(); - let usk_key_id = my_id.inner.user_signing_key().keys().iter().next().unwrap().0; let keys_query = DataSet::key_query_with_identity_a(); let txn_id = TransactionId::new(); @@ -1632,33 +1752,14 @@ pub(crate) mod tests { // Manually verify for the purpose of this test let sig_upload = other_identity.verify().await.unwrap(); - let raw_extracted = - sig_upload.signed_keys.get(other_user_id).unwrap().iter().next().unwrap().1.get(); - - let new_signature: CrossSigningKey = serde_json::from_str(raw_extracted).unwrap(); - - let mut msk_to_update: CrossSigningKey = - serde_json::from_value(DataSet::msk_b().get("@bob:localhost").unwrap().clone()) - .unwrap(); - - msk_to_update.signatures.add_signature( - my_user_id.to_owned(), - usk_key_id.to_owned(), - new_signature.signatures.get_signature(my_user_id, usk_key_id).unwrap(), + let kq_response = simulate_key_query_response_for_verification( + sig_upload, + my_id, + my_user_id, + other_user_id, + DataSet::msk_b(), + DataSet::ssk_b(), ); - - // we want to update bob device keys with the new signature - let data = json!({ - "device_keys": {}, // For the purpose of this test we don't need devices here - "failures": {}, - "master_keys": { - DataSet::user_id(): msk_to_update - , - }, - "self_signing_keys": DataSet::ssk_b(), - }); - - let kq_response: KeyQueryResponse = ruma_response_from_json(&data); machine.mark_request_as_sent(&TransactionId::new(), &kq_response).await.unwrap(); // The identity should not need any user approval now diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index b6c50526876..9a2afa8633b 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -39,7 +39,9 @@ mod verification; pub mod testing { pub use crate::identities::{ device::testing::get_device, - user::testing::{get_other_identity, get_own_identity}, + user::testing::{ + get_other_identity, get_own_identity, simulate_key_query_response_for_verification, + }, }; } diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 1f0c0e3647c..4797fbe60c1 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -527,6 +527,7 @@ mod tests { use ruma::{ device_id, events::room::history_visibility::HistoryVisibility, room_id, TransactionId, }; + use serde_json::json; use crate::{ error::SessionRecipientCollectionError, @@ -534,6 +535,7 @@ mod tests { session_manager::{ group_sessions::share_strategy::collect_session_recipients, CollectStrategy, }, + testing::simulate_key_query_response_for_verification, types::events::room_key_withheld::WithheldCode, CrossSigningKeyExport, EncryptionSettings, LocalTrust, OlmError, OlmMachine, }; @@ -1337,41 +1339,32 @@ mod tests { .verify() .await .unwrap(); - let raw_extracted = - verification_request.signed_keys.get(user2).unwrap().iter().next().unwrap().1.get(); - let signed_key: crate::types::CrossSigningKey = - serde_json::from_str(raw_extracted).unwrap(); - let new_signatures = signed_key.signatures.get(KeyDistributionTestData::me_id()).unwrap(); - let mut master_key = machine - .get_identity(user2, None) + + let master_key = + &machine.get_identity(user2, None).await.unwrap().unwrap().other().unwrap().master_key; + + let my_identity = machine + .get_identity(KeyDistributionTestData::me_id(), None) .await - .unwrap() - .unwrap() - .other() - .unwrap() - .master_key - .as_ref() - .clone(); - - for (key_id, signature) in new_signatures.iter() { - master_key.as_mut().signatures.add_signature( - KeyDistributionTestData::me_id().to_owned(), - key_id.to_owned(), - signature.as_ref().unwrap().ed25519().unwrap(), - ); - } - let json = serde_json::json!({ - "device_keys": {}, - "failures": {}, - "master_keys": { - user2: master_key, - }, - "user_signing_keys": {}, - "self_signing_keys": MaloIdentityChangeDataSet::updated_key_query().self_signing_keys, - } + .expect("Should not fail to find own identity") + .expect("Our own identity should not be missing") + .own() + .expect("Our own identity should be of type Own"); + + let msk = json!({ user2: serde_json::to_value(master_key).expect("Should not fail to serialize")}); + let ssk = + serde_json::to_value(&MaloIdentityChangeDataSet::updated_key_query().self_signing_keys) + .expect("Should not fail to serialize"); + + let kq_response = simulate_key_query_response_for_verification( + verification_request, + my_identity, + KeyDistributionTestData::me_id(), + user2, + msk, + ssk, ); - let kq_response = matrix_sdk_test::ruma_response_from_json(&json); machine .mark_request_as_sent( &TransactionId::new(), From ad677cb6f2ee5412162b4a6b18ed24a5b6a33fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 18 Oct 2024 12:31:45 +0200 Subject: [PATCH 324/979] chore(ffi): Add optional `canonical_alias` field to `CreateRoomParameters` --- bindings/matrix-sdk-ffi/src/client.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 1c972151e9d..b04d9ca5943 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1319,6 +1319,8 @@ pub struct CreateRoomParameters { pub power_level_content_override: Option, #[uniffi(default = None)] pub join_rule_override: Option, + #[uniffi(default = None)] + pub canonical_alias: Option, } impl TryFrom for create_room::v3::Request { @@ -1331,6 +1333,7 @@ impl TryFrom for create_room::v3::Request { request.is_direct = value.is_direct; request.visibility = value.visibility.into(); request.preset = Some(value.preset.into()); + request.room_alias_name = value.canonical_alias; request.invite = match value.invite { Some(invite) => invite .iter() From 1750bf597f72aac6f28daa697ed03b6e8cf372e9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 9 Oct 2024 15:45:51 +0200 Subject: [PATCH 325/979] test(sdk): Fix a comment. --- crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs index edf3f992127..d6ccc5a0ef9 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs @@ -495,7 +495,7 @@ mod tests { // | d | e | f | g | h | i | // +---+---+---+---+---+---+ // - // “main” will have its index updated from 3 to 0. + // “main” will have its index updated from 0 to 3. // “other” will have its index updated from 6 to 3. { let updates = linked_chunk.updates().unwrap(); From a7f69973c238173082656997cb276646ac1e0f65 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 9 Oct 2024 17:39:13 +0200 Subject: [PATCH 326/979] feat(sdk): Dropping a `UpdatesSubscriber` release the reader token for the GC. The event cache stores its events in a linked chunk. The linked chunk supports updates (`ObservableUpdates`) via `LinkedChunk::updates()`. This `ObservableUpdates` receives all updates that are happening inside the `LinkedChunk`. An `ObservableUpdates` wraps `UpdatesInner`, which is the real logic to handle multiple update readers. Each reader has a unique `ReaderToken`. `UpdatesInner` has a garbage collector that drops all updates that are read by all readers. And here comes the problem. A category of readers are `UpdatesSubscriber`, returned by `ObservableUpdates::subscribe()`. When an `UpdatesSubscriber` is dropped, its reader token was still alive, thus preventing the garbage collector to clear all its pending updates: they were kept in memory for the eternity. This patch implements `Drop` for `UpdatesSubscriber` to correctly remove its `ReaderToken` from `UpdatesInner`. This patch also adds a test that runs multiple subscribers, and when one is dropped, its pending updates are collected by the garbage collector. --- .../src/event_cache/linked_chunk/updates.rs | 198 +++++++++++++++++- 1 file changed, 188 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs index d6ccc5a0ef9..68da551c640 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs @@ -322,6 +322,22 @@ where } } +impl Drop for UpdatesSubscriber { + fn drop(&mut self) { + // Remove `Self::token` from `UpdatesInner::last_index_per_reader`. + // This is important so that the garbage collector can do its jobs correctly + // without a dead dangling reader token. + if let Some(updates) = self.updates.upgrade() { + let mut updates = updates.write().unwrap(); + + // Remove the reader token from `UpdatesInner`. + // It's safe to ignore the result of `remove` here: `None` means the token was + // already removed (note: it should be unreachable). + let _ = updates.last_index_per_reader.remove(&self.token); + } + } +} + #[cfg(test)] mod tests { use std::{ @@ -563,19 +579,19 @@ mod tests { } } - #[test] - fn test_updates_stream() { - use super::Update::*; + struct CounterWaker { + number_of_wakeup: Mutex, + } - struct CounterWaker { - number_of_wakeup: Mutex, + impl Wake for CounterWaker { + fn wake(self: Arc) { + *self.number_of_wakeup.lock().unwrap() += 1; } + } - impl Wake for CounterWaker { - fn wake(self: Arc) { - *self.number_of_wakeup.lock().unwrap() += 1; - } - } + #[test] + fn test_updates_stream() { + use super::Update::*; let counter_waker = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) }); let waker = counter_waker.clone().into(); @@ -646,4 +662,166 @@ mod tests { // Wakers calls have not changed. assert_eq!(*counter_waker.number_of_wakeup.lock().unwrap(), 2); } + + #[test] + fn test_updates_multiple_streams() { + use super::Update::*; + + let counter_waker1 = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) }); + let counter_waker2 = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) }); + + let waker1 = counter_waker1.clone().into(); + let waker2 = counter_waker2.clone().into(); + + let mut context1 = Context::from_waker(&waker1); + let mut context2 = Context::from_waker(&waker2); + + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + let updates_subscriber1 = linked_chunk.updates().unwrap().subscribe(); + pin_mut!(updates_subscriber1); + + // Scope for `updates_subscriber2`. + let updates_subscriber2_token = { + let updates_subscriber2 = linked_chunk.updates().unwrap().subscribe(); + pin_mut!(updates_subscriber2); + + // No update, streams are pending. + assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending); + assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 0); + assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending); + assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 0); + + // Let's generate an update. + linked_chunk.push_items_back(['a']); + + // The wakers must have been called. + assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 1); + assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 1); + + // There is an update! Right after that, the streams are pending again. + assert_matches!( + updates_subscriber1.as_mut().poll_next(&mut context1), + Poll::Ready(Some(items)) => { + assert_eq!( + items, + &[PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] }] + ); + } + ); + assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending); + assert_matches!( + updates_subscriber2.as_mut().poll_next(&mut context2), + Poll::Ready(Some(items)) => { + assert_eq!( + items, + &[PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] }] + ); + } + ); + assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending); + + // Let's generate two other updates. + linked_chunk.push_items_back(['b']); + linked_chunk.push_items_back(['c']); + + // A waker is consumed when called. The first call to `push_items_back` will + // call and consume the wakers. The second call to `push_items_back` will do + // nothing as the wakers have been consumed. New wakers will be registered on + // polling. + // + // So, the waker must have been called only once for the two updates. + assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 2); + assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 2); + + // Let's poll `updates_subscriber1` only. + assert_matches!( + updates_subscriber1.as_mut().poll_next(&mut context1), + Poll::Ready(Some(items)) => { + assert_eq!( + items, + &[ + PushItems { at: Position(ChunkIdentifier(0), 1), items: vec!['b'] }, + PushItems { at: Position(ChunkIdentifier(0), 2), items: vec!['c'] }, + ] + ); + } + ); + assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending); + + // For the sake of this test, we also need to advance the main reader token. + let _ = linked_chunk.updates().unwrap().take(); + let _ = linked_chunk.updates().unwrap().take(); + + // If we inspect the garbage collector state, `a`, `b` and `c` should still be + // present because not all of them have been consumed by `updates_subscriber2` + // yet. + { + let updates = linked_chunk.updates().unwrap(); + + let inner = updates.inner.read().unwrap(); + + // Inspect number of updates in memory. + // We get 2 because the garbage collector runs before data are taken, not after: + // `updates_subscriber2` has read `a` only, so `b` and `c` remain. + assert_eq!(inner.len(), 2); + + // Inspect the indices. + let indices = &inner.last_index_per_reader; + + assert_eq!(indices.get(&updates_subscriber1.token), Some(&2)); + assert_eq!(indices.get(&updates_subscriber2.token), Some(&0)); + } + + // Poll `updates_subscriber1` again: there is no new update so it must be + // pending. + assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending); + + // The state of the garbage collector is unchanged: `a`, `b` and `c` are still + // in memory. + { + let updates = linked_chunk.updates().unwrap(); + + let inner = updates.inner.read().unwrap(); + + // Inspect number of updates in memory. Value is unchanged. + assert_eq!(inner.len(), 2); + + // Inspect the indices. They are unchanged. + let indices = &inner.last_index_per_reader; + + assert_eq!(indices.get(&updates_subscriber1.token), Some(&2)); + assert_eq!(indices.get(&updates_subscriber2.token), Some(&0)); + } + + updates_subscriber2.token + // Drop `updates_subscriber2`! + }; + + // `updates_subscriber2` has been dropped. Poll `updates_subscriber1` again: + // still no new update, but it will run the garbage collector again, and this + // time `updates_subscriber2` is not “retaining” `b` and `c`. The garbage + // collector must be empty. + assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending); + + // Inspect the garbage collector. + { + let updates = linked_chunk.updates().unwrap(); + + let inner = updates.inner.read().unwrap(); + + // Inspect number of updates in memory. + assert_eq!(inner.len(), 0); + + // Inspect the indices. + let indices = &inner.last_index_per_reader; + + assert_eq!(indices.get(&updates_subscriber1.token), Some(&0)); + assert_eq!(indices.get(&updates_subscriber2_token), None); // token is unknown! + } + + // When dropping the `LinkedChunk`, it closes the stream. + drop(linked_chunk); + assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Ready(None)); + } } From 0c26988cf579daabdb51e1bb1cc52a5b952e0b4e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 16 Oct 2024 17:27:42 +0100 Subject: [PATCH 327/979] refactor(base): Remove `impl From` for `SyncTimelineEvent` I feel like the ability to convert straight from a `Raw>` into a `SyncTimelineEvent` is somewhat over-simplified: the two are only occasionally equivalent, and it's better to be explicit. Changelog: `SyncTimelineEvent` no longer implements `From>`. --- crates/matrix-sdk-base/src/client.rs | 6 +++-- crates/matrix-sdk-base/src/rooms/normal.rs | 6 ++--- .../src/deserialized_responses.rs | 6 ----- .../src/timeline/event_item/mod.rs | 5 ++-- .../matrix-sdk-ui/src/timeline/tests/basic.rs | 7 ++--- .../src/timeline/tests/encryption.rs | 24 ++++++++--------- .../src/timeline/tests/event_filter.rs | 9 ++++--- .../src/timeline/tests/invalid.rs | 13 ++++----- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 10 +++---- .../matrix-sdk-ui/src/timeline/tests/polls.rs | 3 ++- .../src/timeline/tests/shields.rs | 6 ++--- .../matrix-sdk/src/event_cache/pagination.rs | 27 ++++++++++--------- crates/matrix-sdk/src/sliding_sync/mod.rs | 4 +-- 13 files changed, 63 insertions(+), 63 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 2e3835a8cee..031beab66dc 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -396,8 +396,10 @@ impl BaseClient { let mut timeline = Timeline::new(limited, prev_batch); let mut push_context = self.get_push_room_context(room, room_info, changes).await?; - for event in events { - let mut event: SyncTimelineEvent = event.into(); + for raw_event in events { + // Start by assuming we have a plaintext event. We'll replace it with a + // decrypted or UTD event below if necessary. + let mut event = SyncTimelineEvent::new(raw_event); match event.raw().deserialize() { Ok(e) => { diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 9f790ba33f4..3d426261f2b 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1876,9 +1876,9 @@ mod tests { last_prev_batch: Some("pb".to_owned()), sync_info: SyncInfo::FullySynced, encryption_state_synced: true, - latest_event: Some(Box::new(LatestEvent::new( - Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap().into(), - ))), + latest_event: Some(Box::new(LatestEvent::new(SyncTimelineEvent::new( + Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(), + )))), base_info: Box::new( assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }), ), diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index f19d523eeae..672e057b6a1 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -359,12 +359,6 @@ impl SyncTimelineEvent { } } -impl From> for SyncTimelineEvent { - fn from(inner: Raw) -> Self { - Self::new(inner) - } -} - impl From for SyncTimelineEvent { fn from(o: TimelineEvent) -> Self { Self { kind: o.kind, push_actions: o.push_actions.unwrap_or_default() } 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 67ee1299029..9c28bad5b59 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -1023,7 +1023,7 @@ mod tests { formatted_body: &str, ts: u64, ) -> SyncTimelineEvent { - sync_timeline_event!({ + SyncTimelineEvent::new(sync_timeline_event!({ "event_id": "$eventid6", "sender": user_id, "origin_server_ts": ts, @@ -1035,7 +1035,6 @@ mod tests { "formatted_body": formatted_body, "msgtype": "m.text" }, - }) - .into() + })) } } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index bf613932fdd..3d86d2c4383 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -16,6 +16,7 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; +use matrix_sdk::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB, CAROL}; use ruma::{ events::{ @@ -112,7 +113,7 @@ async fn test_sticker() { let mut stream = timeline.subscribe_events().await; timeline - .handle_live_event(sync_timeline_event!({ + .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ "content": { "body": "Happy sticker", "info": { @@ -127,7 +128,7 @@ async fn test_sticker() { "origin_server_ts": 143273582, "sender": "@alice:server.name", "type": "m.sticker", - })) + }))) .await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -278,7 +279,7 @@ async fn test_dedup_pagination() { let event = timeline .event_builder .make_sync_message_event(*ALICE, RoomMessageEventContent::text_plain("o/")); - timeline.handle_live_event(event.clone()).await; + timeline.handle_live_event(SyncTimelineEvent::new(event.clone())).await; // This cast is not actually correct, sync events aren't valid // back-paginated events, as they are missing `room_id`. However, the // timeline doesn't care about that `room_id` and casts back to diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index 8e0952d2e13..ce038e61574 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -27,15 +27,13 @@ use matrix_sdk::{ crypto::{decrypt_room_key_export, types::events::UtdCause, OlmMachine}, test_utils::test_client_builder, }; +use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::{async_test, BOB}; use ruma::{ assign, - events::{ - room::encrypted::{ - EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, - RoomEncryptedEventContent, - }, - AnySyncTimelineEvent, + events::room::encrypted::{ + EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, + RoomEncryptedEventContent, }, room_id, serde::Raw, @@ -475,7 +473,7 @@ async fn test_utd_cause_for_nonmember_event_is_found() { let mut stream = timeline.subscribe().await; // When we add an event with "membership: leave" - timeline.handle_live_event(raw_event_with_unsigned(json!({ "membership": "leave" }))).await; + timeline.handle_live_event(utd_event_with_unsigned(json!({ "membership": "leave" }))).await; // Then its UTD cause is membership let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -495,7 +493,7 @@ async fn test_utd_cause_for_nonmember_event_is_found_unstable_prefix() { // When we add an event with "io.element.msc4115.membership: leave" timeline - .handle_live_event(raw_event_with_unsigned( + .handle_live_event(utd_event_with_unsigned( json!({ "io.element.msc4115.membership": "leave" }), )) .await; @@ -517,7 +515,7 @@ async fn test_utd_cause_for_member_event_is_unknown() { let mut stream = timeline.subscribe().await; // When we add an event with "membership: join" - timeline.handle_live_event(raw_event_with_unsigned(json!({ "membership": "join" }))).await; + timeline.handle_live_event(utd_event_with_unsigned(json!({ "membership": "join" }))).await; // Then its UTD cause is membership let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -536,7 +534,7 @@ async fn test_utd_cause_for_missing_membership_is_unknown() { let mut stream = timeline.subscribe().await; // When we add an event with no membership in unsigned - timeline.handle_live_event(raw_event_with_unsigned(json!({}))).await; + timeline.handle_live_event(utd_event_with_unsigned(json!({}))).await; // Then its UTD cause is membership let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -548,8 +546,8 @@ async fn test_utd_cause_for_missing_membership_is_unknown() { assert_eq!(*cause, UtdCause::Unknown); } -fn raw_event_with_unsigned(unsigned: serde_json::Value) -> Raw { - Raw::from_json( +fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { + SyncTimelineEvent::new(Raw::from_json( to_raw_value(&json!({ "event_id": "$myevent", "sender": "@u:s", @@ -566,5 +564,5 @@ fn raw_event_with_unsigned(unsigned: serde_json::Value) -> Raw value); @@ -78,14 +79,14 @@ async fn test_invalid_event_content() { // Similar to above, the m.room.member state event must also not have an // empty content object. timeline - .handle_live_event(sync_timeline_event!({ + .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ "content": {}, "event_id": "$d5G0HA0FAZ37wP8kXlNkxx3I", "origin_server_ts": 2179, "sender": "@alice:example.org", "type": "m.room.member", "state_key": "@alice:example.org", - })) + }))) .await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -106,7 +107,7 @@ async fn test_invalid_event() { // This event is missing the sender field which the homeserver must add to // all timeline events. Because the event is malformed, it will be ignored. timeline - .handle_live_event(sync_timeline_event!({ + .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ "content": { "body": "hello world", "msgtype": "m.text" @@ -114,7 +115,7 @@ async fn test_invalid_event() { "event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I", "origin_server_ts": 10, "type": "m.room.message", - })) + }))) .await; assert_eq!(timeline.controller.items().await.len(), 0); } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 46ae26f589b..26cadd3b6dc 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -183,7 +183,7 @@ impl TestTimeline { C: RedactedMessageLikeEventContent, { let ev = self.event_builder.make_sync_redacted_message_event(sender, content); - self.handle_live_event(Raw::new(&ev).unwrap().cast()).await; + self.handle_live_event(SyncTimelineEvent::new(ev)).await; } async fn handle_live_state_event(&self, sender: &UserId, content: C, prev_content: Option) @@ -191,7 +191,7 @@ impl TestTimeline { C: StaticStateEventContent, { let ev = self.event_builder.make_sync_state_event(sender, "", content, prev_content); - self.handle_live_event(ev).await; + self.handle_live_event(SyncTimelineEvent::new(ev)).await; } async fn handle_live_state_event_with_state_key( @@ -209,7 +209,7 @@ impl TestTimeline { content, prev_content, ); - self.handle_live_event(Raw::new(&ev).unwrap().cast()).await; + self.handle_live_event(SyncTimelineEvent::new(ev)).await; } async fn handle_live_redacted_state_event(&self, sender: &UserId, content: C) @@ -217,7 +217,7 @@ impl TestTimeline { C: RedactedStateEventContent, { let ev = self.event_builder.make_sync_redacted_state_event(sender, "", content); - self.handle_live_event(Raw::new(&ev).unwrap().cast()).await; + self.handle_live_event(SyncTimelineEvent::new(ev)).await; } async fn handle_live_redacted_state_event_with_state_key( @@ -230,7 +230,7 @@ impl TestTimeline { { let ev = self.event_builder.make_sync_redacted_state_event(sender, state_key.as_ref(), content); - self.handle_live_event(Raw::new(&ev).unwrap().cast()).await; + self.handle_live_event(SyncTimelineEvent::new(ev)).await; } async fn handle_live_event(&self, event: impl Into) { diff --git a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs index b26f2a3851e..74d678a9955 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/polls.rs @@ -1,3 +1,4 @@ +use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::{ events::{ @@ -216,7 +217,7 @@ impl TestTimeline { ); let event = self.event_builder.make_sync_message_event_with_id(sender, event_id, event_content); - self.handle_live_event(event).await; + self.handle_live_event(SyncTimelineEvent::new(event)).await; } async fn send_poll_response(&self, sender: &UserId, answers: Vec<&str>, poll_id: &EventId) { diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index ad86b21c913..5cc973c4016 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -1,6 +1,6 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; -use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode}; +use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode, SyncTimelineEvent}; use matrix_sdk_test::{async_test, sync_timeline_event, ALICE}; use ruma::{ event_id, @@ -97,7 +97,7 @@ async fn test_local_sent_in_clear_shield() { // When the remote echo comes in. timeline - .handle_live_event(sync_timeline_event!({ + .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ "content": { "body": "Local message", "msgtype": "m.text", @@ -106,7 +106,7 @@ async fn test_local_sent_in_clear_shield() { "event_id": event_id, "origin_server_ts": timestamp, "type": "m.room.message", - })) + }))) .await; let item = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value); let event_item = item.as_event().unwrap(); diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 03509371102..74e943aaebd 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -320,7 +320,10 @@ mod tests { use ruma::room_id; use tokio::{spawn, time::sleep}; - use crate::{event_cache::store::Gap, test_utils::logged_in_client}; + use crate::{ + deserialized_responses::SyncTimelineEvent, event_cache::store::Gap, + test_utils::logged_in_client, + }; #[async_test] async fn test_wait_no_pagination_token() { @@ -335,14 +338,15 @@ mod tests { let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap(); // When I only have events in a room, - room_event_cache.inner.state.write().await.events.push_events([sync_timeline_event!({ - "sender": "b@z.h", - "type": "m.room.message", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content": { "body":"yolo", "msgtype": "m.text" }, - }) - .into()]); + room_event_cache.inner.state.write().await.events.push_events([ + SyncTimelineEvent::new(sync_timeline_event!({ + "sender": "b@z.h", + "type": "m.room.message", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content": { "body":"yolo", "msgtype": "m.text" }, + })), + ]); let pagination = room_event_cache.pagination(); @@ -395,14 +399,13 @@ mod tests { { let room_events = &mut room_event_cache.inner.state.write().await.events; room_events.push_gap(Gap { prev_token: expected_token.clone() }); - room_events.push_events([sync_timeline_event!({ + room_events.push_events([SyncTimelineEvent::new(sync_timeline_event!({ "sender": "b@z.h", "type": "m.room.message", "event_id": "$ida", "origin_server_ts": 12344446, "content": { "body":"yolo", "msgtype": "m.text" }, - }) - .into()]); + }))]); } let pagination = room_event_cache.pagination(); diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 9de9a21afb7..43258f4f634 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -36,7 +36,7 @@ use async_stream::stream; pub use client::{Version, VersionBuilder}; use futures_core::stream::Stream; pub use matrix_sdk_base::sliding_sync::http; -use matrix_sdk_common::timer; +use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timer}; use ruma::{ api::{client::error::ErrorKind, OutgoingRequest}, assign, OwnedEventId, OwnedRoomId, RoomId, @@ -345,7 +345,7 @@ impl SlidingSync { if let Some(joined_room) = sync_response.rooms.join.remove(&room_id) { joined_room.timeline.events } else { - room_data.timeline.drain(..).map(Into::into).collect() + room_data.timeline.drain(..).map(SyncTimelineEvent::new).collect() }; match rooms_map.get_mut(&room_id) { From befcd069c3922c4a8b583e8807278134c62a8340 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:36:34 +0100 Subject: [PATCH 328/979] FFI: Expose `UserIdentity::is_verified` and add a new `Encryption::user_identity` method. (#4142) --- bindings/matrix-sdk-ffi/src/encryption.rs | 45 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 25849e60e42..e6ab1253b41 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -6,6 +6,7 @@ use matrix_sdk::{ encryption::{backups, recovery}, }; use thiserror::Error; +use tracing::{error, info}; use zeroize::Zeroize; use super::RUNTIME; @@ -413,16 +414,40 @@ impl Encryption { /// Get the E2EE identity of a user. /// - /// Returns Ok(None) if this user does not exist. + /// This method always tries to fetch the identity from the store, which we + /// only have if the user is tracked, meaning that we are both members + /// of the same encrypted room. If no user is found locally, a request will + /// be made to the homeserver. /// - /// Returns an error if there was a problem contacting the crypto store, or - /// if our client is not logged in. - pub async fn get_user_identity( + /// # Arguments + /// + /// * `user_id` - The ID of the user that the identity belongs to. + /// + /// Returns a `UserIdentity` if one is found. Returns an error if there + /// was an issue with the crypto store or with the request to the + /// homeserver. + /// + /// This will always return `None` if the client hasn't been logged in. + pub async fn user_identity( &self, user_id: String, ) -> Result>, ClientError> { - let identity = self.inner.get_user_identity(user_id.as_str().try_into()?).await?; - Ok(identity.map(|i| Arc::new(UserIdentity { inner: i }))) + match self.inner.get_user_identity(user_id.as_str().try_into()?).await { + Ok(Some(identity)) => { + return Ok(Some(Arc::new(UserIdentity { inner: identity }))); + } + Ok(None) => { + info!("No identity found in the store."); + } + Err(error) => { + error!("Failed fetching identity from the store: {}", error); + } + }; + + info!("Requesting identity from the server."); + + let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?; + Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity }))) } } @@ -461,6 +486,14 @@ impl UserIdentity { pub(crate) fn master_key(&self) -> Option { self.inner.master_key().get_first_key().map(|k| k.to_base64()) } + + /// Is the user identity considered to be verified. + /// + /// If the identity belongs to another user, our own user identity needs to + /// be verified as well for the identity to be considered to be verified. + pub fn is_verified(&self) -> bool { + self.inner.is_verified() + } } #[derive(uniffi::Object)] From 951a4354c6db2cc8ac470788001e0baa47c7aa7d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 15 Oct 2024 18:08:14 +0200 Subject: [PATCH 329/979] refactor(timeline): get rid of `local_item_by_transaction_id` There's no need for this API anymore. Changelog: `Timeline::get_event_timeline_item_by_transaction_id` has been removed. There's no API that makes use of an `EventTimelineItem` now, those APIs are using a `TimelineEventItemId` instead. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 22 +--------------- crates/matrix-sdk-ui/src/timeline/mod.rs | 28 ++------------------- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index e7ce10c17c0..85e4479a444 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -53,7 +53,7 @@ use ruma::{ }, AnyMessageLikeEventContent, }, - EventId, OwnedTransactionId, + EventId, }; use tokio::{ sync::Mutex, @@ -576,26 +576,6 @@ impl Timeline { Ok(item.into()) } - /// Get the current timeline item for the given transaction ID, if any. - /// - /// This will always return a local echo, if found. - /// - /// It's preferable to store the timeline items in the model for your UI, if - /// possible, instead of just storing IDs and coming back to the timeline - /// object to look up items. - pub async fn get_event_timeline_item_by_transaction_id( - &self, - transaction_id: String, - ) -> Result { - let transaction_id: OwnedTransactionId = transaction_id.into(); - let item = self - .inner - .local_item_by_transaction_id(&transaction_id) - .await - .context("Item with given transaction ID not found")?; - Ok(item.into()) - } - /// Redacts an event from the timeline. /// /// Only works for events that exist as timeline items. diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 4e79dd12054..93168e1732a 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -49,8 +49,7 @@ use ruma::{ SyncMessageLikeEvent, }, serde::Raw, - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomVersionId, TransactionId, - UserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomVersionId, UserId, }; use thiserror::Error; use tracing::{error, instrument, trace, warn}; @@ -95,11 +94,7 @@ pub use self::{ traits::RoomExt, virtual_item::VirtualTimelineItem, }; -use self::{ - controller::TimelineController, - futures::SendAttachment, - util::{rfind_event_by_id, rfind_event_item}, -}; +use self::{controller::TimelineController, futures::SendAttachment, util::rfind_event_by_id}; /// Information needed to reply to an event. #[derive(Debug, Clone)] @@ -253,25 +248,6 @@ impl Timeline { Some(item.to_owned()) } - /// Get the current local echo timeline item for the given transaction ID, - /// if any. - /// - /// This will always return a local echo, if found. - /// - /// It's preferable to store the timeline items in the model for your UI, if - /// possible, instead of just storing IDs and coming back to the timeline - /// object to look up items. - pub async fn local_item_by_transaction_id( - &self, - target: &TransactionId, - ) -> Option { - let items = self.controller.items().await; - let (_, item) = rfind_event_item(&items, |item| { - item.as_local().map_or(false, |local| local.transaction_id == target) - })?; - Some(item.to_owned()) - } - /// Get the latest of the timeline's event items. pub async fn latest_event(&self) -> Option { if self.controller.is_live().await { From 2df359d316a35ca908bc4a264b2f97b062a3f7eb Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 21 Oct 2024 16:40:54 +0100 Subject: [PATCH 330/979] fix(experimental-algorithms) Add missing argument to handle_supported_key_request --- crates/matrix-sdk-crypto/src/gossiping/machine.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index b5d2b1dbc4f..22a4e4e9cdd 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -510,7 +510,8 @@ impl GossipMachine { } #[cfg(feature = "experimental-algorithms")] RequestedKeyInfo::MegolmV2AesSha2(i) => { - self.handle_supported_key_request(event, &i.room_id, &i.session_id).await + self.handle_supported_key_request(cache, event, &i.room_id, &i.session_id) + .await } RequestedKeyInfo::Unknown(i) => { debug!( From c8b38257f1cd5e62a18a9c505b29770bdb88b938 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 9 Oct 2024 16:15:25 +0100 Subject: [PATCH 331/979] refactor(common): add `TimelineEventKind::UnableToDecrypt` --- .../src/deserialized_responses.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 672e057b6a1..4c59eb0c686 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -334,6 +334,12 @@ impl SyncTimelineEvent { Self { kind: TimelineEventKind::PlainText { event }, push_actions } } + /// Create a new `SyncTimelineEvent` to represent the given decryption + /// failure. + pub fn new_utd_event(event: Raw, utd_info: UnableToDecryptInfo) -> Self { + Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: vec![] } + } + /// Get the event id of this `SyncTimelineEvent` if the event has any valid /// id. pub fn event_id(&self) -> Option { @@ -450,6 +456,11 @@ impl TimelineEvent { } } + /// Create a new `TimelineEvent` to represent the given decryption failure. + pub fn new_utd_event(event: Raw, utd_info: UnableToDecryptInfo) -> Self { + Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: None } + } + /// Returns a reference to the (potentially decrypted) Matrix event inside /// this `TimelineEvent`. pub fn raw(&self) -> &Raw { @@ -482,6 +493,17 @@ pub enum TimelineEventKind { /// A successfully-decrypted encrypted event. Decrypted(DecryptedRoomEvent), + /// An encrypted event which could not be decrypted. + UnableToDecrypt { + /// The `m.room.encrypted` event. Depending on the source of the event, + /// it could actually be an [`AnyTimelineEvent`] (i.e., it may + /// have a `room_id` property). + event: Raw, + + /// Information on the reason we failed to decrypt + utd_info: UnableToDecryptInfo, + }, + /// An unencrypted event. PlainText { /// The actual event. Depending on the source of the event, it could @@ -502,6 +524,7 @@ impl TimelineEventKind { // expected to contain a `room_id`). It just means that the `room_id` will be ignored // in a future deserialization. TimelineEventKind::Decrypted(d) => d.event.cast_ref(), + TimelineEventKind::UnableToDecrypt { event, .. } => event.cast_ref(), TimelineEventKind::PlainText { event } => event, } } @@ -511,6 +534,7 @@ impl TimelineEventKind { pub fn encryption_info(&self) -> Option<&EncryptionInfo> { match self { TimelineEventKind::Decrypted(d) => Some(&d.encryption_info), + TimelineEventKind::UnableToDecrypt { .. } => None, TimelineEventKind::PlainText { .. } => None, } } @@ -525,6 +549,7 @@ impl TimelineEventKind { // expected to contain a `room_id`). It just means that the `room_id` will be ignored // in a future deserialization. TimelineEventKind::Decrypted(d) => d.event.cast(), + TimelineEventKind::UnableToDecrypt { event, .. } => event.cast(), TimelineEventKind::PlainText { event } => event, } } @@ -539,6 +564,12 @@ impl fmt::Debug for TimelineEventKind { .field("event", &DebugRawEvent(event)) .finish(), + Self::UnableToDecrypt { event, utd_info } => f + .debug_struct("TimelineEventDecryptionResult::UnableToDecrypt") + .field("event", &DebugRawEvent(event)) + .field("utd_info", &utd_info) + .finish(), + Self::Decrypted(decrypted) => { f.debug_tuple("TimelineEventDecryptionResult::Decrypted").field(decrypted).finish() } From 543152d914ca5ad687c3a9e1b6398ada8b2a146b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 10 Oct 2024 15:38:38 +0100 Subject: [PATCH 332/979] refactor(timeline): store UTDs in `decrypt_sync_room_event` When `decrypt_sync_room_event` fails to decrypt an event, return the UTD as a `SyncTimelineEvent` instead of an Error. --- crates/matrix-sdk-base/src/client.rs | 57 +++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 031beab66dc..fdb798fdc09 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -29,7 +29,8 @@ use futures_util::Stream; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::{ store::DynCryptoStore, CollectStrategy, DecryptionSettings, EncryptionSettings, - EncryptionSyncChanges, OlmError, OlmMachine, ToDeviceRequest, TrustRequirement, + EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult, ToDeviceRequest, + TrustRequirement, }; #[cfg(feature = "e2e-encryption")] use ruma::events::{ @@ -343,6 +344,13 @@ impl BaseClient { Ok(()) } + /// Attempt to decrypt the given raw event into a `SyncTimelineEvent`. + /// + /// In the case of a decryption error, returns a `SyncTimelineEvent` + /// representing the decryption error; in the case of problems with our + /// application, returns `Err`. + /// + /// Returns `Ok(None)` if encryption is not configured. #[cfg(feature = "e2e-encryption")] async fn decrypt_sync_room_event( &self, @@ -355,24 +363,37 @@ impl BaseClient { let decryption_settings = DecryptionSettings { sender_device_trust_requirement: self.decryption_trust_requirement, }; - let event: SyncTimelineEvent = - olm.decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await?.into(); - - if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.raw().deserialize() { - match &e { - AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original( - original_event, - )) => { - if let MessageType::VerificationRequest(_) = &original_event.content.msgtype { - self.handle_verification_event(&e, room_id).await?; + + let event = match olm + .try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings) + .await? + { + RoomEventDecryptionResult::Decrypted(decrypted) => { + let event: SyncTimelineEvent = decrypted.into(); + + if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.raw().deserialize() { + match &e { + AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original( + original_event, + )) => { + if let MessageType::VerificationRequest(_) = + &original_event.content.msgtype + { + self.handle_verification_event(&e, room_id).await?; + } + } + _ if e.event_type().to_string().starts_with("m.key.verification") => { + self.handle_verification_event(&e, room_id).await?; + } + _ => (), } } - _ if e.event_type().to_string().starts_with("m.key.verification") => { - self.handle_verification_event(&e, room_id).await?; - } - _ => (), + event } - } + RoomEventDecryptionResult::UnableToDecrypt(utd_info) => { + SyncTimelineEvent::new_utd_event(event.clone(), utd_info) + } + }; Ok(Some(event)) } @@ -460,10 +481,10 @@ impl BaseClient { AnySyncMessageLikeEvent::RoomEncrypted( SyncMessageLikeEvent::Original(_), ) => { - if let Ok(Some(e)) = Box::pin( + if let Some(e) = Box::pin( self.decrypt_sync_room_event(event.raw(), room.room_id()), ) - .await + .await? { event = e; } From b69575d5ffb8a224fe9f13708ce63307adfdff28 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 10 Oct 2024 17:32:00 +0100 Subject: [PATCH 333/979] refactor(timeline): store UTDs in `decrypt_room_event` When `decrypt_room_event` fails to decrypt an event, return the UTD as a `TimelineEvent` instead of an Error. --- .../src/deserialized_responses.rs | 8 +++ .../matrix-sdk-ui/src/notification_client.rs | 54 +++++++++++-------- crates/matrix-sdk/src/room/mod.rs | 20 ++++--- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 4c59eb0c686..c63a0e42968 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -708,6 +708,14 @@ pub enum UnableToDecryptReason { SenderIdentityNotTrusted(VerificationLevel), } +impl UnableToDecryptReason { + /// Returns true if this UTD is due to a missing room key (and hence might + /// resolve itself if we wait a bit.) + pub fn is_missing_room_key(&self) -> bool { + matches!(self, Self::MissingMegolmSession | Self::UnknownMegolmMessageIndex) + } +} + /// Deserialization helper for [`SyncTimelineEvent`], for the modern format. /// /// This has the exact same fields as [`SyncTimelineEvent`] itself, but has a diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index cc306440f2d..0c80298f0c4 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -20,10 +20,7 @@ use std::{ use futures_util::{pin_mut, StreamExt as _}; use matrix_sdk::{room::Room, Client, ClientBuildError, SlidingSyncList, SlidingSyncMode}; use matrix_sdk_base::{ - crypto::{vodozemac, MegolmError}, - deserialized_responses::TimelineEvent, - sliding_sync::http, - RoomState, StoreError, + deserialized_responses::TimelineEvent, sliding_sync::http, RoomState, StoreError, }; use ruma::{ assign, @@ -216,24 +213,24 @@ impl NotificationClient { tokio::time::sleep(Duration::from_millis(wait)).await; - match room.decrypt_event(raw_event.cast_ref()).await { - Ok(new_event) => { + let new_event = room.decrypt_event(raw_event.cast_ref()).await?; + + match new_event.kind { + matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt { + utd_info, ..} => { + if utd_info.reason.is_missing_room_key() { + // Decryption error that could be caused by a missing room + // key; retry in a few. + wait *= 2; + } else { + debug!("Event could not be decrypted, but waiting longer is unlikely to help: {:?}", utd_info.reason); + return Ok(None); + } + } + _ => { trace!("Waiting succeeded and event could be decrypted!"); return Ok(Some(new_event)); } - Err(matrix_sdk::Error::MegolmError( - MegolmError::MissingRoomKey(_) - | MegolmError::Decryption( - vodozemac::megolm::DecryptionError::UnknownMessageIndex(_, _), - ), - )) => { - // Decryption error that could be caused by a missing room key; - // retry in a few. - wait *= 2; - } - Err(err) => { - return Err(err.into()); - } } } @@ -259,10 +256,21 @@ impl NotificationClient { match encryption_sync { Ok(sync) => match sync.run_fixed_iterations(2, sync_permit_guard).await { Ok(()) => match room.decrypt_event(raw_event.cast_ref()).await { - Ok(new_event) => { - trace!("Encryption sync managed to decrypt the event."); - Ok(Some(new_event)) - } + Ok(new_event) => match new_event.kind { + matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt { + utd_info, .. + } => { + trace!( + "Encryption sync failed to decrypt the event: {:?}", + utd_info.reason + ); + Ok(None) + } + _ => { + trace!("Encryption sync managed to decrypt the event."); + Ok(Some(new_event)) + } + }, Err(err) => { trace!("Encryption sync failed to decrypt the event: {err}"); Ok(None) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index a0bf0ced59b..35d889d24f3 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -19,7 +19,7 @@ use futures_util::{ #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] pub use identity_status_changes::IdentityStatusChanges; #[cfg(feature = "e2e-encryption")] -use matrix_sdk_base::crypto::DecryptionSettings; +use matrix_sdk_base::crypto::{DecryptionSettings, RoomEventDecryptionResult}; #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] use matrix_sdk_base::crypto::{IdentityStatusChange, RoomIdentityProvider, UserIdentity}; use matrix_sdk_base::{ @@ -1215,7 +1215,8 @@ impl Room { /// # Arguments /// * `event` - The room event to be decrypted. /// - /// Returns the decrypted event. + /// Returns the decrypted event. In the case of a decryption error, returns + /// a `TimelineEvent` representing the decryption error. #[cfg(feature = "e2e-encryption")] pub async fn decrypt_event( &self, @@ -1227,24 +1228,21 @@ impl Room { let decryption_settings = DecryptionSettings { sender_device_trust_requirement: self.client.base_client().decryption_trust_requirement, }; - let decrypted = match machine - .decrypt_room_event(event.cast_ref(), self.inner.room_id(), &decryption_settings) - .await + let mut event: TimelineEvent = match machine + .try_decrypt_room_event(event.cast_ref(), self.inner.room_id(), &decryption_settings) + .await? { - Ok(event) => event, - Err(e) => { + RoomEventDecryptionResult::Decrypted(decrypted) => decrypted.into(), + RoomEventDecryptionResult::UnableToDecrypt(utd_info) => { self.client .encryption() .backups() .maybe_download_room_key(self.room_id().to_owned(), event.clone()); - - return Err(e.into()); + TimelineEvent::new_utd_event(event.clone().cast(), utd_info) } }; - let mut event: TimelineEvent = decrypted.into(); event.push_actions = self.event_push_actions(event.raw()).await?; - Ok(event) } From 3887c10444ec41eb42957bbdb45625aadb2f1e64 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 11 Oct 2024 17:18:01 +0100 Subject: [PATCH 334/979] test: Update tests to use new UTD TimelineEventKind variant Make the tests behave the same way as the network code, by returning UTDs as `TimelineEventKind::UnableToDecrypt` instead of `TimelineEventKind::PlainText`. --- .../matrix-sdk-base/src/sliding_sync/mod.rs | 11 +++++- .../src/timeline/tests/encryption.rs | 39 ++++++++++++++----- .../src/timeline/tests/read_receipts.rs | 3 +- .../src/timeline/tests/shields.rs | 3 +- crates/matrix-sdk/src/test_utils/events.rs | 20 ++++++++++ 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 819d1662eec..17efc70323e 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -876,7 +876,10 @@ mod tests { }; use assert_matches::assert_matches; - use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer}; + use matrix_sdk_common::{ + deserialized_responses::{SyncTimelineEvent, UnableToDecryptInfo, UnableToDecryptReason}, + ring_buffer::RingBuffer, + }; use matrix_sdk_test::async_test; use ruma::{ api::client::sync::sync_events::UnreadNotificationsCount, @@ -2494,7 +2497,7 @@ mod tests { } fn make_encrypted_event(id: &str) -> SyncTimelineEvent { - SyncTimelineEvent::new( + SyncTimelineEvent::new_utd_event( Raw::from_json_string( json!({ "type": "m.room.encrypted", @@ -2512,6 +2515,10 @@ mod tests { .to_string(), ) .unwrap(), + UnableToDecryptInfo { + session_id: Some("".to_owned()), + reason: UnableToDecryptReason::MissingMegolmSession, + }, ) } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index ce038e61574..4b0683529d8 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -27,7 +27,7 @@ use matrix_sdk::{ crypto::{decrypt_room_key_export, types::events::UtdCause, OlmMachine}, test_utils::test_client_builder, }; -use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_base::deserialized_responses::{SyncTimelineEvent, UnableToDecryptReason}; use matrix_sdk_test::{async_test, BOB}; use ruma::{ assign, @@ -105,7 +105,8 @@ async fn test_retry_message_decryption() { ), None, )) - .sender(&BOB), + .sender(&BOB) + .into_utd_sync_timeline_event(), ) .await; @@ -215,7 +216,11 @@ async fn test_retry_edit_decryption() { .into(), ); timeline - .handle_live_event(f.event(RoomEncryptedEventContent::new(encrypted, None)).sender(&BOB)) + .handle_live_event( + f.event(RoomEncryptedEventContent::new(encrypted, None)) + .sender(&BOB) + .into_utd_sync_timeline_event(), + ) .await; let event_id = @@ -242,7 +247,8 @@ async fn test_retry_edit_decryption() { f.event(assign!(RoomEncryptedEventContent::new(encrypted, None), { relates_to: Some(Relation::Replacement(Replacement::new(event_id))), })) - .sender(&BOB), + .sender(&BOB) + .into_utd_sync_timeline_event(), ) .await; @@ -321,7 +327,8 @@ async fn test_retry_edit_and_more() { mBZdKIaqDTUBFvcvbn2gQaWtUipQdJQRKyv2h0AWveVkv75lp5hRb7jolCi08oMX8cM+V3Zzyi7\ mlPAzZjDz0PaRbQwfbMTTHkcL7TZybBi4vLX4f5ZR2Iiysc7gw", )) - .sender(&BOB), + .sender(&BOB) + .into_utd_sync_timeline_event(), ) .await; @@ -337,7 +344,9 @@ async fn test_retry_edit_and_more() { ); timeline .handle_live_event( - f.event(assign!(msg2, { relates_to: Some(Relation::Replacement(Replacement::new(event_id))) })).sender(&BOB), + f.event(assign!(msg2, { relates_to: Some(Relation::Replacement(Replacement::new(event_id))) })) + .sender(&BOB) + .into_utd_sync_timeline_event(), ) .await; @@ -349,7 +358,8 @@ async fn test_retry_edit_and_more() { 2r/fEvAW/9QB+N6fX4g9729bt5ftXRqa5QI7NA351RNUveRHxVvx+2x0WJArQjYGRk7tMS2rUto\ IYt2ZY17nE1UJjN7M87STnCF9c9qy4aGNqIpeVIht6XbtgD7gQ", )) - .sender(&BOB), + .sender(&BOB) + .into_utd_sync_timeline_event(), ) .await; @@ -422,7 +432,8 @@ async fn test_retry_message_decryption_highlighted() { ), None, )) - .sender(&BOB), + .sender(&BOB) + .into_utd_sync_timeline_event(), ) .await; @@ -547,7 +558,7 @@ async fn test_utd_cause_for_missing_membership_is_unknown() { } fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { - SyncTimelineEvent::new(Raw::from_json( + let raw = Raw::from_json( to_raw_value(&json!({ "event_id": "$myevent", "sender": "@u:s", @@ -564,5 +575,13 @@ fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { })) .unwrap(), - )) + ); + + SyncTimelineEvent::new_utd_event( + raw, + matrix_sdk::deserialized_responses::UnableToDecryptInfo { + session_id: Some("SESSION_ID".into()), + reason: UnableToDecryptReason::MissingMegolmSession, + }, + ) } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs index 574a8bec6a4..6f0c2e02a17 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs @@ -400,7 +400,8 @@ async fn test_read_receipts_updates_on_message_decryption() { ), None, )) - .sender(&BOB), + .sender(&BOB) + .into_utd_sync_timeline_event(), ) .await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index 5cc973c4016..33c90d0e25c 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -151,7 +151,8 @@ async fn test_utd_shield() { ), None, )) - .sender(&ALICE), + .sender(&ALICE) + .into_utd_sync_timeline_event(), ) .await; diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index 9ace4ccd49f..3ecaebb5b47 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -16,7 +16,9 @@ use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; +use as_variant::as_variant; use matrix_sdk_base::deserialized_responses::{SyncTimelineEvent, TimelineEvent}; +use matrix_sdk_common::deserialized_responses::UnableToDecryptReason; use ruma::{ events::{ message::TextContentBlock, @@ -31,6 +33,7 @@ use ruma::{ reaction::ReactionEventContent, relation::{Annotation, InReplyTo, Replacement, Thread}, room::{ + encrypted::{EncryptedEventScheme, RoomEncryptedEventContent}, message::{Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, redaction::RoomRedactionEventContent, }, @@ -185,6 +188,23 @@ where } } +impl EventBuilder { + /// Turn this event into a SyncTimelineEvent representing a decryption + /// failure + pub fn into_utd_sync_timeline_event(self) -> SyncTimelineEvent { + let session_id = as_variant!(&self.content.scheme, EncryptedEventScheme::MegolmV1AesSha2) + .map(|content| content.session_id.clone()); + + SyncTimelineEvent::new_utd_event( + self.into(), + crate::deserialized_responses::UnableToDecryptInfo { + session_id, + reason: UnableToDecryptReason::MissingMegolmSession, + }, + ) + } +} + impl EventBuilder { /// Adds a reply relation to the current event. pub fn reply_to(mut self, event_id: &EventId) -> Self { From d3bfdb9563544db37e755b363fbec410038a6609 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 17 Oct 2024 16:08:52 +0200 Subject: [PATCH 335/979] feat(media)!: optionally cache a media after upload Changelog: Uploaded medias can now be cached in multiple attachment-related methods like `Room::send_attachment`. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 65 ++++++++++++++++--- crates/matrix-sdk-ui/src/timeline/futures.rs | 26 ++++++-- crates/matrix-sdk/src/room/futures.rs | 33 +++++++++- crates/matrix-sdk/src/room/mod.rs | 34 +++++++--- .../tests/integration/room/attachment/mod.rs | 28 +++++++- 5 files changed, 156 insertions(+), 30 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 85e4479a444..b27bc3d0c0c 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -104,12 +104,18 @@ impl Timeline { mime_type: Option, attachment_config: AttachmentConfig, progress_watcher: Option>, + store_in_cache: bool, ) -> Result<(), RoomError> { let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; let mime_type = mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let request = self.inner.send_attachment(filename, mime_type, attachment_config); + let mut request = self.inner.send_attachment(filename, mime_type, attachment_config); + + if store_in_cache { + request.store_in_cache(); + } + if let Some(progress_watcher) = progress_watcher { let mut subscriber = request.subscribe_to_send_progress(); RUNTIME.spawn(async move { @@ -270,6 +276,7 @@ impl Timeline { } } + #[allow(clippy::too_many_arguments)] pub fn send_image( self: Arc, url: String, @@ -278,6 +285,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + store_in_cache: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_image_info = BaseImageInfo::try_from(&image_info) @@ -289,11 +297,18 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, image_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + image_info.mimetype, + attachment_config, + progress_watcher, + store_in_cache, + ) + .await })) } + #[allow(clippy::too_many_arguments)] pub fn send_video( self: Arc, url: String, @@ -302,6 +317,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + store_in_cache: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) @@ -313,8 +329,14 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, video_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + video_info.mimetype, + attachment_config, + progress_watcher, + store_in_cache, + ) + .await })) } @@ -325,6 +347,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + store_in_cache: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) @@ -336,11 +359,18 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + audio_info.mimetype, + attachment_config, + progress_watcher, + store_in_cache, + ) + .await })) } + #[allow(clippy::too_many_arguments)] pub fn send_voice_message( self: Arc, url: String, @@ -349,6 +379,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + store_in_cache: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) @@ -361,8 +392,14 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + audio_info.mimetype, + attachment_config, + progress_watcher, + store_in_cache, + ) + .await })) } @@ -371,6 +408,7 @@ impl Timeline { url: String, file_info: FileInfo, progress_watcher: Option>, + store_in_cache: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_file_info: BaseFileInfo = @@ -379,7 +417,14 @@ impl Timeline { let attachment_config = AttachmentConfig::new().info(attachment_info); - self.send_attachment(url, file_info.mimetype, attachment_config, progress_watcher).await + self.send_attachment( + url, + file_info.mimetype, + attachment_config, + progress_watcher, + store_in_cache, + ) + .await })) } diff --git a/crates/matrix-sdk-ui/src/timeline/futures.rs b/crates/matrix-sdk-ui/src/timeline/futures.rs index 694a2203fb4..91d20e708d9 100644 --- a/crates/matrix-sdk-ui/src/timeline/futures.rs +++ b/crates/matrix-sdk-ui/src/timeline/futures.rs @@ -15,6 +15,7 @@ pub struct SendAttachment<'a> { config: AttachmentConfig, tracing_span: Span, pub(crate) send_progress: SharedObservable, + store_in_cache: bool, } impl<'a> SendAttachment<'a> { @@ -31,6 +32,7 @@ impl<'a> SendAttachment<'a> { config, tracing_span: Span::current(), send_progress: Default::default(), + store_in_cache: false, } } @@ -40,6 +42,14 @@ impl<'a> SendAttachment<'a> { pub fn subscribe_to_send_progress(&self) -> Subscriber { self.send_progress.subscribe() } + + /// Whether the sent attachment should be stored in the cache or not. + /// + /// If set to true, then retrieving the data for the attachment will result + /// in a cache hit immediately after upload. + pub fn store_in_cache(&mut self) { + self.store_in_cache = true; + } } impl<'a> IntoFuture for SendAttachment<'a> { @@ -47,7 +57,9 @@ impl<'a> IntoFuture for SendAttachment<'a> { boxed_into_future!(extra_bounds: 'a); fn into_future(self) -> Self::IntoFuture { - let Self { timeline, path, mime_type, config, tracing_span, send_progress } = self; + let Self { timeline, path, mime_type, config, tracing_span, send_progress, store_in_cache } = + self; + let fut = async move { let filename = path .file_name() @@ -56,12 +68,16 @@ impl<'a> IntoFuture for SendAttachment<'a> { .ok_or(Error::InvalidAttachmentFileName)?; let data = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?; - timeline + let mut fut = timeline .room() .send_attachment(filename, &mime_type, data, config) - .with_send_progress_observable(send_progress) - .await - .map_err(|_| Error::FailedSendingAttachment)?; + .with_send_progress_observable(send_progress); + + if store_in_cache { + fut = fut.store_in_cache(); + } + + fut.await.map_err(|_| Error::FailedSendingAttachment)?; Ok(()) }; diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index 396d15cd2e4..f1b91463fe7 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -246,6 +246,7 @@ pub struct SendAttachment<'a> { config: AttachmentConfig, tracing_span: Span, send_progress: SharedObservable, + store_in_cache: bool, } impl<'a> SendAttachment<'a> { @@ -264,6 +265,7 @@ impl<'a> SendAttachment<'a> { config, tracing_span: Span::current(), send_progress: Default::default(), + store_in_cache: false, } } @@ -277,6 +279,15 @@ impl<'a> SendAttachment<'a> { self.send_progress = send_progress; self } + + /// Whether the sent attachment should be stored in the cache or not. + /// + /// If set to true, then retrieving the data for the attachment will result + /// in a cache hit immediately after upload. + pub fn store_in_cache(mut self) -> Self { + self.store_in_cache = true; + self + } } impl<'a> IntoFuture for SendAttachment<'a> { @@ -284,10 +295,26 @@ impl<'a> IntoFuture for SendAttachment<'a> { boxed_into_future!(extra_bounds: 'a); fn into_future(self) -> Self::IntoFuture { - let Self { room, filename, content_type, data, config, tracing_span, send_progress } = self; + let Self { + room, + filename, + content_type, + data, + config, + tracing_span, + send_progress, + store_in_cache, + } = self; let fut = async move { - room.prepare_and_send_attachment(filename, content_type, data, config, send_progress) - .await + room.prepare_and_send_attachment( + filename, + content_type, + data, + config, + send_progress, + store_in_cache, + ) + .await }; Box::pin(fut.instrument(tracing_span)) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 35d889d24f3..09f1e8480fb 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1909,6 +1909,12 @@ impl Room { /// media. /// /// * `config` - Metadata and configuration for the attachment. + /// + /// * `send_progress` - An observable to transmit forward progress about the + /// upload. + /// + /// * `store_in_cache` - A boolean defining whether the uploaded media will + /// be stored in the cache immediately after a successful upload. pub(super) async fn prepare_and_send_attachment<'a>( &'a self, filename: &'a str, @@ -1916,19 +1922,22 @@ impl Room { data: Vec, mut config: AttachmentConfig, send_progress: SharedObservable, + store_in_cache: bool, ) -> Result { self.ensure_room_joined()?; let txn_id = config.txn_id.take(); let mentions = config.mentions.take(); + let thumbnail = config.thumbnail.take(); + #[cfg(feature = "e2e-encryption")] let (media_source, thumbnail_source, thumbnail_info) = if self.is_encrypted().await? { self.client .upload_encrypted_media_and_thumbnail( content_type, - data, - config.thumbnail.take(), + data.clone(), + thumbnail, send_progress, ) .await? @@ -1937,8 +1946,8 @@ impl Room { .media() .upload_plain_media_and_thumbnail( content_type, - data, - config.thumbnail.take(), + data.clone(), + thumbnail, send_progress, ) .await? @@ -1948,14 +1957,19 @@ impl Room { let (media_source, thumbnail_source, thumbnail_info) = self .client .media() - .upload_plain_media_and_thumbnail( - content_type, - data, - config.thumbnail.take(), - send_progress, - ) + .upload_plain_media_and_thumbnail(content_type, data.clone(), thumbnail, send_progress) .await?; + if store_in_cache { + let cache_store = self.client.event_cache_store(); + + let request = MediaRequest { source: media_source.clone(), format: MediaFormat::File }; + // This shouldn't prevent the whole process from finishing properly. + if let Err(err) = cache_store.add_media_content(&request, data).await { + warn!("unable to cache the media after uploading it: {err}"); + } + } + let msg_type = self.make_attachment_message( content_type, media_source, diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 1bb8aa5d83c..c34f69b8b84 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -6,10 +6,15 @@ use matrix_sdk::{ Thumbnail, }, config::SyncSettings, + media::{MediaFormat, MediaRequest}, test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{async_test, mocks::mock_encryption_state, test_json, DEFAULT_TEST_ROOM_ID}; -use ruma::{event_id, events::Mentions, owned_user_id, uint}; +use ruma::{ + event_id, + events::{room::MediaSource, Mentions}, + owned_mxc_uri, owned_user_id, uint, +}; use serde_json::json; use wiremock::{ matchers::{body_partial_json, header, method, path, path_regex}, @@ -60,10 +65,29 @@ async fn test_room_attachment_send() { b"Hello world".to_vec(), AttachmentConfig::new(), ) + .store_in_cache() .await .unwrap(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id); + + // The media is immediately cached in the cache store, so we don't need to set + // up another mock endpoint for getting the media. + let reloaded = client + .media() + .get_media_content( + &MediaRequest { + source: MediaSource::Plain(owned_mxc_uri!( + "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + )), + format: MediaFormat::File, + }, + true, + ) + .await + .unwrap(); + + assert_eq!(reloaded, b"Hello world"); } #[async_test] From b46ebbf34e944f7b63e0d514591d6eb9ca7f9a6c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 21 Oct 2024 16:02:32 +0200 Subject: [PATCH 336/979] feat(media): don't clone the data when uploading an encrypted media --- crates/matrix-sdk/src/encryption/mod.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 0466716b3e6..b59c3e96d0a 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -456,7 +456,7 @@ impl Client { pub(crate) async fn upload_encrypted_media_and_thumbnail( &self, content_type: &mime::Mime, - data: Vec, + data: &[u8], thumbnail: Option, send_progress: SharedObservable, ) -> Result<(MediaSource, Option, Option>)> { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 09f1e8480fb..5d34fcf20fb 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1934,12 +1934,7 @@ impl Room { #[cfg(feature = "e2e-encryption")] let (media_source, thumbnail_source, thumbnail_info) = if self.is_encrypted().await? { self.client - .upload_encrypted_media_and_thumbnail( - content_type, - data.clone(), - thumbnail, - send_progress, - ) + .upload_encrypted_media_and_thumbnail(content_type, &data, thumbnail, send_progress) .await? } else { self.client From 9c03c5dd7e4acc1b7b683909009484f580593072 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 22 Oct 2024 11:37:43 +0200 Subject: [PATCH 337/979] feat(media): cache thumbnails too with a sensible media request key We can't know which key is going to be used precisely for the thumbnail, so assume non-animated cropped same-size thumbnail media request. Changelog: when `SendAttachment::store_in_cache()` is set, the thumbnail is also cached with a sensible default media request (not animated, cropped, same dimensions as the uploaded thumbnail). --- crates/matrix-sdk/src/room/mod.rs | 47 +++++++- .../tests/integration/room/attachment/mod.rs | 109 +++++++++++++----- 2 files changed, 129 insertions(+), 27 deletions(-) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 5d34fcf20fb..21f0b558f03 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -26,6 +26,7 @@ use matrix_sdk_base::{ deserialized_responses::{ RawAnySyncOrStrippedState, RawSyncOrStrippedState, SyncOrStrippedState, TimelineEvent, }, + media::{MediaThumbnailSettings, MediaThumbnailSize}, store::StateStoreExt, ComposerDraft, RoomInfoNotableUpdateReasons, RoomMemberships, StateChanges, StateStoreDataKey, StateStoreDataValue, @@ -1915,6 +1916,7 @@ impl Room { /// /// * `store_in_cache` - A boolean defining whether the uploaded media will /// be stored in the cache immediately after a successful upload. + #[instrument(skip_all)] pub(super) async fn prepare_and_send_attachment<'a>( &'a self, filename: &'a str, @@ -1931,6 +1933,20 @@ impl Room { let thumbnail = config.thumbnail.take(); + // If necessary, store caching data for the thumbnail ahead of time. + let thumbnail_cache_info = if store_in_cache { + // Use a small closure returning Option to avoid an unnecessary complicated + // chain of map/and_then. + let get_info = || { + let thumbnail = thumbnail.as_ref()?; + let info = thumbnail.info.as_ref()?; + Some((thumbnail.data.clone(), info.height?, info.width?)) + }; + get_info() + } else { + None + }; + #[cfg(feature = "e2e-encryption")] let (media_source, thumbnail_source, thumbnail_info) = if self.is_encrypted().await? { self.client @@ -1941,6 +1957,8 @@ impl Room { .media() .upload_plain_media_and_thumbnail( content_type, + // TODO: get rid of this clone; wait for Ruma to use `Bytes` or something + // similar. data.clone(), thumbnail, send_progress, @@ -1958,11 +1976,38 @@ impl Room { if store_in_cache { let cache_store = self.client.event_cache_store(); + // A failure to cache shouldn't prevent the whole upload from finishing + // properly, so only log errors during caching. + + debug!("caching the media"); let request = MediaRequest { source: media_source.clone(), format: MediaFormat::File }; - // This shouldn't prevent the whole process from finishing properly. if let Err(err) = cache_store.add_media_content(&request, data).await { warn!("unable to cache the media after uploading it: {err}"); } + + if let Some(((data, height, width), source)) = + thumbnail_cache_info.zip(thumbnail_source.as_ref()) + { + debug!("caching the thumbnail"); + + // Do a best guess at figuring the media request: not animated, cropped + // thumbnail of the original size. + let request = MediaRequest { + source: source.clone(), + format: MediaFormat::Thumbnail(MediaThumbnailSettings { + size: MediaThumbnailSize { + method: ruma::media::Method::Crop, + width, + height, + }, + animated: false, + }), + }; + + if let Err(err) = cache_store.add_media_content(&request, data).await { + warn!("unable to cache the media after uploading it: {err}"); + } + } } let msg_type = self.make_attachment_message( diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index c34f69b8b84..31e2d395958 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{sync::Mutex, time::Duration}; use matrix_sdk::{ attachment::{ @@ -6,7 +6,7 @@ use matrix_sdk::{ Thumbnail, }, config::SyncSettings, - media::{MediaFormat, MediaRequest}, + media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{async_test, mocks::mock_encryption_state, test_json, DEFAULT_TEST_ROOM_ID}; @@ -65,29 +65,10 @@ async fn test_room_attachment_send() { b"Hello world".to_vec(), AttachmentConfig::new(), ) - .store_in_cache() .await .unwrap(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id); - - // The media is immediately cached in the cache store, so we don't need to set - // up another mock endpoint for getting the media. - let reloaded = client - .media() - .get_media_content( - &MediaRequest { - source: MediaSource::Plain(owned_mxc_uri!( - "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - )), - format: MediaFormat::File, - }, - true, - ) - .await - .unwrap(); - - assert_eq!(reloaded, b"Hello world"); } #[async_test] @@ -201,6 +182,9 @@ async fn test_room_attachment_send_wrong_info() { async fn test_room_attachment_send_info_thumbnail() { let (client, server) = logged_in_client_with_server().await; + let media_mxc = owned_mxc_uri!("mxc://example.com/media"); + let thumbnail_mxc = owned_mxc_uri!("mxc://example.com/thumbnail"); + Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) .and(header("authorization", "Bearer 1234")) @@ -215,20 +199,37 @@ async fn test_room_attachment_send_info_thumbnail() { "mimetype":"image/jpeg", "size": 3600, }, - "thumbnail_url": "mxc://example.com/AQwafuaFswefuhsfAFAgsw", + "thumbnail_url": thumbnail_mxc, } }))) .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) .mount(&server) .await; + let counter = Mutex::new(0); Mock::given(method("POST")) .and(path("/_matrix/media/r0/upload")) .and(header("authorization", "Bearer 1234")) .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) + .respond_with({ + // First request: return the thumbnail MXC; + // Second request: return the media MXC. + let media_mxc = media_mxc.clone(); + let thumbnail_mxc = thumbnail_mxc.clone(); + move |_: &wiremock::Request| { + let mut counter = counter.lock().unwrap(); + if *counter == 0 { + *counter += 1; + ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": &thumbnail_mxc + })) + } else { + ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": &media_mxc + })) + } + } + }) .expect(2) .mount(&server) .await; @@ -242,6 +243,24 @@ async fn test_room_attachment_send_info_thumbnail() { let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + // Preconditions: nothing is found in the cache. + let media_request = + MediaRequest { source: MediaSource::Plain(media_mxc), format: MediaFormat::File }; + let thumbnail_request = MediaRequest { + source: MediaSource::Plain(thumbnail_mxc.clone()), + format: MediaFormat::Thumbnail(MediaThumbnailSettings { + size: MediaThumbnailSize { + method: ruma::media::Method::Crop, + width: uint!(480), + height: uint!(360), + }, + animated: false, + }), + }; + let _ = client.media().get_media_content(&media_request, true).await.unwrap_err(); + let _ = client.media().get_media_content(&thumbnail_request, true).await.unwrap_err(); + + // Send the attachment with a thumbnail. let config = AttachmentConfig::with_thumbnail(Thumbnail { data: b"Thumbnail".to_vec(), content_type: mime::IMAGE_JPEG, @@ -260,10 +279,48 @@ async fn test_room_attachment_send_info_thumbnail() { let response = room .send_attachment("image", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config) + .store_in_cache() .await .unwrap(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + // The event was sent. + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id); + + // The media is immediately cached in the cache store, so we don't need to set + // up another mock endpoint for getting the media. + let reloaded = client.media().get_media_content(&media_request, true).await.unwrap(); + assert_eq!(reloaded, b"Hello world"); + + // The thumbnail is cached with sensible defaults. + let reloaded = client.media().get_media_content(&thumbnail_request, true).await.unwrap(); + assert_eq!(reloaded, b"Thumbnail"); + + // The thumbnail can't be retrieved as a file. + let _ = client + .media() + .get_media_content( + &MediaRequest { + source: MediaSource::Plain(thumbnail_mxc.clone()), + format: MediaFormat::File, + }, + true, + ) + .await + .unwrap_err(); + + // But it is not found when requesting it as a thumbnail with a different size. + let thumbnail_request = MediaRequest { + source: MediaSource::Plain(thumbnail_mxc), + format: MediaFormat::Thumbnail(MediaThumbnailSettings { + size: MediaThumbnailSize { + method: ruma::media::Method::Crop, + width: uint!(42), + height: uint!(1337), + }, + animated: false, + }), + }; + let _ = client.media().get_media_content(&thumbnail_request, true).await.unwrap_err(); } #[async_test] From e3180cdbc536cb7ba8b49caf484ae2a78ac40af1 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 16 Oct 2024 10:17:16 +0100 Subject: [PATCH 338/979] fix(crypto): Don't warn about verified users when subscribing to identity updates --- .../src/room/identity_status_changes.rs | 133 +++++++++++++++++- crates/matrix-sdk/src/room/mod.rs | 4 +- .../src/test_json/keys_query_sets.rs | 5 +- 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index 47cbfad79e5..5d85456a4fb 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -20,7 +20,9 @@ use std::collections::BTreeMap; use async_stream::stream; use futures_core::Stream; use futures_util::{stream_select, StreamExt}; -use matrix_sdk_base::crypto::{IdentityStatusChange, RoomIdentityChange, RoomIdentityState}; +use matrix_sdk_base::crypto::{ + IdentityState, IdentityStatusChange, RoomIdentityChange, RoomIdentityState, +}; use ruma::{events::room::member::SyncRoomMemberEvent, OwnedUserId, UserId}; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; @@ -70,6 +72,10 @@ impl IdentityStatusChanges { /// client to display a list of room members whose identities have /// changed, and allow the user to acknowledge this or act upon it. /// + /// The first item in the stream provides the current state of the room: + /// each member of the room who is not in "pinned" or "verified" state will + /// be included (except the current user). + /// /// Note: when an unpinned user leaves a room, an update is generated /// stating that they have become pinned, even though they may not /// necessarily have become pinned, but we don't care any more because they @@ -89,7 +95,7 @@ impl IdentityStatusChanges { Ok(stream!({ let mut current_state = - filter_non_self(state.room_identity_state.current_state(), &own_user_id); + filter_for_initial_update(state.room_identity_state.current_state(), &own_user_id); if !current_state.is_empty() { current_state.sort(); @@ -110,10 +116,26 @@ impl IdentityStatusChanges { } } +fn filter_for_initial_update( + mut input: Vec, + own_user_id: &UserId, +) -> Vec { + // We are never interested in changes to our own identity, and also for initial + // updates, we are only interested in "bad" states where we need to + // notify the user, so we can remove Verified states (Pinned states are + // already missing, because Pinned is considered the default). + input.retain(|change| { + change.user_id != own_user_id && change.changed_to != IdentityState::Verified + }); + + input +} + fn filter_non_self( mut input: Vec, own_user_id: &UserId, ) -> Vec { + // We are never interested in changes to our own identity input.retain(|change| change.user_id != own_user_id); input } @@ -355,6 +377,41 @@ mod tests { assert_eq!(change4.len(), 1); } + #[async_test] + async fn test_when_an_unpinned_user_is_already_present_we_report_it_immediately() { + // Given a room containing Bob, who is unpinned + let t = TestSetup::new_room_with_other_member().await; + t.unpin().await; + + // When we start listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + + // Then we were immediately notified about Bob being unpinned + let change = next_change(&mut pin!(changes)).await; + assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].changed_to, IdentityState::PinViolation); + assert_eq!(change.len(), 1); + } + + #[async_test] + async fn test_when_a_verified_user_is_already_present_we_dont_report_it() { + // Given a room containing Bob, who is unpinned + let t = TestSetup::new_room_with_other_member().await; + t.verify().await; + + // When we start listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + + // (And we unpin so that something is available in the changes stream) + t.unpin().await; + + // Then we were only notified about the unpin, not being verified + let change = next_change(&mut pin!(changes)).await; + assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].changed_to, IdentityState::VerificationViolation); + assert_eq!(change.len(), 1); + } + // TODO: I (andyb) haven't figured out how to test room membership changes that // affect our own user (they should not be shown). Specifically, I haven't // figure out how to get out own user into a non-pinned state. @@ -374,12 +431,15 @@ mod tests { use futures_core::Stream; use matrix_sdk_base::{ - crypto::{IdentityStatusChange, OtherUserIdentity}, + crypto::{ + testing::simulate_key_query_response_for_verification, IdentityStatusChange, + OtherUserIdentity, + }, RoomState, }; use matrix_sdk_test::{ - test_json::{self, keys_query_sets::IdentityChangeDataSet}, - JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, + test_json, test_json::keys_query_sets::IdentityChangeDataSet, JoinedRoomBuilder, + StateTestEvent, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, }; use ruma::{ api::client::keys::get_keys, events::room::member::MembershipState, owned_user_id, @@ -445,7 +505,7 @@ mod tests { self.change_identity(IdentityChangeDataSet::key_query_with_identity_a()).await; } - // Sanity check: we are pinned + // Sanity check: they are pinned assert!(self.is_pinned().await); } @@ -459,10 +519,55 @@ mod tests { self.change_identity(IdentityChangeDataSet::key_query_with_identity_b()).await; } - // Sanity: we are unpinned + // Sanity: they are unpinned assert!(!self.is_pinned().await); } + pub(super) async fn verify(&self) { + // If they don't have an identity yet, set one up + if self.user_identity().await.is_none() { + self.change_identity(IdentityChangeDataSet::key_query_with_identity_a()).await; + } + + let my_user_id = self.client.user_id().expect("I should have a user id"); + let my_identity = self + .client + .encryption() + .get_user_identity(my_user_id) + .await + .expect("Should not fail to get own user identity") + .expect("Should have an own user identity") + .underlying_identity() + .own() + .expect("Our own identity should be of type Own"); + + // Get the request + let signature_upload_request = self + .crypto_other_identity() + .await + .verify() + .await + .expect("Should be able to verify other identity"); + + let verification_response = simulate_key_query_response_for_verification( + signature_upload_request, + my_identity, + my_user_id, + self.user_id(), + IdentityChangeDataSet::msk_a(), + IdentityChangeDataSet::ssk_a(), + ); + + // Receive the response into our client + self.client + .mark_request_as_sent(&TransactionId::new(), &verification_response) + .await + .unwrap(); + + // Sanity: they are verified + assert!(self.is_verified().await); + } + pub(super) async fn join(&mut self) { self.membership_change(MembershipState::Join).await; } @@ -484,6 +589,16 @@ mod tests { async fn init() -> (Client, OwnedUserId, SyncResponseBuilder) { let (client, _server) = create_client_and_server().await; + // Ensure our user has cross-signing keys etc. + client + .olm_machine() + .await + .as_ref() + .expect("We should have an Olm machine") + .bootstrap_cross_signing(true) + .await + .expect("Should be able to bootstrap cross-signing"); + // Note: if you change the user_id, you will need to change lots of hard-coded // stuff inside IdentityChangeDataSet let user_id = owned_user_id!("@bob:localhost"); @@ -539,6 +654,10 @@ mod tests { !self.crypto_other_identity().await.identity_needs_user_approval() } + async fn is_verified(&self) -> bool { + self.crypto_other_identity().await.is_verified() + } + async fn crypto_other_identity(&self) -> OtherUserIdentity { self.user_identity() .await diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 21f0b558f03..b6588f15f0f 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -392,8 +392,8 @@ impl Room { /// will be included.) /// /// The first item in the stream provides the current state of the room: - /// each member of the room who is not in "pinned" state will be - /// included (except the current user). + /// each member of the room who is not in "pinned" or "verified" state will + /// be included (except the current user). /// /// If the `changed_to` property of an [`IdentityStatusChange`] is set to /// `PinViolation` then a warning should be displayed to the user. If it is diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index ae1d8864f77..4c357acc78e 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -486,7 +486,7 @@ impl IdentityChangeDataSet { }) } - fn msk_a() -> Value { + pub fn msk_a() -> Value { json!({ "@bob:localhost": { "keys": { @@ -505,7 +505,8 @@ impl IdentityChangeDataSet { } }) } - fn ssk_a() -> Value { + + pub fn ssk_a() -> Value { json!({ "@bob:localhost": { "keys": { From 74722f48aadae2ceeda4c73ad8435e330b5ddf83 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 13:21:22 +0200 Subject: [PATCH 339/979] fix(ui): Add the `m.call.member` state event in the required state. This patch adds the `m.call.member` state event in the `required_state` for `all_rooms` of the `RoomListService`. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 2 +- crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 932d0e9e019..843f1b6f268 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -151,8 +151,8 @@ impl RoomListService { (StateEventType::RoomMember, "$ME".to_owned()), (StateEventType::RoomName, "".to_owned()), (StateEventType::RoomCanonicalAlias, "".to_owned()), - (StateEventType::RoomAvatar, "".to_owned()), (StateEventType::RoomPowerLevels, "".to_owned()), + (StateEventType::CallMember, "".to_owned()), ]) .include_heroes(Some(true)) .filters(Some(assign!(http::request::ListFilters::default(), { 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 db7291209e6..8580a0dbf48 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -334,6 +334,7 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.canonical_alias", ""], ["m.room.avatar", ""], ["m.room.power_levels", ""], + ["m.call.member", ""], ], "include_heroes": true, "filters": { From 996b391506e2d2172433dcd56d86db564dfb890e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 13:28:13 +0200 Subject: [PATCH 340/979] feat(ui): Add `m.room.topic` and `m.room.pinned_events` in `all_rooms`. This patch adds the `m.room.topic` and `m.room.pinned_events` state events in the `required_state` of the `all_rooms` sliding sync list of `RoomListService`. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 4 +++- .../matrix-sdk-ui/tests/integration/room_list_service.rs | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 843f1b6f268..08237d6bdaa 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -146,12 +146,14 @@ impl RoomListService { ) .timeline_limit(1) .required_state(vec![ + (StateEventType::RoomName, "".to_owned()), (StateEventType::RoomEncryption, "".to_owned()), (StateEventType::RoomMember, "$LAZY".to_owned()), (StateEventType::RoomMember, "$ME".to_owned()), - (StateEventType::RoomName, "".to_owned()), + (StateEventType::RoomTopic, "".to_owned()), (StateEventType::RoomCanonicalAlias, "".to_owned()), (StateEventType::RoomPowerLevels, "".to_owned()), + (StateEventType::RoomPinnedEvents, "".to_owned()), (StateEventType::CallMember, "".to_owned()), ]) .include_heroes(Some(true)) 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 8580a0dbf48..efde3a3bf50 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -327,14 +327,15 @@ async fn test_sync_all_states() -> Result<(), Error> { ALL_ROOMS: { "ranges": [[0, 19]], "required_state": [ + ["m.room.name", ""], ["m.room.encryption", ""], ["m.room.member", "$LAZY"], ["m.room.member", "$ME"], - ["m.room.name", ""], + ["m.room.topic", ""], ["m.room.canonical_alias", ""], - ["m.room.avatar", ""], ["m.room.power_levels", ""], - ["m.call.member", ""], + ["m.room.pinned_events", ""], + ["org.matrix.msc3401.call.member", ""], ], "include_heroes": true, "filters": { From e62c47132e2145ec67c2eaf5f899aa40425e4eca Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 13:49:58 +0200 Subject: [PATCH 341/979] feat(ui): `RoomListService::subscribe_to_rooms` no longer has a `settings` argument. This patch removes the `settings` argument of `RoomListService::subscribe_to_rooms`. The settings were mostly composed of: * `required_state`: now shared with `all_rooms`, so that we are sure they are synced; except that `m.room.create` is added for subscriptions. * `timeline_limit`: now defaults to 20. This patch thus creates the `DEFAULT_REQUIRED_STATE` and `DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT` constants. Finally, this patch updates the tests, and updates all usages of `subscribe_to_rooms`. --- bindings/matrix-sdk-ffi/src/room_list.rs | 45 ++---------- .../src/room_list_service/mod.rs | 63 +++++++++-------- .../tests/integration/room_list_service.rs | 68 ++++++------------- labs/multiverse/src/main.rs | 8 +-- 4 files changed, 61 insertions(+), 123 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index d8700571580..5eae181a6c7 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -2,12 +2,9 @@ use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Durat use eyeball_im::VectorDiff; use futures_util::{pin_mut, StreamExt, TryFutureExt}; -use matrix_sdk::{ - ruma::{ - api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount, - assign, RoomId, - }, - sliding_sync::http, +use matrix_sdk::ruma::{ + api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount, + RoomId, }; use matrix_sdk_ui::{ room_list_service::filters::{ @@ -135,11 +132,7 @@ impl RoomListService { }))) } - fn subscribe_to_rooms( - &self, - room_ids: Vec, - settings: Option, - ) -> Result<(), RoomListError> { + fn subscribe_to_rooms(&self, room_ids: Vec) -> Result<(), RoomListError> { let room_ids = room_ids .into_iter() .map(|room_id| { @@ -147,10 +140,7 @@ impl RoomListService { }) .collect::, _>>()?; - self.inner.subscribe_to_rooms( - &room_ids.iter().map(AsRef::as_ref).collect::>(), - settings.map(Into::into), - ); + self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::>()); Ok(()) } @@ -680,31 +670,6 @@ impl RoomListItem { } } -#[derive(uniffi::Record)] -pub struct RequiredState { - pub key: String, - pub value: String, -} - -#[derive(uniffi::Record)] -pub struct RoomSubscription { - pub required_state: Option>, - pub timeline_limit: u32, - pub include_heroes: Option, -} - -impl From for http::request::RoomSubscription { - fn from(val: RoomSubscription) -> Self { - assign!(http::request::RoomSubscription::default(), { - required_state: val.required_state.map(|r| - r.into_iter().map(|s| (s.key.into(), s.value)).collect() - ).unwrap_or_default(), - timeline_limit: val.timeline_limit.into(), - include_heroes: val.include_heroes, - }) - } -} - #[derive(uniffi::Object)] pub struct UnreadNotificationsCount { highlight_count: u32, diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 08237d6bdaa..327e5147580 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -57,7 +57,7 @@ mod room_list; pub mod sorters; mod state; -use std::{sync::Arc, time::Duration}; +use std::{iter::once, sync::Arc, time::Duration}; use async_stream::stream; use eyeball::Subscriber; @@ -69,7 +69,7 @@ use matrix_sdk::{ use matrix_sdk_base::sliding_sync::http; pub use room::*; pub use room_list::*; -use ruma::{assign, directory::RoomTypeFilter, events::StateEventType, OwnedRoomId, RoomId}; +use ruma::{assign, directory::RoomTypeFilter, events::StateEventType, OwnedRoomId, RoomId, UInt}; pub use state::*; use thiserror::Error; use tokio::time::timeout; @@ -77,6 +77,22 @@ use tracing::debug; use crate::timeline; +/// The default `required_state` constant value for sliding sync lists and +/// sliding sync room subscriptions. +const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[ + (StateEventType::RoomName, ""), + (StateEventType::RoomEncryption, ""), + (StateEventType::RoomMember, "$LAZY"), + (StateEventType::RoomMember, "$ME"), + (StateEventType::RoomTopic, ""), + (StateEventType::RoomCanonicalAlias, ""), + (StateEventType::RoomPowerLevels, ""), + (StateEventType::CallMember, ""), +]; + +/// The default `timeline_limit` value when used with room subscriptions. +const DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT: u32 = 20; + /// The [`RoomListService`] type. See the module's documentation to learn more. #[derive(Debug)] pub struct RoomListService { @@ -145,17 +161,12 @@ impl RoomListService { .add_range(ALL_ROOMS_DEFAULT_SELECTIVE_RANGE), ) .timeline_limit(1) - .required_state(vec![ - (StateEventType::RoomName, "".to_owned()), - (StateEventType::RoomEncryption, "".to_owned()), - (StateEventType::RoomMember, "$LAZY".to_owned()), - (StateEventType::RoomMember, "$ME".to_owned()), - (StateEventType::RoomTopic, "".to_owned()), - (StateEventType::RoomCanonicalAlias, "".to_owned()), - (StateEventType::RoomPowerLevels, "".to_owned()), - (StateEventType::RoomPinnedEvents, "".to_owned()), - (StateEventType::CallMember, "".to_owned()), - ]) + .required_state( + DEFAULT_REQUIRED_STATE + .iter() + .map(|(state_event, value)| (state_event.clone(), (*value).to_owned())) + .collect(), + ) .include_heroes(Some(true)) .filters(Some(assign!(http::request::ListFilters::default(), { // As defined in the [SlidingSync MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/9450ced7fb9cf5ea9077d029b3adf36aebfa8709/proposals/3575-sync.md?plain=1#L444) @@ -382,22 +393,16 @@ impl RoomListService { /// /// It means that all events from these rooms will be received every time, /// no matter how the `RoomList` is configured. - pub fn subscribe_to_rooms( - &self, - room_ids: &[&RoomId], - settings: Option, - ) { - let mut settings = settings.unwrap_or_default(); - - // Make sure to always include the room creation event in the required state - // events, to know what the room version is. - if !settings - .required_state - .iter() - .any(|(event_type, _state_key)| *event_type == StateEventType::RoomCreate) - { - settings.required_state.push((StateEventType::RoomCreate, "".to_owned())); - } + pub fn subscribe_to_rooms(&self, room_ids: &[&RoomId]) { + let settings = assign!(http::request::RoomSubscription::default(), { + required_state: DEFAULT_REQUIRED_STATE.iter().map(|(state_event, value)| { + (state_event.clone(), (*value).to_owned()) + }) + .chain(once((StateEventType::RoomCreate, "".to_owned()))) + .chain(once((StateEventType::RoomPinnedEvents, "".to_owned()))) + .collect(), + timeline_limit: UInt::from(DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT), + }); let cancel_in_flight_request = match self.state_machine.get() { State::Init | State::Recovering | State::Error { .. } | State::Terminated { .. } => { 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 efde3a3bf50..3fd0c52f91e 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -7,9 +7,7 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; use futures_util::{pin_mut, FutureExt, StreamExt}; use matrix_sdk::{test_utils::logged_in_client_with_server, Client}; -use matrix_sdk_base::{ - sliding_sync::http::request::RoomSubscription, sync::UnreadNotificationsCount, -}; +use matrix_sdk_base::sync::UnreadNotificationsCount; use matrix_sdk_test::{async_test, mocks::mock_encryption_state}; use matrix_sdk_ui::{ room_list_service::{ @@ -20,10 +18,8 @@ use matrix_sdk_ui::{ RoomListService, }; use ruma::{ - api::client::room::create_room::v3::Request as CreateRoomRequest, - assign, event_id, - events::{room::message::RoomMessageEventContent, StateEventType}, - mxc_uri, room_id, uint, + api::client::room::create_room::v3::Request as CreateRoomRequest, event_id, + events::room::message::RoomMessageEventContent, mxc_uri, room_id, }; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; @@ -334,7 +330,6 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.topic", ""], ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], - ["m.room.pinned_events", ""], ["org.matrix.msc3401.call.member", ""], ], "include_heroes": true, @@ -2083,18 +2078,7 @@ async fn test_room_subscription() -> Result<(), Error> { // Subscribe. - room_list.subscribe_to_rooms( - &[room_id_1], - Some(assign!(RoomSubscription::default(), { - required_state: vec![ - (StateEventType::RoomName, "".to_owned()), - (StateEventType::RoomTopic, "".to_owned()), - (StateEventType::RoomAvatar, "".to_owned()), - (StateEventType::RoomCanonicalAlias, "".to_owned()), - ], - timeline_limit: uint!(30), - })), - ); + room_list.subscribe_to_rooms(&[room_id_1]); sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -2109,12 +2093,17 @@ async fn test_room_subscription() -> Result<(), Error> { room_id_1: { "required_state": [ ["m.room.name", ""], + ["m.room.encryption", ""], + ["m.room.member", "$LAZY"], + ["m.room.member", "$ME"], ["m.room.topic", ""], - ["m.room.avatar", ""], ["m.room.canonical_alias", ""], + ["m.room.power_levels", ""], + ["org.matrix.msc3401.call.member", ""], ["m.room.create", ""], + ["m.room.pinned_events", ""], ], - "timeline_limit": 30, + "timeline_limit": 20, }, }, }, @@ -2127,18 +2116,7 @@ async fn test_room_subscription() -> Result<(), Error> { // Subscribe to another room. - room_list.subscribe_to_rooms( - &[room_id_2], - Some(assign!(RoomSubscription::default(), { - required_state: vec![ - (StateEventType::RoomName, "".to_owned()), - (StateEventType::RoomTopic, "".to_owned()), - (StateEventType::RoomAvatar, "".to_owned()), - (StateEventType::RoomCanonicalAlias, "".to_owned()), - ], - timeline_limit: uint!(30), - })), - ); + room_list.subscribe_to_rooms(&[room_id_2]); sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -2153,12 +2131,17 @@ async fn test_room_subscription() -> Result<(), Error> { room_id_2: { "required_state": [ ["m.room.name", ""], + ["m.room.encryption", ""], + ["m.room.member", "$LAZY"], + ["m.room.member", "$ME"], ["m.room.topic", ""], - ["m.room.avatar", ""], ["m.room.canonical_alias", ""], + ["m.room.power_levels", ""], + ["org.matrix.msc3401.call.member", ""], ["m.room.create", ""], + ["m.room.pinned_events", ""], ], - "timeline_limit": 30, + "timeline_limit": 20, }, }, }, @@ -2171,18 +2154,7 @@ async fn test_room_subscription() -> Result<(), Error> { // Subscribe to an already subscribed room. Nothing happens. - room_list.subscribe_to_rooms( - &[room_id_1], - Some(assign!(RoomSubscription::default(), { - required_state: vec![ - (StateEventType::RoomName, "".to_owned()), - (StateEventType::RoomTopic, "".to_owned()), - (StateEventType::RoomAvatar, "".to_owned()), - (StateEventType::RoomCanonicalAlias, "".to_owned()), - ], - timeline_limit: uint!(30), - })), - ); + room_list.subscribe_to_rooms(&[room_id_1]); sync_then_assert_request_and_fake_response! { [server, room_list, sync] diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index e844449cec7..810766fa7cb 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -23,9 +23,8 @@ use matrix_sdk::{ ruma::{ api::client::receipt::create_receipt::v3::ReceiptType, events::room::message::{MessageType, RoomMessageEventContent}, - uint, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, + MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, }, - sliding_sync::http::request::RoomSubscription, AuthSession, Client, ServerName, SqliteCryptoStore, SqliteStateStore, }; use matrix_sdk_ui::{ @@ -445,10 +444,7 @@ impl App { .get_selected_room_id(Some(selected)) .and_then(|room_id| self.ui_rooms.lock().unwrap().get(&room_id).cloned()) { - let mut sub = RoomSubscription::default(); - sub.timeline_limit = uint!(30); - - self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()], Some(sub)); + self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()]); self.current_room_subscription = Some(room); } } From 65bb3733790c5655d1ff7f9a5f9d410d1cfcf915 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 14:17:19 +0200 Subject: [PATCH 342/979] chore(ui): Add the `DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE` constant. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch refactors 2 `chain(once(…))` with a 1 `chain`. It also clarifies the extra `required_state` that are added for room subscriptions. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 327e5147580..261cced7c14 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -57,7 +57,7 @@ mod room_list; pub mod sorters; mod state; -use std::{iter::once, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; use async_stream::stream; use eyeball::Subscriber; @@ -90,6 +90,11 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[ (StateEventType::CallMember, ""), ]; +/// The default `required_state` constant value for sliding sync room +/// subscriptions that must be added to `DEFAULT_REQUIRED_STATE`. +const DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE: &[(StateEventType, &str)] = + &[(StateEventType::RoomCreate, ""), (StateEventType::RoomPinnedEvents, "")]; + /// The default `timeline_limit` value when used with room subscriptions. const DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT: u32 = 20; @@ -398,8 +403,11 @@ impl RoomListService { required_state: DEFAULT_REQUIRED_STATE.iter().map(|(state_event, value)| { (state_event.clone(), (*value).to_owned()) }) - .chain(once((StateEventType::RoomCreate, "".to_owned()))) - .chain(once((StateEventType::RoomPinnedEvents, "".to_owned()))) + .chain( + DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE.iter().map(|(state_event, value)| { + (state_event.clone(), (*value).to_owned()) + }) + ) .collect(), timeline_limit: UInt::from(DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT), }); From 6196ebaba6db68b957b4f90255a05ffa6e2f3d3c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 22 Oct 2024 15:30:38 +0200 Subject: [PATCH 343/979] chore(media): use the same media method when caching a thumbnail as the default one used in the FFI The FFI will request a scaled version of the thumbnail by default; let's use the same cache key when caching the thumbnail after an upload. Thanks @zecakeh for flagging the issue. --- crates/matrix-sdk/src/room/mod.rs | 2 +- crates/matrix-sdk/tests/integration/room/attachment/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index b6588f15f0f..1fa314d473c 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1996,7 +1996,7 @@ impl Room { source: source.clone(), format: MediaFormat::Thumbnail(MediaThumbnailSettings { size: MediaThumbnailSize { - method: ruma::media::Method::Crop, + method: ruma::media::Method::Scale, width, height, }, diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 31e2d395958..252721f6d5d 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -250,7 +250,7 @@ async fn test_room_attachment_send_info_thumbnail() { source: MediaSource::Plain(thumbnail_mxc.clone()), format: MediaFormat::Thumbnail(MediaThumbnailSettings { size: MediaThumbnailSize { - method: ruma::media::Method::Crop, + method: ruma::media::Method::Scale, width: uint!(480), height: uint!(360), }, @@ -284,7 +284,7 @@ async fn test_room_attachment_send_info_thumbnail() { .unwrap(); // The event was sent. - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id); + assert_eq!(response.event_id, event_id!("$h29iv0s8:example.com")); // The media is immediately cached in the cache store, so we don't need to set // up another mock endpoint for getting the media. @@ -313,7 +313,7 @@ async fn test_room_attachment_send_info_thumbnail() { source: MediaSource::Plain(thumbnail_mxc), format: MediaFormat::Thumbnail(MediaThumbnailSettings { size: MediaThumbnailSize { - method: ruma::media::Method::Crop, + method: ruma::media::Method::Scale, width: uint!(42), height: uint!(1337), }, From f2f99fb207289e2b0b82c21eee81477a7e4049bc Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 22 Oct 2024 17:23:14 +0300 Subject: [PATCH 344/979] chore(ffi): move the `store_in_cache` timeline media upload parameter before the `progress_watcher` closure for aesthetic reasons --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index b27bc3d0c0c..15ac286a6d7 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -103,8 +103,8 @@ impl Timeline { filename: String, mime_type: Option, attachment_config: AttachmentConfig, - progress_watcher: Option>, store_in_cache: bool, + progress_watcher: Option>, ) -> Result<(), RoomError> { let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; let mime_type = @@ -284,8 +284,8 @@ impl Timeline { image_info: ImageInfo, caption: Option, formatted_caption: Option, - progress_watcher: Option>, store_in_cache: bool, + progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_image_info = BaseImageInfo::try_from(&image_info) @@ -301,8 +301,8 @@ impl Timeline { url, image_info.mimetype, attachment_config, - progress_watcher, store_in_cache, + progress_watcher, ) .await })) @@ -316,8 +316,8 @@ impl Timeline { video_info: VideoInfo, caption: Option, formatted_caption: Option, - progress_watcher: Option>, store_in_cache: bool, + progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) @@ -333,8 +333,8 @@ impl Timeline { url, video_info.mimetype, attachment_config, - progress_watcher, store_in_cache, + progress_watcher, ) .await })) @@ -346,8 +346,8 @@ impl Timeline { audio_info: AudioInfo, caption: Option, formatted_caption: Option, - progress_watcher: Option>, store_in_cache: bool, + progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) @@ -363,8 +363,8 @@ impl Timeline { url, audio_info.mimetype, attachment_config, - progress_watcher, store_in_cache, + progress_watcher, ) .await })) @@ -378,8 +378,8 @@ impl Timeline { waveform: Vec, caption: Option, formatted_caption: Option, - progress_watcher: Option>, store_in_cache: bool, + progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) @@ -396,8 +396,8 @@ impl Timeline { url, audio_info.mimetype, attachment_config, - progress_watcher, store_in_cache, + progress_watcher, ) .await })) @@ -407,8 +407,8 @@ impl Timeline { self: Arc, url: String, file_info: FileInfo, - progress_watcher: Option>, store_in_cache: bool, + progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_file_info: BaseFileInfo = @@ -421,8 +421,8 @@ impl Timeline { url, file_info.mimetype, attachment_config, - progress_watcher, store_in_cache, + progress_watcher, ) .await })) From 3f5d54c494b5de8bc6d80a79d5ab9e5678e161e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 17 Oct 2024 17:26:17 +0200 Subject: [PATCH 345/979] chore(knocking): Add optional `reason` and `server_names` parameters to `Client::knock` --- bindings/matrix-sdk-ffi/src/client.rs | 11 +++++++++-- crates/matrix-sdk/src/client/mod.rs | 10 ++++++++-- crates/matrix-sdk/tests/integration/room/left.rs | 4 ++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index b04d9ca5943..f5f693bae21 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -977,9 +977,16 @@ impl Client { } /// Knock on a room to join it using its ID or alias. - pub async fn knock(&self, room_id_or_alias: String) -> Result, ClientError> { + pub async fn knock( + &self, + room_id_or_alias: String, + reason: Option, + server_names: Vec, + ) -> Result, ClientError> { let room_id = RoomOrAliasId::parse(&room_id_or_alias)?; - let room = self.inner.knock(room_id).await?; + let server_names = + server_names.iter().map(ServerName::parse).collect::, _>>()?; + let room = self.inner.knock(room_id, reason, server_names).await?; Ok(Arc::new(Room::new(room))) } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 92619b03089..e2304998034 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2243,8 +2243,14 @@ impl Client { /// Knock on a room given its `room_id_or_alias` to ask for permission to /// join it. - pub async fn knock(&self, room_id_or_alias: OwnedRoomOrAliasId) -> Result { - let request = knock_room::v3::Request::new(room_id_or_alias); + pub async fn knock( + &self, + room_id_or_alias: OwnedRoomOrAliasId, + reason: Option, + server_names: Vec, + ) -> Result { + let request = + assign!(knock_room::v3::Request::new(room_id_or_alias), { reason, via: server_names }); let response = self.send(request, None).await?; let base_room = self.inner.base_client.room_knocked(&response.room_id).await?; Ok(Room::new(self.clone(), base_room)) diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs index 13042ae1ba6..f2de95649b5 100644 --- a/crates/matrix-sdk/tests/integration/room/left.rs +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -150,7 +150,7 @@ async fn test_knocking() { let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); assert_eq!(room.state(), RoomState::Left); - let room = - client.knock(OwnedRoomOrAliasId::from((*DEFAULT_TEST_ROOM_ID).to_owned())).await.unwrap(); + let room_id = OwnedRoomOrAliasId::from((*DEFAULT_TEST_ROOM_ID).to_owned()); + let room = client.knock(room_id, None, Vec::new()).await.unwrap(); assert_eq!(room.state(), RoomState::Knocked); } From 74de617d7649391e8d47b145f4aef2942caa9bbe Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 11 Oct 2024 16:05:52 +0100 Subject: [PATCH 346/979] refactor(ui): add `TimelineEventKind::UnableToDecrypt` Give `matrix-sdk-ui::event_handler::TimelineEventKind` a new variant which specifically represents events that could not be decrypted. --- .../src/timeline/event_handler.rs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 1161030522f..835ec1f5641 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -35,6 +35,7 @@ use ruma::{ receipt::Receipt, relation::Replacement, room::{ + encrypted::RoomEncryptedEventContent, member::RoomMemberEventContent, message::{self, RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, }, @@ -137,6 +138,9 @@ pub(super) enum TimelineEventKind { relations: BundledMessageLikeRelations, }, + /// An encrypted event that could not be decrypted + UnableToDecrypt { content: RoomEncryptedEventContent }, + /// Some remote event that was redacted a priori, i.e. we never had the /// original content, so we'll just display a dummy redacted timeline /// item. @@ -182,6 +186,10 @@ impl TimelineEventKind { } } AnySyncTimelineEvent::MessageLike(ev) => match ev.original_content() { + Some(AnyMessageLikeEventContent::RoomEncrypted(content)) => { + // An event which is still encrypted. + Self::UnableToDecrypt { content } + } Some(content) => Self::Message { content, relations: ev.relations() }, None => Self::RedactedMessage { event_type: ev.event_type() }, }, @@ -344,21 +352,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } - AnyMessageLikeEventContent::RoomEncrypted(c) => { - // TODO: Handle replacements if the replaced event is also UTD - let raw_event = self.ctx.flow.raw_event(); - let cause = UtdCause::determine(raw_event); - self.add_item(TimelineItemContent::unable_to_decrypt(c, cause), None); - - // Let the hook know that we ran into an unable-to-decrypt that is added to the - // timeline. - if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() { - if let Some(event_id) = &self.ctx.flow.event_id() { - hook.on_utd(event_id, cause).await; - } - } - } - AnyMessageLikeEventContent::Sticker(content) => { if should_add { self.add_item(TimelineItemContent::Sticker(Sticker { content }), None); @@ -402,6 +395,21 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } }, + TimelineEventKind::UnableToDecrypt { content } => { + // TODO: Handle replacements if the replaced event is also UTD + let raw_event = self.ctx.flow.raw_event(); + let cause = UtdCause::determine(raw_event); + self.add_item(TimelineItemContent::unable_to_decrypt(content, cause), None); + + // Let the hook know that we ran into an unable-to-decrypt that is added to the + // timeline. + if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() { + if let Some(event_id) = &self.ctx.flow.event_id() { + hook.on_utd(event_id, cause).await; + } + } + } + TimelineEventKind::RedactedMessage { event_type } => { if event_type != MessageLikeEventType::Reaction && should_add { self.add_item(TimelineItemContent::RedactedMessage, None); From a61bc3cbbdedc3e099f42af01599c6df641f06a0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 11 Oct 2024 16:21:20 +0100 Subject: [PATCH 347/979] refactor(ui): add UTD info to `TimelineEventKind::UnableToDecrypt` Stash the reason for the decryption failure in `matrix-sdk-ui::event_handler::TimelineEventKind::UnableToDecrypt`. It's not yet used. --- .../src/timeline/controller/state.rs | 20 ++++++++--- .../src/timeline/event_handler.rs | 33 +++++++++++++++---- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4f23f58a426..b30102525fb 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -422,7 +422,17 @@ impl TimelineStateTransaction<'_> { settings: &TimelineSettings, day_divider_adjuster: &mut DayDividerAdjuster, ) -> HandleEventResult { - let raw = event.raw(); + let SyncTimelineEvent { push_actions, kind } = event; + let encryption_info = kind.encryption_info().cloned(); + + let (raw, utd_info) = match kind { + matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt { + utd_info, + event, + } => (event, Some(utd_info)), + _ => (kind.into_raw(), None), + }; + let (event_id, sender, timestamp, txn_id, event_kind, should_add) = match raw.deserialize() { Ok(event) => { @@ -479,7 +489,7 @@ impl TimelineStateTransaction<'_> { event.sender().to_owned(), event.origin_server_ts(), event.transaction_id().map(ToOwned::to_owned), - TimelineEventKind::from_event(event, &room_version), + TimelineEventKind::from_event(event, &room_version, utd_info), should_add, ) } @@ -578,11 +588,11 @@ impl TimelineStateTransaction<'_> { } else { Default::default() }, - is_highlighted: event.push_actions.iter().any(Action::is_highlight), + is_highlighted: push_actions.iter().any(Action::is_highlight), flow: Flow::Remote { event_id: event_id.clone(), - raw_event: raw.clone(), - encryption_info: event.encryption_info().cloned(), + raw_event: raw, + encryption_info, txn_id, position, }, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 835ec1f5641..210fec68e39 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -18,8 +18,10 @@ use as_variant::as_variant; use eyeball_im::{ObservableVectorTransaction, ObservableVectorTransactionEntry}; use indexmap::IndexMap; use matrix_sdk::{ - crypto::types::events::UtdCause, deserialized_responses::EncryptionInfo, - ring_buffer::RingBuffer, send_queue::SendHandle, + crypto::types::events::UtdCause, + deserialized_responses::{EncryptionInfo, UnableToDecryptInfo}, + ring_buffer::RingBuffer, + send_queue::SendHandle, }; use ruma::{ events::{ @@ -139,7 +141,10 @@ pub(super) enum TimelineEventKind { }, /// An encrypted event that could not be decrypted - UnableToDecrypt { content: RoomEncryptedEventContent }, + UnableToDecrypt { + content: RoomEncryptedEventContent, + unable_to_decrypt_info: UnableToDecryptInfo, + }, /// Some remote event that was redacted a priori, i.e. we never had the /// original content, so we'll just display a dummy redacted timeline @@ -176,7 +181,11 @@ pub(super) enum TimelineEventKind { impl TimelineEventKind { /// Creates a new `TimelineEventKind` with the given event and room version. - pub fn from_event(event: AnySyncTimelineEvent, room_version: &RoomVersionId) -> Self { + pub fn from_event( + event: AnySyncTimelineEvent, + room_version: &RoomVersionId, + unable_to_decrypt_info: Option, + ) -> Self { match event { AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(ev)) => { if let Some(redacts) = ev.redacts(room_version).map(ToOwned::to_owned) { @@ -188,7 +197,19 @@ impl TimelineEventKind { AnySyncTimelineEvent::MessageLike(ev) => match ev.original_content() { Some(AnyMessageLikeEventContent::RoomEncrypted(content)) => { // An event which is still encrypted. - Self::UnableToDecrypt { content } + if let Some(unable_to_decrypt_info) = unable_to_decrypt_info { + Self::UnableToDecrypt { content, unable_to_decrypt_info } + } else { + // If we get here, it means that some part of the code has created a + // `SyncTimelineEvent` containing an `m.room.encrypted` event + // without decrypting it. Possibly this means that encryption has not been + // configured. + // We treat it the same as any other message-like event. + Self::Message { + content: AnyMessageLikeEventContent::RoomEncrypted(content), + relations: ev.relations(), + } + } } Some(content) => Self::Message { content, relations: ev.relations() }, None => Self::RedactedMessage { event_type: ev.event_type() }, @@ -395,7 +416,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } }, - TimelineEventKind::UnableToDecrypt { content } => { + TimelineEventKind::UnableToDecrypt { content, .. } => { // TODO: Handle replacements if the replaced event is also UTD let raw_event = self.ctx.flow.raw_event(); let cause = UtdCause::determine(raw_event); From 0c812066532616d21e55b510a9e24bee9b408070 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 17 Oct 2024 21:34:42 +0100 Subject: [PATCH 348/979] refactor(timeline): retry_event_decryption: re-use utd cause Rather than calling `UtdCause::determine` again when an event is successfully decrypted on retry, re-use the cause we already determined. --- .../src/timeline/controller/mod.rs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 6dbab04a5db..3a37135059e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -989,8 +989,6 @@ impl TimelineController

{ decryptor: impl Decryptor, session_ids: Option>, ) { - use matrix_sdk::crypto::types::events::UtdCause; - use super::EncryptedMessage; let mut state = self.state.clone().write_owned().await; @@ -1038,16 +1036,17 @@ impl TimelineController

{ async move { let event_item = item.as_event()?; - let session_id = match event_item.content().as_unable_to_decrypt()? { - EncryptedMessage::MegolmV1AesSha2 { session_id, .. } - if should_retry(session_id) => - { - session_id - } - EncryptedMessage::MegolmV1AesSha2 { .. } - | EncryptedMessage::OlmV1Curve25519AesSha2 { .. } - | EncryptedMessage::Unknown => return None, - }; + let (session_id, utd_cause) = + match event_item.content().as_unable_to_decrypt()? { + EncryptedMessage::MegolmV1AesSha2 { session_id, cause, .. } + if should_retry(session_id) => + { + (session_id, cause) + } + EncryptedMessage::MegolmV1AesSha2 { .. } + | EncryptedMessage::OlmV1Curve25519AesSha2 { .. } + | EncryptedMessage::Unknown => return None, + }; tracing::Span::current().record("session_id", session_id); @@ -1069,11 +1068,9 @@ impl TimelineController

{ "Successfully decrypted event that previously failed to decrypt" ); - let cause = UtdCause::determine(Some(original_json)); - // Notify observers that we managed to eventually decrypt an event. if let Some(hook) = unable_to_decrypt_hook { - hook.on_late_decrypt(&remote_event.event_id, cause).await; + hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await; } Some(event) From c4f9c20115899a89a529c1a710678e8fc533d381 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 17 Oct 2024 21:42:02 +0100 Subject: [PATCH 349/979] feat(crypto): rename `UtdCause::Membership` Before we do any more work here, give this variant a better name Breaking-Change: `matrix_sdk_crypto::type::events::UtdCause::Membership` has been renamed to `...::SentBeforeWeJoined`. --- .../matrix-sdk-crypto/src/types/events/utd_cause.rs | 12 ++++++------ .../matrix-sdk-ui/src/timeline/tests/encryption.rs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index 1bd99fde29e..a519348f405 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -24,9 +24,9 @@ pub enum UtdCause { #[default] Unknown = 0, - /// This event was sent when we were not a member of the room (or invited), - /// so it is impossible to decrypt (without MSC3061). - Membership = 1, + /// We are missing the keys for this event, and the event was sent when we + /// were not a member of the room (or invited). + SentBeforeWeJoined = 1, // // TODO: Other causes for UTDs. For example, this message is device-historical, information // extracted from the WithheldCode in the MissingRoomKey object, or various types of Olm @@ -64,7 +64,7 @@ impl UtdCause { if let Ok(Some(unsigned)) = raw_event.get_field::("unsigned") { if let Membership::Leave = unsigned.membership { // We were not a member - this is the cause of the UTD - return UtdCause::Membership; + return UtdCause::SentBeforeWeJoined; } } } @@ -132,7 +132,7 @@ mod tests { // until we have MSC3061. assert_eq!( UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": "leave" } })))), - UtdCause::Membership + UtdCause::SentBeforeWeJoined ); } @@ -143,7 +143,7 @@ mod tests { UtdCause::determine(Some(&raw_event( json!({ "unsigned": { "io.element.msc4115.membership": "leave" } }) ))), - UtdCause::Membership + UtdCause::SentBeforeWeJoined ); } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index 4b0683529d8..0bf9d80d4ce 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -493,7 +493,7 @@ async fn test_utd_cause_for_nonmember_event_is_found() { TimelineItemContent::UnableToDecrypt(EncryptedMessage::MegolmV1AesSha2 { cause, .. }) = event.content() ); - assert_eq!(*cause, UtdCause::Membership); + assert_eq!(*cause, UtdCause::SentBeforeWeJoined); } #[async_test] @@ -516,7 +516,7 @@ async fn test_utd_cause_for_nonmember_event_is_found_unstable_prefix() { TimelineItemContent::UnableToDecrypt(EncryptedMessage::MegolmV1AesSha2 { cause, .. }) = event.content() ); - assert_eq!(*cause, UtdCause::Membership); + assert_eq!(*cause, UtdCause::SentBeforeWeJoined); } #[async_test] From 7cfcc8ecc116f117ad1e39b6b312413cf89ce21e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 17 Oct 2024 22:16:33 +0100 Subject: [PATCH 350/979] refactor(crypto): pass utd info into `UtdCause::determine` We'll need this for future changes --- .../src/types/events/utd_cause.rs | 74 ++++++++++++++++--- .../src/timeline/event_handler.rs | 4 +- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index a519348f405..ffb6813cfde 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -54,7 +54,10 @@ enum Membership { impl UtdCause { /// Decide the cause of this UTD, based on the evidence we have. - pub fn determine(raw_event: Option<&Raw>) -> Self { + pub fn determine( + raw_event: Option<&Raw>, + unable_to_decrypt_info: &UnableToDecryptInfo, + ) -> Self { // TODO: in future, use more information to give a richer answer. E.g. // is this event device-historical? Was the Olm communication disrupted? // Did the sender refuse to send the key because we're not verified? @@ -76,6 +79,7 @@ impl UtdCause { #[cfg(test)] mod tests { + use matrix_sdk_common::deserialized_responses::{UnableToDecryptInfo, UnableToDecryptReason}; use ruma::{events::AnySyncTimelineEvent, serde::Raw}; use serde_json::{json, value::to_raw_value}; @@ -85,13 +89,31 @@ mod tests { fn a_missing_raw_event_means_we_guess_unknown() { // When we don't provide any JSON to check for membership, then we guess the UTD // is unknown. - assert_eq!(UtdCause::determine(None), UtdCause::Unknown); + assert_eq!( + UtdCause::determine( + None, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession, + } + ), + UtdCause::Unknown + ); } #[test] fn if_there_is_no_membership_info_we_guess_unknown() { // If our JSON contains no membership info, then we guess the UTD is unknown. - assert_eq!(UtdCause::determine(Some(&raw_event(json!({})))), UtdCause::Unknown); + assert_eq!( + UtdCause::determine( + Some(&raw_event(json!({}))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::Unknown + ); } #[test] @@ -99,7 +121,13 @@ mod tests { // If our JSON contains a membership property but not the JSON we expected, then // we guess the UTD is unknown. assert_eq!( - UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": 3 } })))), + UtdCause::determine( + Some(&raw_event(json!({ "unsigned": { "membership": 3 } }))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), UtdCause::Unknown ); } @@ -109,9 +137,13 @@ mod tests { // If membership=invite then we expected to be sent the keys so the cause of the // UTD is unknown. assert_eq!( - UtdCause::determine(Some(&raw_event( - json!({ "unsigned": { "membership": "invite" } }), - ))), + UtdCause::determine( + Some(&raw_event(json!({ "unsigned": { "membership": "invite" } }),)), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), UtdCause::Unknown ); } @@ -121,7 +153,13 @@ mod tests { // If membership=join then we expected to be sent the keys so the cause of the // UTD is unknown. assert_eq!( - UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": "join" } })))), + UtdCause::determine( + Some(&raw_event(json!({ "unsigned": { "membership": "join" } }))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), UtdCause::Unknown ); } @@ -131,7 +169,13 @@ mod tests { // If membership=leave then we have an explanation for why we can't decrypt, // until we have MSC3061. assert_eq!( - UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": "leave" } })))), + UtdCause::determine( + Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), UtdCause::SentBeforeWeJoined ); } @@ -140,9 +184,15 @@ mod tests { fn if_unstable_prefix_membership_is_leave_we_guess_membership() { // Before MSC4115 is merged, we support the unstable prefix too. assert_eq!( - UtdCause::determine(Some(&raw_event( - json!({ "unsigned": { "io.element.msc4115.membership": "leave" } }) - ))), + UtdCause::determine( + Some(&raw_event( + json!({ "unsigned": { "io.element.msc4115.membership": "leave" } }) + )), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), UtdCause::SentBeforeWeJoined ); } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 210fec68e39..228eb96e606 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -416,10 +416,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } }, - TimelineEventKind::UnableToDecrypt { content, .. } => { + TimelineEventKind::UnableToDecrypt { content, unable_to_decrypt_info } => { // TODO: Handle replacements if the replaced event is also UTD let raw_event = self.ctx.flow.raw_event(); - let cause = UtdCause::determine(raw_event); + let cause = UtdCause::determine(raw_event, &unable_to_decrypt_info); self.add_item(TimelineItemContent::unable_to_decrypt(content, cause), None); // Let the hook know that we ran into an unable-to-decrypt that is added to the From 1368a8534c1be993c9846b9c86c700fd1b01fc4f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 17 Oct 2024 22:17:22 +0100 Subject: [PATCH 351/979] feat(crypto): Add more reason codes to `UtdCause` --- crates/matrix-sdk-crypto/src/error.rs | 2 + .../src/types/events/utd_cause.rs | 136 +++++++++++++++--- 2 files changed, 118 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 89fe5c5edaf..4400da9d696 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -120,6 +120,8 @@ pub enum MegolmError { /// An encrypted message wasn't decrypted, because the sender's /// cross-signing identity did not satisfy the requested /// [`crate::TrustRequirement`]. + /// + /// The nested value is the sender's current verification level. #[error("decryption failed because trust requirement not satisfied: {0}")] SenderIdentityNotTrusted(VerificationLevel), } diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index ffb6813cfde..1575b424c49 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use matrix_sdk_common::deserialized_responses::{ + UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, +}; use ruma::{events::AnySyncTimelineEvent, serde::Raw}; use serde::Deserialize; @@ -27,13 +30,23 @@ pub enum UtdCause { /// We are missing the keys for this event, and the event was sent when we /// were not a member of the room (or invited). SentBeforeWeJoined = 1, - // - // TODO: Other causes for UTDs. For example, this message is device-historical, information - // extracted from the WithheldCode in the MissingRoomKey object, or various types of Olm - // session problems. - // - // Note: This needs to be a simple enum so we can export it via FFI, so if more information - // needs to be provided, it should be through a separate type. + + /// The message was sent by a user identity we have not verified, but the + /// user was previously verified. + VerificationViolation = 2, + + /// The [`crate::TrustRequirement`] requires that the sending device be + /// signed by its owner, and it was not. + UnsignedDevice = 3, + + /// The [`crate::TrustRequirement`] requires that the sending device be + /// signed by its owner, and we were unable to securely find the device. + /// + /// This could be because the device has since been deleted, because we + /// haven't yet downloaded it from the server, or because the session + /// data was obtained from an insecure source (imported from a file, + /// obtained from a legacy (asymmetric) backup, unsafe key forward, etc.) + UnknownDevice = 4, } /// MSC4115 membership info in the unsigned area. @@ -59,27 +72,45 @@ impl UtdCause { unable_to_decrypt_info: &UnableToDecryptInfo, ) -> Self { // TODO: in future, use more information to give a richer answer. E.g. - // is this event device-historical? Was the Olm communication disrupted? - // Did the sender refuse to send the key because we're not verified? - - // Look in the unsigned area for a `membership` field. - if let Some(raw_event) = raw_event { - if let Ok(Some(unsigned)) = raw_event.get_field::("unsigned") { - if let Membership::Leave = unsigned.membership { - // We were not a member - this is the cause of the UTD - return UtdCause::SentBeforeWeJoined; + match unable_to_decrypt_info.reason { + UnableToDecryptReason::MissingMegolmSession + | UnableToDecryptReason::UnknownMegolmMessageIndex => { + // Look in the unsigned area for a `membership` field. + if let Some(raw_event) = raw_event { + if let Ok(Some(unsigned)) = + raw_event.get_field::("unsigned") + { + if let Membership::Leave = unsigned.membership { + // We were not a member - this is the cause of the UTD + return UtdCause::SentBeforeWeJoined; + } + } } + UtdCause::Unknown + } + + UnableToDecryptReason::SenderIdentityNotTrusted( + VerificationLevel::VerificationViolation, + ) => UtdCause::VerificationViolation, + + UnableToDecryptReason::SenderIdentityNotTrusted(VerificationLevel::UnsignedDevice) => { + UtdCause::UnsignedDevice } - } - // We can't find an explanation for this UTD - UtdCause::Unknown + UnableToDecryptReason::SenderIdentityNotTrusted(VerificationLevel::None(_)) => { + UtdCause::UnknownDevice + } + + _ => UtdCause::Unknown, + } } } #[cfg(test)] mod tests { - use matrix_sdk_common::deserialized_responses::{UnableToDecryptInfo, UnableToDecryptReason}; + use matrix_sdk_common::deserialized_responses::{ + DeviceLinkProblem, UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, + }; use ruma::{events::AnySyncTimelineEvent, serde::Raw}; use serde_json::{json, value::to_raw_value}; @@ -180,6 +211,23 @@ mod tests { ); } + #[test] + fn if_reason_is_not_missing_key_we_guess_unknown_even_if_membership_is_leave() { + // If the UnableToDecryptReason is other than MissingMegolmSession or + // UnknownMegolmMessageIndex, we do not know the reason for the failure + // even if membership=leave. + assert_eq!( + UtdCause::determine( + Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MalformedEncryptedEvent + } + ), + UtdCause::Unknown + ); + } + #[test] fn if_unstable_prefix_membership_is_leave_we_guess_membership() { // Before MSC4115 is merged, we support the unstable prefix too. @@ -197,6 +245,54 @@ mod tests { ); } + #[test] + fn verification_violation_is_passed_through() { + assert_eq!( + UtdCause::determine( + Some(&raw_event(json!({}))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::SenderIdentityNotTrusted( + VerificationLevel::VerificationViolation, + ) + } + ), + UtdCause::VerificationViolation + ); + } + + #[test] + fn unsigned_device_is_passed_through() { + assert_eq!( + UtdCause::determine( + Some(&raw_event(json!({}))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::SenderIdentityNotTrusted( + VerificationLevel::UnsignedDevice, + ) + } + ), + UtdCause::UnsignedDevice + ); + } + + #[test] + fn unknown_device_is_passed_through() { + assert_eq!( + UtdCause::determine( + Some(&raw_event(json!({}))), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::SenderIdentityNotTrusted( + VerificationLevel::None(DeviceLinkProblem::MissingDevice) + ) + } + ), + UtdCause::UnknownDevice + ); + } + fn raw_event(value: serde_json::Value) -> Raw { Raw::from_json(to_raw_value(&value).unwrap()) } From 3291a426d8585ffc5f31e122dad8e5ec1768ab97 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 22 Oct 2024 11:57:16 +0100 Subject: [PATCH 352/979] test(crypto): rename UtdCause tests with `test_` prefix --- .../src/types/events/utd_cause.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index 1575b424c49..5a260ad50d9 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -117,7 +117,7 @@ mod tests { use crate::types::events::UtdCause; #[test] - fn a_missing_raw_event_means_we_guess_unknown() { + fn test_a_missing_raw_event_means_we_guess_unknown() { // When we don't provide any JSON to check for membership, then we guess the UTD // is unknown. assert_eq!( @@ -133,7 +133,7 @@ mod tests { } #[test] - fn if_there_is_no_membership_info_we_guess_unknown() { + fn test_if_there_is_no_membership_info_we_guess_unknown() { // If our JSON contains no membership info, then we guess the UTD is unknown. assert_eq!( UtdCause::determine( @@ -148,7 +148,7 @@ mod tests { } #[test] - fn if_membership_info_cant_be_parsed_we_guess_unknown() { + fn test_if_membership_info_cant_be_parsed_we_guess_unknown() { // If our JSON contains a membership property but not the JSON we expected, then // we guess the UTD is unknown. assert_eq!( @@ -164,7 +164,7 @@ mod tests { } #[test] - fn if_membership_is_invite_we_guess_unknown() { + fn test_if_membership_is_invite_we_guess_unknown() { // If membership=invite then we expected to be sent the keys so the cause of the // UTD is unknown. assert_eq!( @@ -180,7 +180,7 @@ mod tests { } #[test] - fn if_membership_is_join_we_guess_unknown() { + fn test_if_membership_is_join_we_guess_unknown() { // If membership=join then we expected to be sent the keys so the cause of the // UTD is unknown. assert_eq!( @@ -196,7 +196,7 @@ mod tests { } #[test] - fn if_membership_is_leave_we_guess_membership() { + fn test_if_membership_is_leave_we_guess_membership() { // If membership=leave then we have an explanation for why we can't decrypt, // until we have MSC3061. assert_eq!( @@ -212,7 +212,7 @@ mod tests { } #[test] - fn if_reason_is_not_missing_key_we_guess_unknown_even_if_membership_is_leave() { + fn test_if_reason_is_not_missing_key_we_guess_unknown_even_if_membership_is_leave() { // If the UnableToDecryptReason is other than MissingMegolmSession or // UnknownMegolmMessageIndex, we do not know the reason for the failure // even if membership=leave. @@ -229,7 +229,7 @@ mod tests { } #[test] - fn if_unstable_prefix_membership_is_leave_we_guess_membership() { + fn test_if_unstable_prefix_membership_is_leave_we_guess_membership() { // Before MSC4115 is merged, we support the unstable prefix too. assert_eq!( UtdCause::determine( @@ -246,7 +246,7 @@ mod tests { } #[test] - fn verification_violation_is_passed_through() { + fn test_verification_violation_is_passed_through() { assert_eq!( UtdCause::determine( Some(&raw_event(json!({}))), @@ -262,7 +262,7 @@ mod tests { } #[test] - fn unsigned_device_is_passed_through() { + fn test_unsigned_device_is_passed_through() { assert_eq!( UtdCause::determine( Some(&raw_event(json!({}))), @@ -278,7 +278,7 @@ mod tests { } #[test] - fn unknown_device_is_passed_through() { + fn test_unknown_device_is_passed_through() { assert_eq!( UtdCause::determine( Some(&raw_event(json!({}))), From 31e9600078cbaa9c3119ebe79f4a8fd6860b9ce4 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 23 Oct 2024 12:48:55 +0200 Subject: [PATCH 353/979] feat(send_queue): Persist failed to send errors (#4137) Modify the SendQueue in order to persist the error that cause the event to fail to send as a `QueueWedgeError`. The `QueueWedgeError` is not a 1:1 mapping for all kinds of errors, but holds variant and information that the client can react to in order to propose "quick fixes"/solution before retrying to send. Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/3973 Also fixes https://github.com/element-hq/element-x-ios/issues/3287 because when a timeline reset occurs the fail to send reason is also lost. This PR starts with a refactoring commit https://github.com/matrix-org/matrix-rust-sdk/commit/e7696003e846b64c41761109f326fd37c4506040 to introduce the new `QueueWedgedError` and move the logic that was in the ffi layer to convert api errors to SendState error variant. This `QueueWedgedError` can be directly use in the `SendingFailed` variant and expose to ffi. Second commit https://github.com/matrix-org/matrix-rust-sdk/commit/109c1337465ba7965825fec858fd2a4b8b954611 adds the persistence, `QueuedEvent` now have an optional error field instead of a `is_weged` boolean. Same for LocalEchoContent::Event. Adds also Migration for sqlite and indexeddb Co-authored-by: Benjamin Bouvier Changelog: We now persist the error that caused an event to fail to send. The error `QueueWedgeError` contains info that client can use to try to resolve the problem when the error is not automatically retry-able. Some breaking changes occurred in the FFI layer for `timeline::EventSendState`, `SendingFailed` now directly contains the wedge reason enum; use it in place of the removed variant of `EventSendState`. --- bindings/matrix-sdk-ffi/src/error.rs | 79 +++++++++- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 78 ++-------- crates/matrix-sdk-base/src/lib.rs | 2 +- .../src/store/integration_tests.rs | 28 +++- .../matrix-sdk-base/src/store/memory_store.rs | 12 +- crates/matrix-sdk-base/src/store/mod.rs | 2 +- crates/matrix-sdk-base/src/store/traits.rs | 70 +++++++-- .../src/state_store/mod.rs | 81 +++++++++- .../007_a_send_queue_wedge_reason.sql | 5 + .../state_store/007_b_send_queue_clean.sql | 2 + crates/matrix-sdk-sqlite/src/state_store.rs | 142 ++++++++++++++++-- .../src/timeline/controller/mod.rs | 14 +- .../matrix-sdk-ui/src/timeline/tests/echo.rs | 15 +- .../tests/integration/timeline/queue.rs | 16 +- crates/matrix-sdk/src/error.rs | 7 +- crates/matrix-sdk/src/lib.rs | 2 +- crates/matrix-sdk/src/send_queue.rs | 52 +++++-- .../tests/integration/send_queue.rs | 4 +- 18 files changed, 460 insertions(+), 151 deletions(-) create mode 100644 crates/matrix-sdk-sqlite/migrations/state_store/007_a_send_queue_wedge_reason.sql create mode 100644 crates/matrix-sdk-sqlite/migrations/state_store/007_b_send_queue_clean.sql diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index cdfc07a690a..6262b765dc4 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -1,13 +1,13 @@ -use std::fmt::Display; +use std::{collections::HashMap, fmt, fmt::Display}; use matrix_sdk::{ encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError, - NotificationSettingsError as SdkNotificationSettingsError, StoreError, + NotificationSettingsError as SdkNotificationSettingsError, + QueueWedgeError as SdkQueueWedgeError, StoreError, }; use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline}; use uniffi::UnexpectedUniFFICallbackError; - #[derive(Debug, thiserror::Error)] pub enum ClientError { #[error("client error: {msg}")] @@ -146,6 +146,79 @@ impl From for ClientError { } } +/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple +/// String. +/// +/// Represent a failed to send unrecoverable error of an event sent via the +/// send_queue. It is a serializable representation of a client error, see +/// `From` implementation for more details. These errors can not be +/// automatically retried, but yet some manual action can be taken before retry +/// sending. If not the only solution is to delete the local event. +#[derive(Debug, Clone, uniffi::Enum)] +pub enum QueueWedgeError { + /// This error occurs when there are some insecure devices in the room, and + /// the current encryption setting prohibit sharing with them. + InsecureDevices { + /// The insecure devices as a Map of userID to deviceID. + user_device_map: HashMap>, + }, + + /// This error occurs when a previously verified user is not anymore, and + /// the current encryption setting prohibit sharing when it happens. + IdentityViolations { + /// The users that are expected to be verified but are not. + users: Vec, + }, + + /// It is required to set up cross-signing and properly erify the current + /// session before sending. + CrossVerificationRequired, + + /// Other errors. + GenericApiError { msg: String }, +} + +/// Simple display implementation that strips out userIds/DeviceIds to avoid +/// accidental logging. +impl Display for QueueWedgeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + QueueWedgeError::InsecureDevices { .. } => { + f.write_str("There are insecure devices in the room") + } + QueueWedgeError::IdentityViolations { .. } => { + f.write_str("Some users that were previously verified are not anymore") + } + QueueWedgeError::CrossVerificationRequired => { + f.write_str("Own verification is required") + } + QueueWedgeError::GenericApiError { msg } => f.write_str(msg), + } + } +} +impl From for QueueWedgeError { + fn from(value: SdkQueueWedgeError) -> Self { + match value { + SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices { + user_device_map: user_device_map + .iter() + .map(|(user_id, devices)| { + ( + user_id.to_string(), + devices.iter().map(|device_id| device_id.to_string()).collect(), + ) + }) + .collect(), + }, + SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations { + users: users.iter().map(ruma::OwnedUserId::to_string).collect(), + }, + SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired, + SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg }, + } + } +} + #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] pub enum RoomError { diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 15ac286a6d7..cbdd8ab21c9 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -82,6 +82,8 @@ mod content; pub use content::MessageContent; +use crate::error::QueueWedgeError; + #[derive(uniffi::Object)] #[repr(transparent)] pub struct Timeline { @@ -951,42 +953,12 @@ pub enum EventSendState { /// The local event has not been sent yet. NotSentYet, - /// One or more verified users in the room has an unsigned device. - /// - /// Happens only when the room key recipient strategy (as set by - /// [`ClientBuilder::room_key_recipient_strategy`]) has - /// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem) set. - VerifiedUserHasUnsignedDevice { - /// The unsigned devices belonging to verified users. A map from user ID - /// to a list of device IDs. - devices: HashMap>, - }, - - /// One or more verified users in the room has changed identity since they - /// were verified. - /// - /// Happens only when the room key recipient strategy (as set by - /// [`ClientBuilder::room_key_recipient_strategy`]) has - /// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem) - /// set, or when using [`CollectStrategy::IdentityBasedStrategy`]. - VerifiedUserChangedIdentity { - /// The users that were previously verified, but are no longer - users: Vec, - }, - - /// The user does not have cross-signing set up, but - /// [`CollectStrategy::IdentityBasedStrategy`] was used. - CrossSigningNotSetup, - - /// The current device is not verified, but - /// [`CollectStrategy::IdentityBasedStrategy`] was used. - SendingFromUnverifiedDevice, - /// The local event has been sent to the server, but unsuccessfully: The /// sending has failed. SendingFailed { - /// Stringified error message. - error: String, + /// The error reason, with information for the user. + error: QueueWedgeError, + /// Whether the error is considered recoverable or not. /// /// An error that's recoverable will disable the room's send queue, @@ -994,6 +966,7 @@ pub enum EventSendState { /// decides to cancel sending it. is_recoverable: bool, }, + /// The local event has been sent successfully to the server. Sent { event_id: String }, } @@ -1005,46 +978,17 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState { match value { NotSentYet => Self::NotSentYet, SendingFailed { error, is_recoverable } => { - event_send_state_from_sending_failed(error, *is_recoverable) + let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into(); + Self::SendingFailed { + is_recoverable: *is_recoverable, + error: as_queue_wedge_error.into(), + } } Sent { event_id } => Self::Sent { event_id: event_id.to_string() }, } } } -fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) -> EventSendState { - use matrix_sdk::crypto::{OlmError, SessionRecipientCollectionError::*}; - - match error { - // Special-case the SessionRecipientCollectionErrors, to pass the information they contain - // back to the application. - Error::OlmError(OlmError::SessionRecipientCollectionError(error)) => match error { - VerifiedUserHasUnsignedDevice(devices) => { - let devices = devices - .iter() - .map(|(user_id, devices)| { - ( - user_id.to_string(), - devices.iter().map(|device_id| device_id.to_string()).collect(), - ) - }) - .collect(); - EventSendState::VerifiedUserHasUnsignedDevice { devices } - } - - VerifiedUserChangedIdentity(bad_users) => EventSendState::VerifiedUserChangedIdentity { - users: bad_users.iter().map(|user_id| user_id.to_string()).collect(), - }, - - CrossSigningNotSetup => EventSendState::CrossSigningNotSetup, - - SendingFromUnverifiedDevice => EventSendState::SendingFromUnverifiedDevice, - }, - - _ => EventSendState::SendingFailed { error: error.to_string(), is_recoverable }, - } -} - /// Recommended decorations for decrypted messages, representing the message's /// authenticity properties. #[derive(uniffi::Enum, Clone)] diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 09cc6b36c2b..611305a59e6 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -61,7 +61,7 @@ pub use rooms::{ RoomStateFilter, }; pub use store::{ - ComposerDraft, ComposerDraftType, StateChanges, StateStore, StateStoreDataKey, + ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError, }; pub use utils::{ diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 7fd99b32a28..eccebdc9190 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -36,7 +36,10 @@ use serde_json::{json, value::Value as JsonValue}; use super::{DependentQueuedEventKind, DynStateStore, ServerCapabilities}; use crate::{ deserialized_responses::MemberEvent, - store::{traits::ChildTransactionId, Result, SerializableEventContent, StateStoreExt}, + store::{ + traits::ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, + StateStoreExt, + }, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue, }; @@ -1223,7 +1226,7 @@ impl StateStoreIntegrationTests for DynStateStore { assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), "msg0"); - assert!(!pending[0].is_wedged); + assert!(!pending[0].is_wedged()); } // Saving another three things should work. @@ -1249,12 +1252,18 @@ impl StateStoreIntegrationTests for DynStateStore { let deserialized = pending[i].event.deserialize().unwrap(); assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), format!("msg{i}")); - assert!(!pending[i].is_wedged); + assert!(!pending[i].is_wedged()); } // Marking an event as wedged works. let txn2 = &pending[2].transaction_id; - self.update_send_queue_event_status(room_id, txn2, true).await.unwrap(); + self.update_send_queue_event_status( + room_id, + txn2, + Some(QueueWedgeError::GenericApiError { msg: "Oops".to_owned() }), + ) + .await + .unwrap(); // And it is reflected. let pending = self.load_send_queue_events(room_id).await.unwrap(); @@ -1263,10 +1272,13 @@ impl StateStoreIntegrationTests for DynStateStore { assert_eq!(pending.len(), 4); assert_eq!(pending[0].transaction_id, txn0); assert_eq!(pending[2].transaction_id, *txn2); - assert!(pending[2].is_wedged); + assert!(pending[2].is_wedged()); + let error = pending[2].clone().error.unwrap(); + let generic_error = assert_matches!(error, QueueWedgeError::GenericApiError { msg } => msg); + assert_eq!(generic_error, "Oops"); for i in 0..4 { if i != 2 { - assert!(!pending[i].is_wedged); + assert!(!pending[i].is_wedged()); } } @@ -1288,7 +1300,7 @@ impl StateStoreIntegrationTests for DynStateStore { assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), "wow that's a cool test"); - assert!(!pending[2].is_wedged); + assert!(!pending[2].is_wedged()); for i in 0..4 { if i != 2 { @@ -1296,7 +1308,7 @@ impl StateStoreIntegrationTests for DynStateStore { assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), format!("msg{i}")); - assert!(!pending[i].is_wedged); + assert!(!pending[i].is_wedged()); } } } diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 995c7113870..c5d8c01384a 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -44,8 +44,8 @@ use super::{ StoreError, }; use crate::{ - deserialized_responses::RawAnySyncOrStrippedState, MinimalRoomMemberEvent, RoomMemberships, - StateStoreDataKey, StateStoreDataValue, + deserialized_responses::RawAnySyncOrStrippedState, store::QueueWedgeError, + MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue, }; /// In-memory, non-persistent implementation of the `StateStore`. @@ -815,7 +815,7 @@ impl StateStore for MemoryStore { .unwrap() .entry(room_id.to_owned()) .or_default() - .push(QueuedEvent { event, transaction_id, is_wedged: false }); + .push(QueuedEvent { event, transaction_id, error: None }); Ok(()) } @@ -835,7 +835,7 @@ impl StateStore for MemoryStore { .find(|item| item.transaction_id == transaction_id) { entry.event = content; - entry.is_wedged = false; + entry.error = None; Ok(true) } else { Ok(false) @@ -876,7 +876,7 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, transaction_id: &TransactionId, - wedged: bool, + error: Option, ) -> Result<(), Self::Error> { if let Some(entry) = self .send_queue_events @@ -887,7 +887,7 @@ impl StateStore for MemoryStore { .iter_mut() .find(|item| item.transaction_id == transaction_id) { - entry.is_wedged = wedged; + entry.error = error; } Ok(()) } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 471a17c6717..6c79893923c 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -75,7 +75,7 @@ pub use self::{ memory_store::MemoryStore, traits::{ ChildTransactionId, ComposerDraft, ComposerDraftType, DependentQueuedEvent, - DependentQueuedEventKind, DynStateStore, IntoStateStore, QueuedEvent, + DependentQueuedEventKind, DynStateStore, IntoStateStore, QueueWedgeError, QueuedEvent, SerializableEventContent, ServerCapabilities, StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt, }, diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index d96e3f24cb0..142b7584529 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -37,8 +37,8 @@ use ruma::{ }, serde::Raw, time::SystemTime, - EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, - TransactionId, UserId, + EventId, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, + OwnedUserId, RoomId, TransactionId, UserId, }; use serde::{Deserialize, Serialize}; @@ -392,12 +392,15 @@ pub trait StateStore: AsyncTraitDeps { room_id: &RoomId, ) -> Result, Self::Error>; - /// Updates the send queue wedged status for a given send queue event. + /// Updates the send queue error status (wedge) for a given send queue + /// event. + /// Set `error` to None if the problem has been resolved and the event was + /// finally sent. async fn update_send_queue_event_status( &self, room_id: &RoomId, transaction_id: &TransactionId, - wedged: bool, + error: Option, ) -> Result<(), Self::Error>; /// Loads all the rooms which have any pending events in their send queue. @@ -662,10 +665,10 @@ impl StateStore for EraseStateStoreError { &self, room_id: &RoomId, transaction_id: &TransactionId, - wedged: bool, + error: Option, ) -> Result<(), Self::Error> { self.0 - .update_send_queue_event_status(room_id, transaction_id, wedged) + .update_send_queue_event_status(room_id, transaction_id, error) .await .map_err(Into::into) } @@ -1153,10 +1156,55 @@ pub struct QueuedEvent { /// Unique transaction id for the queued event, acting as a key. pub transaction_id: OwnedTransactionId, - /// If the event couldn't be sent because of an API error, it's marked as - /// wedged, and won't ever be peeked for sending. The only option is to - /// remove it. - pub is_wedged: bool, + /// Set when the event couldn't be sent because of an unrecoverable API + /// error. `None` if the event is in queue for being sent. + pub error: Option, +} + +impl QueuedEvent { + /// True if the event couldn't be sent because of an unrecoverable API + /// error. See [`Self::error`] for more details on the reason. + pub fn is_wedged(&self) -> bool { + self.error.is_some() + } +} + +/// Represents a failed to send unrecoverable error of an event sent via the +/// send_queue. +/// +/// It is a serializable representation of a client error, see +/// `From` implementation for more details. These errors can not be +/// automatically retried, but yet some manual action can be taken before retry +/// sending. If not the only solution is to delete the local event. +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum QueueWedgeError { + /// This error occurs when there are some insecure devices in the room, and + /// the current encryption setting prohibits sharing with them. + #[error("There are insecure devices in the room")] + InsecureDevices { + /// The insecure devices as a Map of userID to deviceID. + user_device_map: BTreeMap>, + }, + + /// This error occurs when a previously verified user is not anymore, and + /// the current encryption setting prohibits sharing when it happens. + #[error("Some users that were previously verified are not anymore")] + IdentityViolations { + /// The users that are expected to be verified but are not. + users: Vec, + }, + + /// It is required to set up cross-signing and properly verify the current + /// session before sending. + #[error("Own verification is required")] + CrossVerificationRequired, + + /// Other errors. + #[error("Other unrecoverable error: {msg}")] + GenericApiError { + /// Description of the error. + msg: String, + }, } /// The specific user intent that characterizes a [`DependentQueuedEvent`]. @@ -1254,7 +1302,7 @@ impl fmt::Debug for QueuedEvent { // Hide the content from the debug log. f.debug_struct("QueuedEvent") .field("transaction_id", &self.transaction_id) - .field("is_wedged", &self.is_wedged) + .field("is_wedged", &self.is_wedged()) .finish_non_exhaustive() } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 9f4c705aec9..193fd4c7bbf 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -147,6 +147,7 @@ mod keys { } pub use keys::ALL_STORES; +use matrix_sdk_base::store::QueueWedgeError; /// Encrypt (if needs be) then JSON-serialize a value. fn serialize_value(store_cipher: Option<&StoreCipher>, event: &impl Serialize) -> Result { @@ -432,7 +433,12 @@ struct PersistedQueuedEvent { // All these fields are the same as in [`QueuedEvent`]. event: SerializableEventContent, transaction_id: OwnedTransactionId, - is_wedged: bool, + + // Deprecated (from old format), now replaced with error field. + // Kept here for migration + is_wedged: Option, + + pub error: Option, } // Small hack to have the following macro invocation act as the appropriate @@ -1325,7 +1331,8 @@ impl_state_store!({ room_id: room_id.to_owned(), event: content, transaction_id, - is_wedged: false, + is_wedged: None, + error: None, }); // Save the new vector into db. @@ -1363,7 +1370,8 @@ impl_state_store!({ // Modify the one event. if let Some(entry) = prev.iter_mut().find(|entry| entry.transaction_id == transaction_id) { entry.event = content; - entry.is_wedged = false; + entry.is_wedged = None; + entry.error = None; // Save the new vector into db. obj.put_key_val(&encoded_key, &self.serialize_value(&prev)?)?; @@ -1432,7 +1440,15 @@ impl_state_store!({ .map(|item| QueuedEvent { event: item.event, transaction_id: item.transaction_id, - is_wedged: item.is_wedged, + error: match item.is_wedged { + Some(true) => { + // migrate a generic error + Some(QueueWedgeError::GenericApiError { + msg: "local echo failed to send in a previous session".into(), + }) + } + _ => item.error, + }, }) .collect()) } @@ -1441,7 +1457,7 @@ impl_state_store!({ &self, room_id: &RoomId, transaction_id: &TransactionId, - wedged: bool, + error: Option, ) -> Result<()> { let encoded_key = self.encode_key(keys::ROOM_SEND_QUEUE, room_id); @@ -1456,7 +1472,8 @@ impl_state_store!({ if let Some(queued_event) = prev.iter_mut().find(|item| item.transaction_id == transaction_id) { - queued_event.is_wedged = wedged; + queued_event.is_wedged = None; + queued_event.error = error; obj.put_key_val(&encoded_key, &self.serialize_value(&prev)?)?; } } @@ -1644,6 +1661,58 @@ impl From<&StrippedRoomMemberEvent> for RoomMember { } } +#[cfg(test)] +mod migration_tests { + use matrix_sdk_base::store::SerializableEventContent; + use ruma::{ + events::room::message::RoomMessageEventContent, room_id, OwnedRoomId, OwnedTransactionId, + TransactionId, + }; + use serde::{Deserialize, Serialize}; + + use crate::state_store::PersistedQueuedEvent; + + #[derive(Serialize, Deserialize)] + struct OldPersistedQueuedEvent { + /// In which room is this event going to be sent. + pub room_id: OwnedRoomId, + + // All these fields are the same as in [`QueuedEvent`]. + event: SerializableEventContent, + transaction_id: OwnedTransactionId, + + is_wedged: bool, + } + + // We now persist an error when an event failed to send instead of just a + // boolean. To support that, `PersistedQueueEvent` changed a bool to + // Option, ensures that this work properly. + #[test] + fn test_migrating_persisted_queue_event_serialization() { + let room_a_id = room_id!("!room_a:dummy.local"); + let transaction_id = TransactionId::new(); + let content = + SerializableEventContent::new(&RoomMessageEventContent::text_plain("Hello").into()) + .unwrap(); + + let old_persisted_queue_event = OldPersistedQueuedEvent { + room_id: room_a_id.to_owned(), + event: content, + transaction_id, + is_wedged: true, + }; + + let serialized_persisted_event = serde_json::to_vec(&old_persisted_queue_event).unwrap(); + + // Load it with the new version. + let new_persisted: PersistedQueuedEvent = + serde_json::from_slice(&serialized_persisted_event).unwrap(); + + assert_eq!(new_persisted.is_wedged, Some(true)); + assert!(new_persisted.error.is_none()); + } +} + #[cfg(all(test, target_arch = "wasm32"))] mod tests { #[cfg(target_arch = "wasm32")] diff --git a/crates/matrix-sdk-sqlite/migrations/state_store/007_a_send_queue_wedge_reason.sql b/crates/matrix-sdk-sqlite/migrations/state_store/007_a_send_queue_wedge_reason.sql new file mode 100644 index 00000000000..31bd19b384a --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/state_store/007_a_send_queue_wedge_reason.sql @@ -0,0 +1,5 @@ +-- New send queue events, now persists the type of error causing it to be wedged +ALTER TABLE "send_queue_events" + -- The serialized json (bytes) representing the error. Used as a value, thus encrypted/decrypted. + -- NULLABLE field (default NULL) + ADD COLUMN "wedge_reason" BLOB; diff --git a/crates/matrix-sdk-sqlite/migrations/state_store/007_b_send_queue_clean.sql b/crates/matrix-sdk-sqlite/migrations/state_store/007_b_send_queue_clean.sql new file mode 100644 index 00000000000..293935f4efe --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/state_store/007_b_send_queue_clean.sql @@ -0,0 +1,2 @@ +ALTER TABLE "send_queue_events" + DROP COLUMN "wedged"; diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 21682db911b..53cd1bd7a00 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -12,7 +12,7 @@ use matrix_sdk_base::{ deserialized_responses::{RawAnySyncOrStrippedState, SyncOrStrippedState}, store::{ migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedEvent, - DependentQueuedEventKind, QueuedEvent, SerializableEventContent, + DependentQueuedEventKind, QueueWedgeError, QueuedEvent, SerializableEventContent, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, @@ -68,7 +68,7 @@ mod keys { /// This is used to figure whether the sqlite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in /// the [`SqliteStateStore::run_migrations`] function.. -const DATABASE_VERSION: u8 = 7; +const DATABASE_VERSION: u8 = 8; /// A sqlite based cryptostore. #[derive(Clone)] @@ -261,6 +261,41 @@ impl SqliteStateStore { .await?; } + if from < 8 && to >= 8 { + // Replace all existing wedged events with a generic error. + let error = QueueWedgeError::GenericApiError { + msg: "local echo failed to send in a previous session".into(), + }; + let default_err = self.serialize_value(&error)?; + + conn.with_transaction(move |txn| { + // Update send queue table to persist the wedge reason if any. + txn.execute_batch(include_str!("../migrations/state_store/007_a_send_queue_wedge_reason.sql"))?; + + // Migrate the data, add a generic error for currently wedged events + + for wedged_entries in txn + .prepare("SELECT room_id, transaction_id FROM send_queue_events WHERE wedged = 1")? + .query_map((), |row| { + Ok( + (row.get::<_, Vec>(0)?,row.get::<_, String>(1)?) + ) + })? { + + let (room_id, transaction_id) = wedged_entries?; + + txn.prepare_cached("UPDATE send_queue_events SET wedge_reason = ? WHERE room_id = ? AND transaction_id = ?")? + .execute((default_err.clone(), room_id, transaction_id))?; + } + + + // Clean up the table now that data is migrated + txn.execute_batch(include_str!("../migrations/state_store/007_b_send_queue_clean.sql"))?; + + txn.set_db_version(8) + }) + .await?; + } Ok(()) } @@ -1653,7 +1688,7 @@ impl StateStore for SqliteStateStore { self.acquire() .await? .with_transaction(move |txn| { - txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content, wedged) VALUES (?, ?, ?, ?, false)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content))?; + txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content) VALUES (?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content))?; Ok(()) }) .await @@ -1675,7 +1710,7 @@ impl StateStore for SqliteStateStore { let num_updated = self.acquire() .await? .with_transaction(move |txn| { - txn.prepare_cached("UPDATE send_queue_events SET wedged = false, content = ? WHERE room_id = ? AND transaction_id = ?")?.execute((content, room_id, transaction_id)) + txn.prepare_cached("UPDATE send_queue_events SET wedge_reason = NULL, content = ? WHERE room_id = ? AND transaction_id = ?")?.execute((content, room_id, transaction_id)) }) .await?; @@ -1715,11 +1750,11 @@ impl StateStore for SqliteStateStore { // Note: ROWID is always present and is an auto-incremented integer counter. We // want to maintain the insertion order, so we can sort using it. // Note 2: transaction_id is not encoded, see why in `save_send_queue_event`. - let res: Vec<(String, Vec, bool)> = self + let res: Vec<(String, Vec, Option>)> = self .acquire() .await? .prepare( - "SELECT transaction_id, content, wedged FROM send_queue_events WHERE room_id = ? ORDER BY ROWID", + "SELECT transaction_id, content, wedge_reason FROM send_queue_events WHERE room_id = ? ORDER BY ROWID", |mut stmt| { stmt.query((room_id,))? .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))) @@ -1733,7 +1768,7 @@ impl StateStore for SqliteStateStore { queued_events.push(QueuedEvent { transaction_id: entry.0.into(), event: self.deserialize_json(&entry.1)?, - is_wedged: entry.2, + error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?, }); } @@ -1744,17 +1779,20 @@ impl StateStore for SqliteStateStore { &self, room_id: &RoomId, transaction_id: &TransactionId, - wedged: bool, + error: Option, ) -> Result<(), Self::Error> { let room_id = self.encode_key(keys::SEND_QUEUE, room_id); // See comment in `save_send_queue_event`. let transaction_id = transaction_id.to_string(); + // Serialize the error to json bytes (encrypted if option is enabled) if set. + let error_value = error.map(|e| self.serialize_value(&e)).transpose()?; + self.acquire() .await? .with_transaction(move |txn| { - txn.prepare_cached("UPDATE send_queue_events SET wedged = ? WHERE room_id = ? AND transaction_id = ?")?.execute((wedged, room_id, transaction_id))?; + txn.prepare_cached("UPDATE send_queue_events SET wedge_reason = ? WHERE room_id = ? AND transaction_id = ?")?.execute((error_value, room_id, transaction_id))?; Ok(()) }) .await @@ -1961,12 +1999,21 @@ mod migration_tests { }, }; - use matrix_sdk_base::{sync::UnreadNotificationsCount, RoomState, StateStore}; + use assert_matches::assert_matches; + use matrix_sdk_base::{ + store::{QueueWedgeError, SerializableEventContent}, + sync::UnreadNotificationsCount, + RoomState, StateStore, + }; use matrix_sdk_test::async_test; use once_cell::sync::Lazy; use ruma::{ - events::{room::create::RoomCreateEventContent, StateEventType}, - room_id, server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, + events::{ + room::{create::RoomCreateEventContent, message::RoomMessageEventContent}, + StateEventType, + }, + room_id, server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, RoomId, TransactionId, + UserId, }; use rusqlite::Transaction; use serde_json::json; @@ -2213,4 +2260,75 @@ mod migration_tests { assert_eq!(room_c.name(), None); assert_eq!(room_c.creator(), Some(room_c_create_sender)); } + + #[async_test] + pub async fn test_migrating_v7_to_v8() { + let path = new_path(); + + let room_a_id = room_id!("!room_a:dummy.local"); + let wedged_event_transaction_id = TransactionId::new(); + let local_event_transaction_id = TransactionId::new(); + + // Create and populate db. + { + let db = create_fake_db(&path, 7).await.unwrap(); + let conn = db.pool.get().await.unwrap(); + + let wedge_tx = wedged_event_transaction_id.clone(); + let local_tx = local_event_transaction_id.clone(); + + conn.with_transaction(move |txn| { + add_send_queue_event_v7(&db, txn, &wedge_tx, room_a_id, true)?; + add_send_queue_event_v7(&db, txn, &local_tx, room_a_id, false)?; + + Result::<_, Error>::Ok(()) + }) + .await + .unwrap(); + } + + // This transparently migrates to the latest version. + let store = SqliteStateStore::open(path, Some(SECRET)).await.unwrap(); + let queued_event = store.load_send_queue_events(room_a_id).await.unwrap(); + + assert_eq!(queued_event.len(), 2); + + let migrated_wedged = + queued_event.iter().find(|e| e.transaction_id == wedged_event_transaction_id).unwrap(); + + assert!(migrated_wedged.is_wedged()); + assert_matches!( + migrated_wedged.error.clone(), + Some(QueueWedgeError::GenericApiError { .. }) + ); + + let migrated_ok = queued_event + .iter() + .find(|e| e.transaction_id == local_event_transaction_id.clone()) + .unwrap(); + + assert!(!migrated_ok.is_wedged()); + assert!(migrated_ok.error.is_none()); + } + + fn add_send_queue_event_v7( + this: &SqliteStateStore, + txn: &Transaction<'_>, + transaction_id: &TransactionId, + room_id: &RoomId, + is_wedged: bool, + ) -> Result<(), Error> { + let content = + SerializableEventContent::new(&RoomMessageEventContent::text_plain("Hello").into())?; + + let room_id_key = this.encode_key(keys::SEND_QUEUE, room_id); + let room_id_value = this.serialize_value(&room_id.to_owned())?; + + let content = this.serialize_json(&content)?; + + txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content, wedged) VALUES (?, ?, ?, ?, ?)")? + .execute((room_id_key, room_id_value, transaction_id.to_string(), content, is_wedged))?; + + Ok(()) + } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 3a37135059e..cd1c39b14d5 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1238,7 +1238,7 @@ impl TimelineController

{ /// Handle a room send update that's a new local echo. pub(crate) async fn handle_local_echo(&self, echo: LocalEcho) { match echo.content { - LocalEchoContent::Event { serialized_event, send_handle, is_wedged } => { + LocalEchoContent::Event { serialized_event, send_handle, send_error } => { let content = match serialized_event.deserialize() { Ok(d) => d, Err(err) => { @@ -1254,15 +1254,11 @@ impl TimelineController

{ ) .await; - if is_wedged { + if let Some(send_error) = send_error { self.update_event_send_state( &echo.transaction_id, EventSendState::SendingFailed { - // Put a dummy error in this case, since we're not persisting the errors - // that occurred in previous sessions. - error: Arc::new(matrix_sdk::Error::UnknownError(Box::new( - MissingLocalEchoFailError, - ))), + error: Arc::new(matrix_sdk::Error::SendQueueWedgeError(send_error)), is_recoverable: false, }, ) @@ -1519,10 +1515,6 @@ impl TimelineController { } } -#[derive(Debug, Error)] -#[error("local echo failed to send in a previous session")] -struct MissingLocalEchoFailError; - #[derive(Debug, Default)] pub(super) struct HandleManyEventsResult { /// The number of items that were added to the timeline. diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index fea31476c78..a5475b6eb06 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{io, sync::Arc}; +use std::sync::Arc; use assert_matches::assert_matches; use eyeball_im::VectorDiff; use matrix_sdk::{ assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate, - test_utils::events::EventFactory, Error, + test_utils::events::EventFactory, }; +use matrix_sdk_base::store::QueueWedgeError; use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::{ event_id, @@ -66,15 +67,15 @@ async fn test_remote_echo_full_trip() { // Scenario 2: The local event has not been sent to the server successfully, it // has failed. In this case, there is no event ID. { - let some_io_error = Error::Io(io::Error::new(io::ErrorKind::Other, "this is a test")); + let error = + Arc::new(matrix_sdk::Error::SendQueueWedgeError(QueueWedgeError::GenericApiError { + msg: "this is a test".to_owned(), + })); timeline .controller .update_event_send_state( &txn_id, - EventSendState::SendingFailed { - error: Arc::new(some_io_error), - is_recoverable: true, - }, + EventSendState::SendingFailed { error, is_recoverable: true }, ) .await; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs index ba29da35bb5..edc522c5ab0 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs @@ -18,7 +18,8 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; -use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Error}; +use matrix_sdk_base::store::QueueWedgeError; use matrix_sdk_test::{ async_test, mocks::mock_encryption_state, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, ALICE, @@ -276,7 +277,7 @@ async fn test_reloaded_failed_local_echoes_are_marked_as_failed() { // The error is not recoverable. assert!(!is_recoverable); - // And it's properly pattern-matched. + // And it's properly pattern-matched as an HTTP error. assert_matches!( error.as_client_api_error().unwrap().error_kind(), Some(ruma::api::client::error::ErrorKind::TooLarge) @@ -296,8 +297,15 @@ async fn test_reloaded_failed_local_echoes_are_marked_as_failed() { // Same recoverable status as above. assert!(!is_recoverable); - // But the error details have been lost. - assert!(error.as_client_api_error().is_none()); + // It was persisted and it can be matched as a string now. + let msg = assert_matches!( + &**error, + Error::SendQueueWedgeError(QueueWedgeError::GenericApiError { msg }) => msg + ); + assert_eq!( + msg, + "the server returned an error: [413 / M_TOO_LARGE] Sounds like you have a lot to say!" + ); } #[async_test] diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index cb9b2789b9c..86c31496f99 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -25,7 +25,8 @@ use matrix_sdk_base::crypto::{ CryptoStoreError, DecryptorError, KeyExportError, MegolmError, OlmError, }; use matrix_sdk_base::{ - event_cache_store::EventCacheStoreError, Error as SdkBaseError, RoomState, StoreError, + event_cache_store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState, + StoreError, }; use reqwest::Error as ReqwestError; use ruma::{ @@ -371,6 +372,10 @@ pub enum Error { #[error(transparent)] EventCache(#[from] EventCacheError), + /// An item has been wedged in the send queue. + #[error(transparent)] + SendQueueWedgeError(#[from] QueueWedgeError), + /// Backups are not enabled #[error("backups are not enabled")] BackupNotEnabled, diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 0ad8d374ee5..758f25d43be 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -25,7 +25,7 @@ pub use matrix_sdk_base::crypto; pub use matrix_sdk_base::{ deserialized_responses, store::{DynStateStore, MemoryStore, StateStoreExt}, - ComposerDraft, ComposerDraftType, DisplayName, Room as BaseRoom, + ComposerDraft, ComposerDraftType, DisplayName, QueueWedgeError, Room as BaseRoom, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, RoomMember as BaseRoomMember, RoomMemberships, RoomState, SessionMeta, StateChanges, StateStore, StoreError, }; diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 82753295ae5..9e0b14aeceb 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -53,8 +53,8 @@ use std::{ use matrix_sdk_base::{ store::{ - ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueuedEvent, - SerializableEventContent, + ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, + QueuedEvent, SerializableEventContent, }, RoomState, StoreError, }; @@ -70,6 +70,8 @@ use ruma::{ use tokio::sync::{broadcast, Notify, RwLock}; use tracing::{debug, error, info, instrument, trace, warn}; +#[cfg(feature = "e2e-encryption")] +use crate::crypto::{OlmError, SessionRecipientCollectionError}; use crate::{ client::WeakClient, config::RequestConfig, @@ -350,7 +352,7 @@ impl RoomSendQueue { room: self.clone(), transaction_id: transaction_id.clone(), }, - is_wedged: false, + send_error: None, }, })); @@ -515,8 +517,15 @@ impl RoomSendQueue { warn!(txn_id = %queued_event.transaction_id, error = ?err, "Unrecoverable error when sending event: {err}"); // Mark the event as wedged, so it's not picked at any future point. - if let Err(err) = queue.mark_as_wedged(&queued_event.transaction_id).await { - warn!("unable to mark event as wedged: {err}"); + + if let Err(storage_error) = queue + .mark_as_wedged( + &queued_event.transaction_id, + QueueWedgeError::from(&err), + ) + .await + { + warn!("unable to mark event as wedged: {storage_error}"); } } @@ -577,6 +586,28 @@ impl RoomSendQueue { } } +impl From<&crate::Error> for QueueWedgeError { + fn from(value: &crate::Error) -> Self { + match value { + #[cfg(feature = "e2e-encryption")] + crate::Error::OlmError(OlmError::SessionRecipientCollectionError(error)) => match error + { + SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice(user_map) => { + QueueWedgeError::InsecureDevices { user_device_map: user_map.clone() } + } + SessionRecipientCollectionError::VerifiedUserChangedIdentity(users) => { + QueueWedgeError::IdentityViolations { users: users.clone() } + } + SessionRecipientCollectionError::CrossSigningNotSetup + | SessionRecipientCollectionError::SendingFromUnverifiedDevice => { + QueueWedgeError::CrossVerificationRequired + } + }, + _ => QueueWedgeError::GenericApiError { msg: value.to_string() }, + } + } +} + struct RoomSendQueueInner { /// The room which this send queue relates to. room: WeakRoom, @@ -658,7 +689,7 @@ impl QueueStorage { let queued_events = self.client()?.store().load_send_queue_events(&self.room_id).await?; - if let Some(event) = queued_events.iter().find(|queued| !queued.is_wedged) { + if let Some(event) = queued_events.iter().find(|queued| !queued.is_wedged()) { being_sent.insert(event.transaction_id.clone()); Ok(Some(event.clone())) @@ -680,6 +711,7 @@ impl QueueStorage { async fn mark_as_wedged( &self, transaction_id: &TransactionId, + reason: QueueWedgeError, ) -> Result<(), RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; @@ -688,7 +720,7 @@ impl QueueStorage { Ok(self .client()? .store() - .update_send_queue_event_status(&self.room_id, transaction_id, true) + .update_send_queue_event_status(&self.room_id, transaction_id, Some(reason)) .await?) } @@ -701,7 +733,7 @@ impl QueueStorage { Ok(self .client()? .store() - .update_send_queue_event_status(&self.room_id, transaction_id, false) + .update_send_queue_event_status(&self.room_id, transaction_id, None) .await?) } @@ -855,7 +887,7 @@ impl QueueStorage { room: room.clone(), transaction_id: queued.transaction_id, }, - is_wedged: queued.is_wedged, + send_error: queued.error, }, } }); @@ -1136,7 +1168,7 @@ pub enum LocalEchoContent { send_handle: SendHandle, /// Whether trying to send this local echo failed in the past with an /// unrecoverable error (see [`SendQueueRoomError::is_recoverable`]). - is_wedged: bool, + send_error: Option, }, /// A local echo has been reacted to. diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 8e6c6d1c29b..c88ea5c6934 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -77,7 +77,7 @@ macro_rules! assert_update { serialized_event, send_handle, // New local echoes should always start as not wedged. - is_wedged: false, + send_error: None, }, transaction_id: txn, }))) = timeout(Duration::from_secs(1), $watch.recv()).await @@ -1056,7 +1056,7 @@ async fn test_edit_with_poll_start() { content: LocalEchoContent::Event { serialized_event, // New local echoes should always start as not wedged. - is_wedged: false, + send_error: None, .. }, transaction_id: txn1, From ca1d82978843c827f26c0356df065e412439d309 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 24 Oct 2024 10:46:38 +0300 Subject: [PATCH 354/979] sliding_sync(state): use `*` for `m.call.member` when requesting state through sliding sync - introduced in https://github.com/matrix-org/matrix-rust-sdk/pull/4159 with an empty string - call members use custom `state_key`s and as such not specifying the sentinel won't match them and state won't be returned --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 2 +- crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 261cced7c14..0fd2ba165f3 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -87,7 +87,7 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[ (StateEventType::RoomTopic, ""), (StateEventType::RoomCanonicalAlias, ""), (StateEventType::RoomPowerLevels, ""), - (StateEventType::CallMember, ""), + (StateEventType::CallMember, "*"), ]; /// The default `required_state` constant value for sliding sync room 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 3fd0c52f91e..f586c528927 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -330,7 +330,7 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.topic", ""], ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], - ["org.matrix.msc3401.call.member", ""], + ["org.matrix.msc3401.call.member", "*"], ], "include_heroes": true, "filters": { @@ -2099,7 +2099,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["m.room.topic", ""], ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], - ["org.matrix.msc3401.call.member", ""], + ["org.matrix.msc3401.call.member", "*"], ["m.room.create", ""], ["m.room.pinned_events", ""], ], @@ -2137,7 +2137,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["m.room.topic", ""], ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], - ["org.matrix.msc3401.call.member", ""], + ["org.matrix.msc3401.call.member", "*"], ["m.room.create", ""], ["m.room.pinned_events", ""], ], From 1a3c5045dd9320e5f03718767c8f026e8577518a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 23 Oct 2024 13:50:34 +0200 Subject: [PATCH 355/979] chore(room): add copyright notice to sdk/room/mod --- crates/matrix-sdk/src/room/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 1fa314d473c..1f237d6d1e1 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1,3 +1,17 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + //! High-level room API use std::{ From f8c23d8aa0324429632e9aff19b12d7a26d2f27d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 23 Oct 2024 16:38:17 +0200 Subject: [PATCH 356/979] feat(media): add support for async uploads Changelog: Support for preallocated media content URI has been added in `Media::create_content_uri()`, and uploading the content for such a preallocated URI is possible with `Media::upload_preallocated()`. --- crates/matrix-sdk/src/error.rs | 6 +- crates/matrix-sdk/src/media.rs | 122 ++++++++++++++++++- crates/matrix-sdk/tests/integration/media.rs | 50 +++++++- 3 files changed, 172 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 86c31496f99..fb08d32b4ce 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -45,7 +45,7 @@ use serde_json::Error as JsonError; use thiserror::Error; use url::ParseError as UrlParseError; -use crate::{event_cache::EventCacheError, store_locks::LockStoreError}; +use crate::{event_cache::EventCacheError, media::MediaError, store_locks::LockStoreError}; /// Result type of the matrix-sdk. pub type Result = std::result::Result; @@ -379,6 +379,10 @@ pub enum Error { /// Backups are not enabled #[error("backups are not enabled")] BackupNotEnabled, + + /// An error happened during handling of a media subrequest. + #[error(transparent)] + Media(#[from] MediaError), } #[rustfmt::skip] // stop rustfmt breaking the `` in docs across multiple lines diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 02fd602f0da..c4553dba731 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -27,19 +27,21 @@ pub use matrix_sdk_base::media::*; use mime::Mime; use ruma::{ api::{ - client::{authenticated_media, media}, + client::{authenticated_media, error::ErrorKind, media}, MatrixVersion, }, assign, events::room::{MediaSource, ThumbnailInfo}, - MxcUri, + MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, }; #[cfg(not(target_arch = "wasm32"))] use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir}; #[cfg(not(target_arch = "wasm32"))] use tokio::{fs::File as TokioFile, io::AsyncWriteExt}; -use crate::{attachment::Thumbnail, futures::SendRequest, Client, Result, TransmissionProgress}; +use crate::{ + attachment::Thumbnail, futures::SendRequest, Client, Error, Result, TransmissionProgress, +}; /// A conservative upload speed of 1Mbps const DEFAULT_UPLOAD_SPEED: u64 = 125_000; @@ -105,6 +107,28 @@ impl fmt::Display for PersistError { } } +/// A preallocated MXC URI created by [`Media::create_content_uri()`], and +/// to be used with [`Media::upload_preallocated()`]. +#[derive(Debug)] +pub struct PreallocatedMxcUri { + /// The URI for the media URI. + pub uri: OwnedMxcUri, + /// The expiration date for the media URI. + expire_date: Option, +} + +/// An error that happened in the realm of media. +#[derive(Debug, thiserror::Error)] +pub enum MediaError { + /// A preallocated MXC URI has expired. + #[error("a preallocated MXC URI has expired")] + ExpiredPreallocatedMxcUri, + + /// Preallocated media already had content, cannot overwrite. + #[error("preallocated media already had content, cannot overwrite")] + CannotOverwriteMedia, +} + /// `IntoFuture` returned by [`Media::upload`]. pub type SendUploadRequest = SendRequest; @@ -154,6 +178,96 @@ impl Media { self.client.send(request, Some(request_config)) } + /// Preallocates an MXC URI for a media that will be uploaded soon. + /// + /// This preallocates an URI *before* any content is uploaded to the server. + /// The resulting preallocated MXC URI can then be consumed with + /// [`Media::upload_preallocated`]. + /// + /// # Examples + /// + /// ```no_run + /// # use std::fs; + /// # use matrix_sdk::{Client, ruma::room_id}; + /// # use url::Url; + /// # use mime; + /// # async { + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let mut client = Client::new(homeserver).await?; + /// + /// let preallocated = client.media().create_content_uri().await?; + /// println!("Cat URI: {}", preallocated.uri); + /// + /// let image = fs::read("/home/example/my-cat.jpg")?; + /// client + /// .media() + /// .upload_preallocated(preallocated, &mime::IMAGE_JPEG, image) + /// .await?; + /// + /// # anyhow::Ok(()) }; + /// ``` + pub async fn create_content_uri(&self) -> Result { + // Note: this request doesn't have any parameters. + let request = media::create_mxc_uri::v1::Request::default(); + + let response = self.client.send(request, None).await?; + + Ok(PreallocatedMxcUri { + uri: response.content_uri, + expire_date: response.unused_expires_at, + }) + } + + /// Fills the content of a preallocated MXC URI with the given content type + /// and data. + /// + /// The URI must have been preallocated with [`Self::create_content_uri`]. + /// See this method's documentation for a full example. + pub async fn upload_preallocated( + &self, + uri: PreallocatedMxcUri, + content_type: &Mime, + data: Vec, + ) -> Result<()> { + // Do a best-effort at reporting an expired MXC URI here; otherwise the server + // may complain about it later. + if let Some(expire_date) = uri.expire_date { + if MilliSecondsSinceUnixEpoch::now() >= expire_date { + return Err(Error::Media(MediaError::ExpiredPreallocatedMxcUri)); + } + } + + let timeout = std::cmp::max( + Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED), + MIN_UPLOAD_REQUEST_TIMEOUT, + ); + + let request = assign!(media::create_content_async::v3::Request::from_url(&uri.uri, data)?, { + content_type: Some(content_type.as_ref().to_owned()), + }); + + let request_config = self.client.request_config().timeout(timeout); + + if let Err(err) = self.client.send(request, Some(request_config)).await { + match err.client_api_error_kind() { + Some(ErrorKind::CannotOverwriteMedia) => { + Err(Error::Media(MediaError::CannotOverwriteMedia)) + } + + // Unfortunately, the spec says a server will return 404 for either an expired MXC + // ID or a non-existing MXC ID. Do a best-effort guess to recognize an expired MXC + // ID based on the error string, which will work with Synapse (as of 2024-10-23). + Some(ErrorKind::Unknown) if err.to_string().contains("expired") => { + Err(Error::Media(MediaError::ExpiredPreallocatedMxcUri)) + } + + _ => Err(err.into()), + } + } else { + Ok(()) + } + } + /// Gets a media file by copying it to a temporary location on disk. /// /// The file won't be encrypted even if it is encrypted on the server. @@ -506,7 +620,7 @@ impl Media { self.upload(content_type, data) .with_send_progress_observable(send_progress) .await - .map_err(crate::Error::from) + .map_err(Error::from) }; let ((thumbnail_source, thumbnail_info), response) = diff --git a/crates/matrix-sdk/tests/integration/media.rs b/crates/matrix-sdk/tests/integration/media.rs index ae35fe5a41b..70ac2eac68d 100644 --- a/crates/matrix-sdk/tests/integration/media.rs +++ b/crates/matrix-sdk/tests/integration/media.rs @@ -10,7 +10,7 @@ use ruma::{ api::client::media::get_content_thumbnail::v3::Method, assign, device_id, events::room::{message::ImageMessageEventContent, ImageInfo, MediaSource}, - mxc_uri, uint, user_id, + mxc_uri, owned_mxc_uri, uint, user_id, }; use serde_json::json; use wiremock::{ @@ -411,3 +411,51 @@ async fn test_get_media_file_with_auth_matrix_stable_feature() { }; client.media().get_thumbnail(&event_content, settings, true).await.unwrap(); } + +#[async_test] +async fn test_async_media_upload() { + let (client, server) = logged_in_client_with_server().await; + + client.reset_server_capabilities().await.unwrap(); + + // Declare Matrix version v1.7. + Mock::given(method("GET")) + .and(path("/_matrix/client/versions")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "versions": [ + "v1.7" + ], + }))) + .expect(1) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/v1/create")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .expect(1) + .mount(&server) + .await; + + Mock::given(method("PUT")) + .and(path("/_matrix/media/v3/upload/example.com/AQwafuaFswefuhsfAFAgsw")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&server) + .await; + + let mxc_uri = client.media().create_content_uri().await.unwrap(); + + assert_eq!(mxc_uri.uri, owned_mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")); + + client + .media() + .upload_preallocated(mxc_uri, &mime::IMAGE_JPEG, b"hello world".to_vec()) + .await + .unwrap(); +} From f5cdbd8e41016c5ac87deef75f4265b78b2b606b Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 18 Oct 2024 12:12:50 +0100 Subject: [PATCH 357/979] refactor(crypto) Rename test functions to reflect wider name change and simplify them slightly by combining the wrapper with the main function. The separation used to be needed, but is not any more. --- .../src/identities/room_identity_state.rs | 84 +++++++------------ 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs index 312aa67d533..87781be5eda 100644 --- a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs +++ b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs @@ -320,7 +320,7 @@ mod tests { // Given someone in the room is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::Pinned).await); + room.member(other_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When their identity changes to unpinned @@ -342,7 +342,7 @@ mod tests { // Given someone in the room is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + room.member(other_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When their identity changes to pinned @@ -394,7 +394,7 @@ mod tests { // Given someone in the room is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::Pinned).await); + room.member(other_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When we are told they are pinned @@ -410,7 +410,7 @@ mod tests { // Given someone in the room is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + room.member(other_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When we are told they are unpinned @@ -426,7 +426,7 @@ mod tests { // Given an empty room and we know of a user who is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identities(user_id, IdentityState::Pinned).await); + room.non_member(other_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When the pinned user joins the room @@ -442,7 +442,7 @@ mod tests { // Given an empty room and we know of a user who is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identities(user_id, IdentityState::PinViolation).await); + room.non_member(other_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When the unpinned user joins the room @@ -464,7 +464,7 @@ mod tests { // Given an empty room and we know of a user who is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identities(user_id, IdentityState::Pinned).await); + room.non_member(other_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When the pinned user is invited to the room @@ -480,7 +480,7 @@ mod tests { // Given an empty room and we know of a user who is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identities(user_id, IdentityState::PinViolation).await); + room.non_member(other_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When the unpinned user is invited to the room @@ -502,7 +502,7 @@ mod tests { // Given I am pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(own_user_identities(user_id, IdentityState::Pinned).await); + room.member(own_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When I become unpinned @@ -518,7 +518,7 @@ mod tests { // Given I am unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(own_user_identities(user_id, IdentityState::PinViolation).await); + room.member(own_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When I become unpinned @@ -534,7 +534,7 @@ mod tests { // Given an empty room and we know of a user who is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(own_user_identities(user_id, IdentityState::Pinned).await); + room.non_member(own_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When the pinned user joins the room @@ -550,7 +550,7 @@ mod tests { // Given an empty room and we know of a user who is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(own_user_identities(user_id, IdentityState::PinViolation).await); + room.non_member(own_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When the unpinned user joins the room @@ -566,7 +566,7 @@ mod tests { // Given a pinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::Pinned).await); + room.member(other_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When the pinned user leaves the room @@ -582,7 +582,7 @@ mod tests { // Given an unpinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + room.member(other_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When the unpinned user leaves the room @@ -604,7 +604,7 @@ mod tests { // Given a pinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::Pinned).await); + room.member(other_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When the pinned user is banned @@ -620,7 +620,7 @@ mod tests { // Given an unpinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::PinViolation).await); + room.member(other_user_identity(user_id, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await; // When the unpinned user is banned @@ -644,9 +644,9 @@ mod tests { let user2 = user_id!("@u2:s.co"); let user3 = user_id!("@u3:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user1, IdentityState::Pinned).await); - room.member(other_user_identities(user2, IdentityState::PinViolation).await); - room.member(other_user_identities(user3, IdentityState::Pinned).await); + room.member(other_user_identity(user1, IdentityState::Pinned).await); + room.member(other_user_identity(user2, IdentityState::PinViolation).await); + room.member(other_user_identity(user3, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When they all change state simultaneously @@ -698,7 +698,7 @@ mod tests { // Given someone in the room is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user_id, IdentityState::Pinned).await); + room.member(other_user_identity(user_id, IdentityState::Pinned).await); let mut state = RoomIdentityState::new(room).await; // When they change state multiple times @@ -753,8 +753,8 @@ mod tests { let user1 = user_id!("@u1:s.co"); let user2 = user_id!("@u2:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user1, IdentityState::Pinned).await); - room.member(other_user_identities(user2, IdentityState::Pinned).await); + room.member(other_user_identity(user1, IdentityState::Pinned).await); + room.member(other_user_identity(user2, IdentityState::Pinned).await); let state = RoomIdentityState::new(room).await; assert!(state.current_state().is_empty()); } @@ -767,10 +767,10 @@ mod tests { let user3 = user_id!("@u3:s.co"); let user4 = user_id!("@u4:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identities(user1, IdentityState::Pinned).await); - room.member(other_user_identities(user2, IdentityState::PinViolation).await); - room.member(other_user_identities(user3, IdentityState::Pinned).await); - room.member(other_user_identities(user4, IdentityState::PinViolation).await); + room.member(other_user_identity(user1, IdentityState::Pinned).await); + room.member(other_user_identity(user2, IdentityState::PinViolation).await); + room.member(other_user_identity(user3, IdentityState::Pinned).await); + room.member(other_user_identity(user4, IdentityState::PinViolation).await); let mut state = RoomIdentityState::new(room).await.current_state(); state.sort_by_key(|change| change.user_id.to_owned()); assert_eq!( @@ -866,13 +866,9 @@ mod tests { for change in changes { let user_identities = if change.own { - let user_identity = - own_user_identity(&change.user_id, change.changed_to.clone()).await; - UserIdentity::Own(user_identity) + own_user_identity(&change.user_id, change.changed_to.clone()).await } else { - let user_identity = - other_user_identity(&change.user_id, change.changed_to.clone()).await; - UserIdentity::Other(user_identity) + other_user_identity(&change.user_id, change.changed_to.clone()).await }; if change.new { @@ -884,19 +880,8 @@ mod tests { RoomIdentityChange::IdentityUpdates(updates) } - /// Create an other `UserIdentity` - async fn other_user_identities( - user_id: &UserId, - identity_state: IdentityState, - ) -> UserIdentity { - UserIdentity::Other(other_user_identity(user_id, identity_state).await) - } - /// Create an other `UserIdentity` for use in tests - async fn other_user_identity( - user_id: &UserId, - identity_state: IdentityState, - ) -> OtherUserIdentity { + async fn other_user_identity(user_id: &UserId, identity_state: IdentityState) -> UserIdentity { use std::sync::Arc; use ruma::owned_device_id; @@ -965,16 +950,11 @@ mod tests { } } - user_identity - } - - /// Create an own `UserIdentity` - async fn own_user_identities(user_id: &UserId, identity_state: IdentityState) -> UserIdentity { - UserIdentity::Own(own_user_identity(user_id, identity_state).await) + UserIdentity::Other(user_identity) } /// Create an own `UserIdentity` for use in tests - async fn own_user_identity(user_id: &UserId, identity_state: IdentityState) -> OwnUserIdentity { + async fn own_user_identity(user_id: &UserId, identity_state: IdentityState) -> UserIdentity { use std::sync::Arc; use ruma::owned_device_id; @@ -1049,7 +1029,7 @@ mod tests { } } - user_identity + UserIdentity::Own(user_identity) } async fn change_master_key(user_identity: &mut OtherUserIdentity, account: &Account) { From 47361b93e977fc5ed8510e463309cc5435ee7b6b Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 18 Oct 2024 15:47:42 +0100 Subject: [PATCH 358/979] refactor(crypto) Test RoomIdentityState by hard-coding identity states --- .../src/identities/room_identity_state.rs | 427 ++++++++---------- 1 file changed, 199 insertions(+), 228 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs index 87781be5eda..1b963be34fe 100644 --- a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs +++ b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs @@ -43,6 +43,24 @@ pub trait RoomIdentityProvider: core::fmt::Debug { /// they are not a member of this room) or None if this user does not /// exist. async fn user_identity(&self, user_id: &UserId) -> Option; + + /// Return the [`IdentityState`] of the supplied user identity. + /// Normally only overridden in tests. + fn state_of(&self, user_identity: &UserIdentity) -> IdentityState { + if user_identity.is_verified() { + IdentityState::Verified + } else if user_identity.has_verification_violation() { + IdentityState::VerificationViolation + } else if let UserIdentity::Other(u) = user_identity { + if u.identity_needs_user_approval() { + IdentityState::PinViolation + } else { + IdentityState::Pinned + } + } else { + IdentityState::Pinned + } + } } /// The state of the identities in a given room - whether they are: @@ -62,7 +80,7 @@ impl RoomIdentityState { /// Create a new RoomIdentityState using the provided room to check whether /// users are members. pub async fn new(room: R) -> Self { - let known_states = KnownStates::from_identities(room.member_identities().await); + let known_states = KnownStates::from_identities(room.member_identities().await, &room); Self { room, known_states } } @@ -135,7 +153,7 @@ impl RoomIdentityState { } } MembershipState::Leave | MembershipState::Ban => { - let leaving_state = state_of(&user_identity); + let leaving_state = self.room.state_of(&user_identity); if leaving_state == IdentityState::PinViolation { // If a user with bad state leaves the room, set them to Pinned, // which effectively removes them @@ -162,7 +180,7 @@ impl RoomIdentityState { ) -> Option { if let UserIdentity::Other(_) = &user_identity { // If the user's state has changed - let new_state = state_of(user_identity); + let new_state = self.room.state_of(user_identity); let old_state = self.known_states.get(user_id); if new_state != old_state { Some(self.set_state(user_identity.user_id(), new_state)) @@ -185,22 +203,6 @@ impl RoomIdentityState { } } -fn state_of(user_identity: &UserIdentity) -> IdentityState { - if user_identity.is_verified() { - IdentityState::Verified - } else if user_identity.has_verification_violation() { - IdentityState::VerificationViolation - } else if let UserIdentity::Other(u) = user_identity { - if u.identity_needs_user_approval() { - IdentityState::PinViolation - } else { - IdentityState::Pinned - } - } else { - IdentityState::Pinned - } -} - /// A change in the status of the identity of a member of the room. Returned by /// [`RoomIdentityState::process_change`] to indicate that something changed in /// this room and we should either show or hide a warning. @@ -259,10 +261,13 @@ struct KnownStates { } impl KnownStates { - fn from_identities(member_identities: impl IntoIterator) -> Self { + fn from_identities( + member_identities: impl IntoIterator, + room: &dyn RoomIdentityProvider, + ) -> Self { let mut known_states = HashMap::new(); for user_identity in member_identities { - let state = state_of(&user_identity); + let state = room.state_of(&user_identity); if state != IdentityState::Pinned { known_states.insert(user_identity.user_id().to_owned(), state); } @@ -289,7 +294,10 @@ impl KnownStates { #[cfg(test)] mod tests { - use std::sync::Arc; + use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + }; use async_trait::async_trait; use matrix_sdk_test::async_test; @@ -304,15 +312,13 @@ mod tests { owned_event_id, owned_user_id, user_id, MilliSecondsSinceUnixEpoch, OwnedUserId, UInt, UserId, }; - use tokio::sync::Mutex; use super::{IdentityState, RoomIdentityChange, RoomIdentityProvider, RoomIdentityState}; use crate::{ identities::user::testing::own_identity_wrapped, - olm::PrivateCrossSigningIdentity, store::{IdentityUpdates, Store}, - Account, IdentityStatusChange, OtherUserIdentity, OtherUserIdentityData, OwnUserIdentity, - OwnUserIdentityData, UserIdentity, + IdentityStatusChange, OtherUserIdentity, OtherUserIdentityData, OwnUserIdentityData, + UserIdentity, }; #[async_test] @@ -320,11 +326,12 @@ mod tests { // Given someone in the room is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When their identity changes to unpinned - let updates = identity_change(user_id, IdentityState::PinViolation, false, false).await; + let updates = + identity_change(&mut room, user_id, IdentityState::PinViolation, false, false).await; let update = state.process_change(updates).await; // Then we emit an update saying they became unpinned @@ -342,11 +349,12 @@ mod tests { // Given someone in the room is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When their identity changes to pinned - let updates = identity_change(user_id, IdentityState::Pinned, false, false).await; + let updates = + identity_change(&mut room, user_id, IdentityState::Pinned, false, false).await; let update = state.process_change(updates).await; // Then we emit an update saying they became pinned @@ -363,11 +371,12 @@ mod tests { async fn test_unpinning_an_identity_not_in_the_room_does_nothing() { // Given an empty room let user_id = user_id!("@u:s.co"); - let room = FakeRoom::new(); - let mut state = RoomIdentityState::new(room).await; + let mut room = FakeRoom::new(); + let mut state = RoomIdentityState::new(room.clone()).await; // When a new unpinned user identity appears but they are not in the room - let updates = identity_change(user_id, IdentityState::PinViolation, true, false).await; + let updates = + identity_change(&mut room, user_id, IdentityState::PinViolation, true, false).await; let update = state.process_change(updates).await; // Then we emit no update @@ -378,11 +387,11 @@ mod tests { async fn test_pinning_an_identity_not_in_the_room_does_nothing() { // Given an empty room let user_id = user_id!("@u:s.co"); - let room = FakeRoom::new(); - let mut state = RoomIdentityState::new(room).await; + let mut room = FakeRoom::new(); + let mut state = RoomIdentityState::new(room.clone()).await; // When a new pinned user appears but is not in the room - let updates = identity_change(user_id, IdentityState::Pinned, true, false).await; + let updates = identity_change(&mut room, user_id, IdentityState::Pinned, true, false).await; let update = state.process_change(updates).await; // Then we emit no update @@ -394,11 +403,12 @@ mod tests { // Given someone in the room is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When we are told they are pinned - let updates = identity_change(user_id, IdentityState::Pinned, false, false).await; + let updates = + identity_change(&mut room, user_id, IdentityState::Pinned, false, false).await; let update = state.process_change(updates).await; // Then we emit no update @@ -410,11 +420,12 @@ mod tests { // Given someone in the room is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When we are told they are unpinned - let updates = identity_change(user_id, IdentityState::PinViolation, false, false).await; + let updates = + identity_change(&mut room, user_id, IdentityState::PinViolation, false, false).await; let update = state.process_change(updates).await; // Then we emit no update @@ -426,8 +437,8 @@ mod tests { // Given an empty room and we know of a user who is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.non_member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When the pinned user joins the room let updates = room_change(user_id, MembershipState::Join); @@ -442,8 +453,8 @@ mod tests { // Given an empty room and we know of a user who is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.non_member(other_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When the unpinned user joins the room let updates = room_change(user_id, MembershipState::Join); @@ -464,8 +475,8 @@ mod tests { // Given an empty room and we know of a user who is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.non_member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When the pinned user is invited to the room let updates = room_change(user_id, MembershipState::Invite); @@ -480,8 +491,8 @@ mod tests { // Given an empty room and we know of a user who is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(other_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.non_member(other_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When the unpinned user is invited to the room let updates = room_change(user_id, MembershipState::Invite); @@ -502,11 +513,12 @@ mod tests { // Given I am pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(own_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.member(own_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When I become unpinned - let updates = identity_change(user_id, IdentityState::PinViolation, false, true).await; + let updates = + identity_change(&mut room, user_id, IdentityState::PinViolation, false, true).await; let update = state.process_change(updates).await; // Then we do nothing because own identities are ignored @@ -518,11 +530,11 @@ mod tests { // Given I am unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(own_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.member(own_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When I become unpinned - let updates = identity_change(user_id, IdentityState::Pinned, false, true).await; + let updates = identity_change(&mut room, user_id, IdentityState::Pinned, false, true).await; let update = state.process_change(updates).await; // Then we do nothing because own identities are ignored @@ -534,8 +546,8 @@ mod tests { // Given an empty room and we know of a user who is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(own_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.non_member(own_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When the pinned user joins the room let updates = room_change(user_id, MembershipState::Join); @@ -550,8 +562,8 @@ mod tests { // Given an empty room and we know of a user who is unpinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.non_member(own_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.non_member(own_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When the unpinned user joins the room let updates = room_change(user_id, MembershipState::Join); @@ -566,8 +578,8 @@ mod tests { // Given a pinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When the pinned user leaves the room let updates = room_change(user_id, MembershipState::Leave); @@ -582,8 +594,8 @@ mod tests { // Given an unpinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When the unpinned user leaves the room let updates = room_change(user_id, MembershipState::Leave); @@ -604,8 +616,8 @@ mod tests { // Given a pinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When the pinned user is banned let updates = room_change(user_id, MembershipState::Ban); @@ -620,8 +632,8 @@ mod tests { // Given an unpinned user is in the room let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::PinViolation).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; // When the unpinned user is banned let updates = room_change(user_id, MembershipState::Ban); @@ -644,32 +656,35 @@ mod tests { let user2 = user_id!("@u2:s.co"); let user3 = user_id!("@u3:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user1, IdentityState::Pinned).await); - room.member(other_user_identity(user2, IdentityState::PinViolation).await); - room.member(other_user_identity(user3, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user1).await, IdentityState::Pinned); + room.member(other_user_identity(user2).await, IdentityState::PinViolation); + room.member(other_user_identity(user3).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When they all change state simultaneously - let updates = identity_changes(&[ - IdentityChangeSpec { - user_id: user1.to_owned(), - changed_to: IdentityState::PinViolation, - new: false, - own: false, - }, - IdentityChangeSpec { - user_id: user2.to_owned(), - changed_to: IdentityState::Pinned, - new: false, - own: false, - }, - IdentityChangeSpec { - user_id: user3.to_owned(), - changed_to: IdentityState::PinViolation, - new: false, - own: false, - }, - ]) + let updates = identity_changes( + &mut room, + &[ + IdentityChangeSpec { + user_id: user1.to_owned(), + changed_to: IdentityState::PinViolation, + new: false, + own: false, + }, + IdentityChangeSpec { + user_id: user2.to_owned(), + changed_to: IdentityState::Pinned, + new: false, + own: false, + }, + IdentityChangeSpec { + user_id: user3.to_owned(), + changed_to: IdentityState::PinViolation, + new: false, + own: false, + }, + ], + ) .await; let update = state.process_change(updates).await; @@ -698,26 +713,31 @@ mod tests { // Given someone in the room is pinned let user_id = user_id!("@u:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user_id, IdentityState::Pinned).await); - let mut state = RoomIdentityState::new(room).await; + room.member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; // When they change state multiple times let update1 = state .process_change( - identity_change(user_id, IdentityState::PinViolation, false, false).await, + identity_change(&mut room, user_id, IdentityState::PinViolation, false, false) + .await, ) .await; let update2 = state .process_change( - identity_change(user_id, IdentityState::PinViolation, false, false).await, + identity_change(&mut room, user_id, IdentityState::PinViolation, false, false) + .await, ) .await; let update3 = state - .process_change(identity_change(user_id, IdentityState::Pinned, false, false).await) + .process_change( + identity_change(&mut room, user_id, IdentityState::Pinned, false, false).await, + ) .await; let update4 = state .process_change( - identity_change(user_id, IdentityState::PinViolation, false, false).await, + identity_change(&mut room, user_id, IdentityState::PinViolation, false, false) + .await, ) .await; @@ -753,8 +773,8 @@ mod tests { let user1 = user_id!("@u1:s.co"); let user2 = user_id!("@u2:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user1, IdentityState::Pinned).await); - room.member(other_user_identity(user2, IdentityState::Pinned).await); + room.member(other_user_identity(user1).await, IdentityState::Pinned); + room.member(other_user_identity(user2).await, IdentityState::Pinned); let state = RoomIdentityState::new(room).await; assert!(state.current_state().is_empty()); } @@ -767,10 +787,10 @@ mod tests { let user3 = user_id!("@u3:s.co"); let user4 = user_id!("@u4:s.co"); let mut room = FakeRoom::new(); - room.member(other_user_identity(user1, IdentityState::Pinned).await); - room.member(other_user_identity(user2, IdentityState::PinViolation).await); - room.member(other_user_identity(user3, IdentityState::Pinned).await); - room.member(other_user_identity(user4, IdentityState::PinViolation).await); + room.member(other_user_identity(user1).await, IdentityState::Pinned); + room.member(other_user_identity(user2).await, IdentityState::PinViolation); + room.member(other_user_identity(user3).await, IdentityState::Pinned); + room.member(other_user_identity(user4).await, IdentityState::PinViolation); let mut state = RoomIdentityState::new(room).await.current_state(); state.sort_by_key(|change| change.user_id.to_owned()); assert_eq!( @@ -783,47 +803,85 @@ mod tests { IdentityStatusChange { user_id: owned_user_id!("@u4:s.co"), changed_to: IdentityState::PinViolation + }, + IdentityStatusChange { + user_id: owned_user_id!("@u5:s.co"), + changed_to: IdentityState::Verified + }, + IdentityStatusChange { + user_id: owned_user_id!("@u6:s.co"), + changed_to: IdentityState::VerificationViolation } ] ); } #[derive(Debug)] + struct Membership { + is_member: bool, + user_identity: UserIdentity, + identity_state: IdentityState, + } + + #[derive(Clone, Debug)] struct FakeRoom { - members: Vec, - non_members: Vec, + users: Arc>>, } impl FakeRoom { fn new() -> Self { - Self { members: Default::default(), non_members: Default::default() } + Self { users: Default::default() } + } + + fn member(&mut self, user_identity: UserIdentity, identity_state: IdentityState) { + self.users.lock().unwrap().insert( + user_identity.user_id().to_owned(), + Membership { is_member: true, user_identity, identity_state }, + ); } - fn member(&mut self, user_identity: UserIdentity) { - self.members.push(user_identity); + fn non_member(&mut self, user_identity: UserIdentity, identity_state: IdentityState) { + self.users.lock().unwrap().insert( + user_identity.user_id().to_owned(), + Membership { is_member: false, user_identity, identity_state }, + ); } - fn non_member(&mut self, user_identity: UserIdentity) { - self.non_members.push(user_identity); + fn update_state(&self, user_id: &UserId, changed_to: &IdentityState) { + self.users + .lock() + .unwrap() + .entry(user_id.to_owned()) + .and_modify(|m| m.identity_state = changed_to.clone()); } } #[async_trait] impl RoomIdentityProvider for FakeRoom { async fn is_member(&self, user_id: &UserId) -> bool { - self.members.iter().any(|u| u.user_id() == user_id) + self.users.lock().unwrap().get(user_id).map(|m| m.is_member).unwrap_or(false) } async fn member_identities(&self) -> Vec { - self.members.clone() + self.users + .lock() + .unwrap() + .values() + .filter_map(|m| if m.is_member { Some(m.user_identity.clone()) } else { None }) + .collect() } async fn user_identity(&self, user_id: &UserId) -> Option { - self.non_members - .iter() - .chain(self.members.iter()) - .find(|u| u.user_id() == user_id) - .cloned() + self.users.lock().unwrap().get(user_id).map(|m| m.user_identity.clone()) + } + + fn state_of(&self, user_identity: &UserIdentity) -> IdentityState { + self.users + .lock() + .unwrap() + .get(user_identity.user_id()) + .map(|m| m.identity_state.clone()) + .unwrap_or(IdentityState::Pinned) } } @@ -840,17 +898,16 @@ mod tests { } async fn identity_change( + room: &mut FakeRoom, user_id: &UserId, changed_to: IdentityState, new: bool, own: bool, ) -> RoomIdentityChange { - identity_changes(&[IdentityChangeSpec { - user_id: user_id.to_owned(), - changed_to, - new, - own, - }]) + identity_changes( + room, + &[IdentityChangeSpec { user_id: user_id.to_owned(), changed_to, new, own }], + ) .await } @@ -861,27 +918,31 @@ mod tests { own: bool, } - async fn identity_changes(changes: &[IdentityChangeSpec]) -> RoomIdentityChange { + async fn identity_changes( + room: &mut FakeRoom, + changes: &[IdentityChangeSpec], + ) -> RoomIdentityChange { let mut updates = IdentityUpdates::default(); for change in changes { - let user_identities = if change.own { - own_user_identity(&change.user_id, change.changed_to.clone()).await + let user_identity = if change.own { + own_user_identity(&change.user_id).await } else { - other_user_identity(&change.user_id, change.changed_to.clone()).await + other_user_identity(&change.user_id).await }; + room.update_state(user_identity.user_id(), &change.changed_to); if change.new { - updates.new.insert(user_identities.user_id().to_owned(), user_identities); + updates.new.insert(user_identity.user_id().to_owned(), user_identity); } else { - updates.changed.insert(user_identities.user_id().to_owned(), user_identities); + updates.changed.insert(user_identity.user_id().to_owned(), user_identity); } } RoomIdentityChange::IdentityUpdates(updates) } /// Create an other `UserIdentity` for use in tests - async fn other_user_identity(user_id: &UserId, identity_state: IdentityState) -> UserIdentity { + async fn other_user_identity(user_id: &UserId) -> UserIdentity { use std::sync::Arc; use ruma::owned_device_id; @@ -903,7 +964,7 @@ mod tests { let other_user_identity_data = OtherUserIdentityData::from_private(&*private_identity.lock().await).await; - let mut user_identity = OtherUserIdentity { + UserIdentity::Other(OtherUserIdentity { inner: other_user_identity_data, own_identity: None, verification_machine: VerificationMachine::new( @@ -917,44 +978,11 @@ mod tests { MemoryStore::new(), )), ), - }; - - match identity_state { - IdentityState::Verified => { - // TODO - assert!(user_identity.is_verified()); - assert!(!user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - assert!(!user_identity.identity_needs_user_approval()); - } - IdentityState::Pinned => { - // Pinned is the default state - assert!(!user_identity.is_verified()); - assert!(!user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - assert!(!user_identity.identity_needs_user_approval()); - } - IdentityState::PinViolation => { - change_master_key(&mut user_identity, &account).await; - assert!(!user_identity.is_verified()); - assert!(!user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - assert!(user_identity.identity_needs_user_approval()); - } - IdentityState::VerificationViolation => { - // TODO - assert!(!user_identity.is_verified()); - assert!(user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - assert!(user_identity.identity_needs_user_approval()); - } - } - - UserIdentity::Other(user_identity) + }) } /// Create an own `UserIdentity` for use in tests - async fn own_user_identity(user_id: &UserId, identity_state: IdentityState) -> UserIdentity { + async fn own_user_identity(user_id: &UserId) -> UserIdentity { use std::sync::Arc; use ruma::owned_device_id; @@ -987,7 +1015,7 @@ mod tests { )), ); - let mut user_identity = own_identity_wrapped( + UserIdentity::Own(own_identity_wrapped( own_user_identity_data, verification_machine.clone(), Store::new( @@ -1000,63 +1028,6 @@ mod tests { )), verification_machine, ), - ); - - match identity_state { - IdentityState::Verified => { - // TODO - assert!(user_identity.is_verified()); - assert!(!user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - } - IdentityState::Pinned => { - // Pinned is the default state - assert!(!user_identity.is_verified()); - assert!(!user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - } - IdentityState::PinViolation => { - change_own_master_key(&mut user_identity, &account).await; - assert!(!user_identity.is_verified()); - assert!(!user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - } - IdentityState::VerificationViolation => { - // TODO - assert!(!user_identity.is_verified()); - assert!(user_identity.was_previously_verified()); - assert!(!user_identity.has_verification_violation()); - } - } - - UserIdentity::Own(user_identity) - } - - async fn change_master_key(user_identity: &mut OtherUserIdentity, account: &Account) { - // Create a new master key and self signing key - let private_identity = - Arc::new(Mutex::new(PrivateCrossSigningIdentity::with_account(account).await.0)); - let data = OtherUserIdentityData::from_private(&*private_identity.lock().await).await; - - // And set them on the existing identity - user_identity - .update(data.master_key().clone(), data.self_signing_key().clone(), None) - .unwrap(); - } - - async fn change_own_master_key(user_identity: &mut OwnUserIdentity, account: &Account) { - // Create a new master key and self signing key - let private_identity = - Arc::new(Mutex::new(PrivateCrossSigningIdentity::with_account(account).await.0)); - let data = OwnUserIdentityData::from_private(&*private_identity.lock().await).await; - - // And set them on the existing identity - user_identity - .update( - data.master_key().clone(), - data.self_signing_key().clone(), - data.user_signing_key().clone(), - ) - .unwrap(); + )) } } From 3558886b989922e76e1969f3fe211d12258d3253 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 18 Oct 2024 15:39:48 +0100 Subject: [PATCH 359/979] feat(crypto) Support Verified and VerificationViolation updates in IdentityStatusChanges streams --- .../src/identities/room_identity_state.rs | 215 ++++++++- .../src/room/identity_status_changes.rs | 410 +++++++++++++----- 2 files changed, 503 insertions(+), 122 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs index 1b963be34fe..d4aa4817a0a 100644 --- a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs +++ b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs @@ -121,7 +121,6 @@ impl RoomIdentityState { for user_identity in identity_updates.new.values().chain(identity_updates.changed.values()) { - // Ignore updates to our own identity let user_id = user_identity.user_id(); if self.room.is_member(user_id).await { let update = self.update_user_state(user_id, user_identity); @@ -144,20 +143,27 @@ impl RoomIdentityState { // Ignore invalid user IDs let user_id: Result<&UserId, _> = event.state_key.as_str().try_into(); if let Ok(user_id) = user_id { - // Ignore non-existent users - if let Some(user_identity) = self.room.user_identity(user_id).await { + // Ignore non-existent users, and changes to our own identity + if let Some(user_identity @ UserIdentity::Other(_)) = + self.room.user_identity(user_id).await + { match event.content.membership { MembershipState::Join | MembershipState::Invite => { + // They are joining the room - check whether we need to display a + // warning to the user if let Some(update) = self.update_user_state(user_id, &user_identity) { return vec![update]; } } MembershipState::Leave | MembershipState::Ban => { - let leaving_state = self.room.state_of(&user_identity); - if leaving_state == IdentityState::PinViolation { - // If a user with bad state leaves the room, set them to Pinned, - // which effectively removes them - return vec![self.set_state(user_id, IdentityState::Pinned)]; + // They are leaving the room - treat that as if they are becoming + // Pinned, which means the UI will remove any banner it was displaying + // for them. + + if let Some(update) = + self.update_user_state_to(user_id, IdentityState::Pinned) + { + return vec![update]; } } MembershipState::Knock => { @@ -179,21 +185,59 @@ impl RoomIdentityState { user_identity: &UserIdentity, ) -> Option { if let UserIdentity::Other(_) = &user_identity { - // If the user's state has changed - let new_state = self.room.state_of(user_identity); - let old_state = self.known_states.get(user_id); - if new_state != old_state { - Some(self.set_state(user_identity.user_id(), new_state)) - } else { - // Nothing changed - None - } + self.update_user_state_to(user_id, self.room.state_of(user_identity)) } else { // Ignore updates to our own identity None } } + // If the supplied `new_state` represents an actual change, updates our internal + // state for this user, and returns the change information we will surface to + // the UI. + fn update_user_state_to( + &mut self, + user_id: &UserId, + new_state: IdentityState, + ) -> Option { + use IdentityState::*; + + let old_state = self.known_states.get(user_id); + + match (old_state, &new_state) { + // good -> bad - report so we can add a message + (Pinned, PinViolation) | + (Pinned, VerificationViolation) | + (Verified, PinViolation) | + (Verified, VerificationViolation) | + + // bad -> good - report so we can remove a message + (PinViolation, Pinned) | + (PinViolation, Verified) | + (VerificationViolation, Pinned) | + (VerificationViolation, Verified) | + + // Changed the type of bad - report so can change the message + (PinViolation, VerificationViolation) | + (VerificationViolation, PinViolation) => Some(self.set_state(user_id, new_state)), + + // good -> good - don't report - no message needed in either case + (Pinned, Verified) | + (Verified, Pinned) => { + // The state has changed, so we update it + self.set_state(user_id, new_state); + // but there is no need to report a change to the UI + None + } + + // State didn't change - don't report - nothing changed + (Pinned, Pinned) | + (Verified, Verified) | + (PinViolation, PinViolation) | + (VerificationViolation, VerificationViolation) => None, + } + } + fn set_state(&mut self, user_id: &UserId, new_state: IdentityState) -> IdentityStatusChange { // Remember the new state of the user self.known_states.set(user_id, &new_state); @@ -204,8 +248,16 @@ impl RoomIdentityState { } /// A change in the status of the identity of a member of the room. Returned by -/// [`RoomIdentityState::process_change`] to indicate that something changed in -/// this room and we should either show or hide a warning. +/// [`RoomIdentityState::process_change`] to indicate that something significant +/// changed in this room and we should either show or hide a warning. +/// +/// Examples of "significant" changes: +/// - pinned->unpinned +/// - verification violation->verified +/// +/// Examples of "insignificant" changes: +/// - pinned->verified +/// - verified->pinned #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct IdentityStatusChange { /// The user ID of the user whose identity status changed @@ -344,6 +396,23 @@ mod tests { ); } + #[async_test] + async fn test_verifying_a_pinned_identity_in_the_room_does_nothing() { + // Given someone in the room is pinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identity(user_id).await, IdentityState::Pinned); + let mut state = RoomIdentityState::new(room.clone()).await; + + // When their identity changes to verified + let updates = + identity_change(&mut room, user_id, IdentityState::Verified, false, false).await; + let update = state.process_change(updates).await; + + // Then we emit no update + assert_eq!(update, vec![]); + } + #[async_test] async fn test_pinning_an_unpinned_identity_in_the_room_notifies() { // Given someone in the room is unpinned @@ -367,6 +436,30 @@ mod tests { ); } + #[async_test] + async fn test_unpinned_identity_becoming_verification_violating_in_the_room_notifies() { + // Given someone in the room is unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identity(user_id).await, IdentityState::PinViolation); + let mut state = RoomIdentityState::new(room.clone()).await; + + // When their identity changes to verification violation + let updates = + identity_change(&mut room, user_id, IdentityState::VerificationViolation, false, false) + .await; + let update = state.process_change(updates).await; + + // Then we emit an update saying they became verification violating + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::VerificationViolation + }] + ); + } + #[async_test] async fn test_unpinning_an_identity_not_in_the_room_does_nothing() { // Given an empty room @@ -448,6 +541,22 @@ mod tests { assert_eq!(update, []); } + #[async_test] + async fn test_a_verified_identity_joining_the_room_does_nothing() { + // Given an empty room and we know of a user who is verified + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(other_user_identity(user_id).await, IdentityState::Verified); + let mut state = RoomIdentityState::new(room).await; + + // When the verified user joins the room + let updates = room_change(user_id, MembershipState::Join); + let update = state.process_change(updates).await; + + // Then we emit no update because they are verified + assert_eq!(update, []); + } + #[async_test] async fn test_an_unpinned_identity_joining_the_room_notifies() { // Given an empty room and we know of a user who is unpinned @@ -508,6 +617,28 @@ mod tests { ); } + #[async_test] + async fn test_a_verification_violating_identity_invited_to_the_room_notifies() { + // Given an empty room and we know of a user who is unpinned + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.non_member(other_user_identity(user_id).await, IdentityState::VerificationViolation); + let mut state = RoomIdentityState::new(room).await; + + // When the user is invited to the room + let updates = room_change(user_id, MembershipState::Invite); + let update = state.process_change(updates).await; + + // Then we emit an update saying they became verification violation + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::VerificationViolation + }] + ); + } + #[async_test] async fn test_own_identity_becoming_unpinned_is_ignored() { // Given I am pinned @@ -589,6 +720,22 @@ mod tests { assert_eq!(update, []); } + #[async_test] + async fn test_a_verified_identity_leaving_the_room_does_nothing() { + // Given a pinned user is in the room + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identity(user_id).await, IdentityState::Verified); + let mut state = RoomIdentityState::new(room).await; + + // When the user leaves the room + let updates = room_change(user_id, MembershipState::Leave); + let update = state.process_change(updates).await; + + // Then we emit no update because they are verified + assert_eq!(update, []); + } + #[async_test] async fn test_an_unpinned_identity_leaving_the_room_notifies() { // Given an unpinned user is in the room @@ -601,7 +748,29 @@ mod tests { let updates = room_change(user_id, MembershipState::Leave); let update = state.process_change(updates).await; - // Then we emit an update saying they became unpinned + // Then we emit an update saying they became pinned + assert_eq!( + update, + vec![IdentityStatusChange { + user_id: user_id.to_owned(), + changed_to: IdentityState::Pinned + }] + ); + } + + #[async_test] + async fn test_a_verification_violating_identity_leaving_the_room_notifies() { + // Given an unpinned user is in the room + let user_id = user_id!("@u:s.co"); + let mut room = FakeRoom::new(); + room.member(other_user_identity(user_id).await, IdentityState::VerificationViolation); + let mut state = RoomIdentityState::new(room).await; + + // When the user leaves the room + let updates = room_change(user_id, MembershipState::Leave); + let update = state.process_change(updates).await; + + // Then we emit an update saying they became pinned assert_eq!( update, vec![IdentityStatusChange { @@ -780,17 +949,21 @@ mod tests { } #[async_test] - async fn test_current_state_contains_all_unpinned_users() { + async fn test_current_state_contains_all_nonpinned_users() { // Given some people are unpinned let user1 = user_id!("@u1:s.co"); let user2 = user_id!("@u2:s.co"); let user3 = user_id!("@u3:s.co"); let user4 = user_id!("@u4:s.co"); + let user5 = user_id!("@u5:s.co"); + let user6 = user_id!("@u6:s.co"); let mut room = FakeRoom::new(); room.member(other_user_identity(user1).await, IdentityState::Pinned); room.member(other_user_identity(user2).await, IdentityState::PinViolation); room.member(other_user_identity(user3).await, IdentityState::Pinned); room.member(other_user_identity(user4).await, IdentityState::PinViolation); + room.member(other_user_identity(user5).await, IdentityState::Verified); + room.member(other_user_identity(user6).await, IdentityState::VerificationViolation); let mut state = RoomIdentityState::new(room).await.current_state(); state.sort_by_key(|change| change.user_id.to_owned()); assert_eq!( diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index 5d85456a4fb..a1b69072ec7 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -56,12 +56,18 @@ pub struct IdentityStatusChanges { } impl IdentityStatusChanges { - /// Create a new stream of changes to the identity status of members of a - /// room. + /// Create a new stream of significant changes to the identity status of + /// members of a room. /// /// The "status" of an identity changes when our level of trust in it /// changes. /// + /// A "significant" change means a warning should either be added or removed + /// (e.g. the user changed from pinned to unpinned (show a warning) or + /// from verification violation to pinned (remove a warning). An + /// insignificant change would be from pinned to verified - no warning + /// is needed in this case. + /// /// For example, if an identity is "pinned" i.e. not manually verified, but /// known, and it becomes a "unpinned" i.e. unknown, because the /// encryption keys are different and the user has not acknowledged @@ -196,94 +202,246 @@ mod tests { use futures_core::Stream; use futures_util::FutureExt; use matrix_sdk_base::crypto::{IdentityState, IdentityStatusChange}; - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, test_json::keys_query_sets::IdentityChangeDataSet}; use test_setup::TestSetup; use tokio_stream::{StreamExt, Timeout}; #[async_test] async fn test_when_user_becomes_unpinned_we_report_it() { // Given a room containing us and Bob - let t = TestSetup::new_room_with_other_member().await; + let t = TestSetup::new_room_with_other_bob().await; // And Bob's identity is pinned - t.pin().await; + t.pin_bob().await; // And we are listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; // When Bob becomes unpinned - t.unpin().await; + t.unpin_bob().await; // Then we were notified about it let change = next_change(&mut pin!(changes)).await; - assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].user_id, t.bob_user_id()); assert_eq!(change[0].changed_to, IdentityState::PinViolation); assert_eq!(change.len(), 1); } + #[async_test] + async fn test_when_user_becomes_verification_violation_we_report_it() { + // Given a room containing us and Bob + let t = TestSetup::new_room_with_other_bob().await; + + // And Bob's identity is verified + t.verify_bob().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + + // When Bob's identity changes + t.unpin_bob().await; + + // Then we were notified about a verification violation + let change = next_change(&mut pin!(changes)).await; + assert_eq!(change[0].user_id, t.bob_user_id()); + assert_eq!(change[0].changed_to, IdentityState::VerificationViolation); + assert_eq!(change.len(), 1); + } + #[async_test] async fn test_when_user_becomes_pinned_we_report_it() { // Given a room containing us and Bob - let t = TestSetup::new_room_with_other_member().await; + let t = TestSetup::new_room_with_other_bob().await; // And Bob's identity is unpinned - t.unpin().await; + t.unpin_bob().await; // And we are listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; let mut changes = pin!(changes); // When Bob becomes pinned - t.pin().await; + t.pin_bob().await; // Then we were notified about the initial state of the room let change1 = next_change(&mut changes).await; - assert_eq!(change1[0].user_id, t.user_id()); + assert_eq!(change1[0].user_id, t.bob_user_id()); assert_eq!(change1[0].changed_to, IdentityState::PinViolation); assert_eq!(change1.len(), 1); // And the change when Bob became pinned let change2 = next_change(&mut changes).await; - assert_eq!(change2[0].user_id, t.user_id()); + assert_eq!(change2[0].user_id, t.bob_user_id()); assert_eq!(change2[0].changed_to, IdentityState::Pinned); assert_eq!(change2.len(), 1); } + #[async_test] + async fn test_when_user_becomes_verified_we_dont_report_it() { + // Given a room containing us and Bob + let t = TestSetup::new_room_with_other_bob().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + let mut changes = pin!(changes); + + // When Bob becomes verified + t.verify_bob().await; + + // (And then unpinned, so we have something to come through the stream) + t.unpin_bob().await; + + // Then we are only notified about the unpinning part + let change2 = next_change(&mut changes).await; + assert_eq!(change2[0].user_id, t.bob_user_id()); + assert_eq!(change2[0].changed_to, IdentityState::VerificationViolation); + assert_eq!(change2.len(), 1); + } + + #[async_test] + async fn test_when_an_unpinned_user_becomes_verified_we_report_it() { + // Given a room containing us and Bob + let t = TestSetup::new_room_with_other_bob().await; + + // And Bob's identity is unpinned + t.unpin_bob_with(IdentityChangeDataSet::key_query_with_identity_a()).await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + let mut changes = pin!(changes); + + // When Bob becomes verified + t.verify_bob().await; + + // Then we were notified about the initial state of the room + let change1 = next_change(&mut changes).await; + assert_eq!(change1[0].user_id, t.bob_user_id()); + assert_eq!(change1[0].changed_to, IdentityState::PinViolation); + assert_eq!(change1.len(), 1); + + // And the change when Bob became verified + let change2 = next_change(&mut changes).await; + assert_eq!(change2[0].user_id, t.bob_user_id()); + assert_eq!(change2[0].changed_to, IdentityState::Verified); + assert_eq!(change2.len(), 1); + } + + #[async_test] + async fn test_when_user_in_verification_violation_becomes_verified_we_report_it() { + // Given a room containing us and Bob + let t = TestSetup::new_room_with_other_bob().await; + + // And Bob is in verification violation + t.verify_bob_with( + IdentityChangeDataSet::key_query_with_identity_b(), + IdentityChangeDataSet::msk_b(), + IdentityChangeDataSet::ssk_b(), + ) + .await; + t.unpin_bob().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + let mut changes = pin!(changes); + + // When Bob becomes verified + t.verify_bob().await; + + // Then we were notified about the initial state of the room + let change1 = next_change(&mut changes).await; + assert_eq!(change1[0].user_id, t.bob_user_id()); + assert_eq!(change1[0].changed_to, IdentityState::VerificationViolation); + assert_eq!(change1.len(), 1); + + // And the change when Bob became verified + let change2 = next_change(&mut changes).await; + assert_eq!(change2[0].user_id, t.bob_user_id()); + assert_eq!(change2[0].changed_to, IdentityState::Verified); + assert_eq!(change2.len(), 1); + } + #[async_test] async fn test_when_an_unpinned_user_joins_we_report_it() { // Given a room containing just us let mut t = TestSetup::new_just_me_room().await; // And Bob's identity is unpinned - t.unpin().await; + t.unpin_bob().await; // And we are listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; // When Bob joins the room - t.join().await; + t.bob_joins().await; // Then we were notified about it let change = next_change(&mut pin!(changes)).await; - assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].user_id, t.bob_user_id()); assert_eq!(change[0].changed_to, IdentityState::PinViolation); assert_eq!(change.len(), 1); } + #[async_test] + async fn test_when_an_verification_violating_user_joins_we_report_it() { + // Given a room containing just us + let mut t = TestSetup::new_just_me_room().await; + + // And Bob's identity is in verification violation + t.verify_bob().await; + t.unpin_bob().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + + // When Bob joins the room + t.bob_joins().await; + + // Then we were notified about it + let change = next_change(&mut pin!(changes)).await; + assert_eq!(change[0].user_id, t.bob_user_id()); + assert_eq!(change[0].changed_to, IdentityState::VerificationViolation); + assert_eq!(change.len(), 1); + } + + #[async_test] + async fn test_when_a_verified_user_joins_we_dont_report_it() { + // Given a room containing just us + let mut t = TestSetup::new_just_me_room().await; + + // And Bob's identity is verified + t.verify_bob().await; + + // And we are listening for identity changes + let changes = t.subscribe_to_identity_status_changes().await; + + // When Bob joins the room + t.bob_joins().await; + + // (Then becomes unpinned so we have something to report) + t.unpin_bob().await; + + //// Then we were only notified about the unpin + let mut changes = pin!(changes); + let change = next_change(&mut changes).await; + assert_eq!(change[0].user_id, t.bob_user_id()); + assert_eq!(change[0].changed_to, IdentityState::VerificationViolation); + assert_eq!(change.len(), 1); + } + #[async_test] async fn test_when_a_pinned_user_joins_we_do_not_report() { // Given a room containing just us let mut t = TestSetup::new_just_me_room().await; // And Bob's identity is unpinned - t.pin().await; + t.pin_bob().await; // And we are listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; let mut changes = pin!(changes); // When Bob joins the room - t.join().await; + t.bob_joins().await; // Then there is no notification tokio::time::sleep(Duration::from_millis(200)).await; @@ -294,21 +452,21 @@ mod tests { #[async_test] async fn test_when_an_unpinned_user_leaves_we_report_it() { // Given a room containing us and Bob - let mut t = TestSetup::new_room_with_other_member().await; + let mut t = TestSetup::new_room_with_other_bob().await; // And Bob's identity is unpinned - t.unpin().await; + t.unpin_bob().await; // And we are listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; let mut changes = pin!(changes); // When Bob leaves the room - t.leave().await; + t.bob_leaves().await; // Then we were notified about the initial state of the room let change1 = next_change(&mut changes).await; - assert_eq!(change1[0].user_id, t.user_id()); + assert_eq!(change1[0].user_id, t.bob_user_id()); assert_eq!(change1[0].changed_to, IdentityState::PinViolation); assert_eq!(change1.len(), 1); @@ -316,7 +474,7 @@ mod tests { let change2 = next_change(&mut changes).await; // Note: the user left the room, but we see that as them "becoming pinned" i.e. // "you no longer need to notify about this user". - assert_eq!(change2[0].user_id, t.user_id()); + assert_eq!(change2[0].user_id, t.bob_user_id()); assert_eq!(change2[0].changed_to, IdentityState::Pinned); assert_eq!(change2.len(), 1); } @@ -327,7 +485,7 @@ mod tests { let mut t = TestSetup::new_just_me_room().await; // And Bob's identity is unpinned - t.unpin().await; + t.unpin_bob().await; // And we are listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; @@ -342,29 +500,29 @@ mod tests { // have fully completed. // When Bob joins the room ... - t.join().await; + t.bob_joins().await; let change1 = next_change(&mut changes).await; // ... becomes pinned ... - t.pin().await; + t.pin_bob().await; let change2 = next_change(&mut changes).await; // ... leaves and joins again (ignored since they stay pinned) ... - t.leave().await; - t.join().await; + t.bob_leaves().await; + t.bob_joins().await; // ... becomes unpinned ... - t.unpin().await; + t.unpin_bob().await; let change3 = next_change(&mut changes).await; // ... and leaves. - t.leave().await; + t.bob_leaves().await; let change4 = next_change(&mut changes).await; - assert_eq!(change1[0].user_id, t.user_id()); - assert_eq!(change2[0].user_id, t.user_id()); - assert_eq!(change3[0].user_id, t.user_id()); - assert_eq!(change4[0].user_id, t.user_id()); + assert_eq!(change1[0].user_id, t.bob_user_id()); + assert_eq!(change2[0].user_id, t.bob_user_id()); + assert_eq!(change3[0].user_id, t.bob_user_id()); + assert_eq!(change4[0].user_id, t.bob_user_id()); assert_eq!(change1[0].changed_to, IdentityState::PinViolation); assert_eq!(change2[0].changed_to, IdentityState::Pinned); @@ -380,15 +538,15 @@ mod tests { #[async_test] async fn test_when_an_unpinned_user_is_already_present_we_report_it_immediately() { // Given a room containing Bob, who is unpinned - let t = TestSetup::new_room_with_other_member().await; - t.unpin().await; + let t = TestSetup::new_room_with_other_bob().await; + t.unpin_bob().await; // When we start listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; // Then we were immediately notified about Bob being unpinned let change = next_change(&mut pin!(changes)).await; - assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].user_id, t.bob_user_id()); assert_eq!(change[0].changed_to, IdentityState::PinViolation); assert_eq!(change.len(), 1); } @@ -396,18 +554,18 @@ mod tests { #[async_test] async fn test_when_a_verified_user_is_already_present_we_dont_report_it() { // Given a room containing Bob, who is unpinned - let t = TestSetup::new_room_with_other_member().await; - t.verify().await; + let t = TestSetup::new_room_with_other_bob().await; + t.verify_bob().await; // When we start listening for identity changes let changes = t.subscribe_to_identity_status_changes().await; // (And we unpin so that something is available in the changes stream) - t.unpin().await; + t.unpin_bob().await; // Then we were only notified about the unpin, not being verified let change = next_change(&mut pin!(changes)).await; - assert_eq!(change[0].user_id, t.user_id()); + assert_eq!(change[0].user_id, t.bob_user_id()); assert_eq!(change[0].changed_to, IdentityState::VerificationViolation); assert_eq!(change.len(), 1); } @@ -422,8 +580,8 @@ mod tests { changes .next() .await - .expect("There should be an identity update") - .expect("Should not time out") + .expect("Should not reach end of changes stream") + .expect("Should not time out waiting for a change") } mod test_setup { @@ -442,8 +600,9 @@ mod tests { StateTestEvent, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, }; use ruma::{ - api::client::keys::get_keys, events::room::member::MembershipState, owned_user_id, - OwnedUserId, TransactionId, UserId, + api::client::keys::{get_keys, get_keys::v3::Response as KeyQueryResponse}, + events::room::member::MembershipState, + owned_user_id, OwnedUserId, TransactionId, UserId, }; use serde_json::json; use tokio_stream::{StreamExt as _, Timeout}; @@ -457,12 +616,17 @@ mod tests { }; /// Sets up a client and a room and allows changing user identities and - /// room memberships. Note: most methods e.g. [`TestSetup::user_id`] are - /// talking about the OTHER user, not our own user. Only methods - /// starting with `self_` are talking about this user. + /// room memberships. Note: most methods e.g. [`TestSetup::bob_user_id`] + /// are talking about the OTHER user, not our own user. Only + /// methods starting with `self_` are talking about this user. + /// + /// This user is called `@example:localhost` but is rarely used + /// mentioned. + /// + /// The other user is called `@bob:localhost`. pub(super) struct TestSetup { client: Client, - user_id: OwnedUserId, + bob_user_id: OwnedUserId, sync_response_builder: SyncResponseBuilder, room: Room, } @@ -471,30 +635,33 @@ mod tests { pub(super) async fn new_just_me_room() -> Self { let (client, user_id, mut sync_response_builder) = Self::init().await; let room = create_just_me_room(&client, &mut sync_response_builder).await; - Self { client, user_id, sync_response_builder, room } + Self { client, bob_user_id: user_id, sync_response_builder, room } } - pub(super) async fn new_room_with_other_member() -> Self { - let (client, user_id, mut sync_response_builder) = Self::init().await; - let room = - create_room_with_other_member(&mut sync_response_builder, &client, &user_id) - .await; - Self { client, user_id, sync_response_builder, room } + pub(super) async fn new_room_with_other_bob() -> Self { + let (client, bob_user_id, mut sync_response_builder) = Self::init().await; + let room = create_room_with_other_member( + &mut sync_response_builder, + &client, + &bob_user_id, + ) + .await; + Self { client, bob_user_id, sync_response_builder, room } } - pub(super) fn user_id(&self) -> &UserId { - &self.user_id + pub(super) fn bob_user_id(&self) -> &UserId { + &self.bob_user_id } - pub(super) async fn pin(&self) { - if self.user_identity().await.is_some() { + pub(super) async fn pin_bob(&self) { + if self.bob_user_identity().await.is_some() { assert!( - !self.is_pinned().await, - "pin() called when the identity is already pinned!" + !self.bob_is_pinned().await, + "pin_bob() called when the identity is already pinned!" ); // Pin it - self.user_identity() + self.bob_user_identity() .await .expect("User should exist") .pin() @@ -502,32 +669,73 @@ mod tests { .expect("Should not fail to pin"); } else { // There was no existing identity. Set one. It will be pinned by default. - self.change_identity(IdentityChangeDataSet::key_query_with_identity_a()).await; + self.change_bob_identity(IdentityChangeDataSet::key_query_with_identity_a()) + .await; } // Sanity check: they are pinned - assert!(self.is_pinned().await); + assert!(self.bob_is_pinned().await); } - pub(super) async fn unpin(&self) { - // Change/set their identity - this will unpin if they already had one. - // If this was the first time we'd done this, they are now pinned. - self.change_identity(IdentityChangeDataSet::key_query_with_identity_a()).await; + pub(super) async fn unpin_bob(&self) { + self.unpin_bob_with(IdentityChangeDataSet::key_query_with_identity_b()).await; + } - if self.is_pinned().await { - // Change their identity. Now they are definitely unpinned - self.change_identity(IdentityChangeDataSet::key_query_with_identity_b()).await; + pub(super) async fn unpin_bob_with(&self, requested: KeyQueryResponse) { + fn master_key_json(key_query_response: &KeyQueryResponse) -> String { + serde_json::to_string( + key_query_response + .master_keys + .first_key_value() + .expect("Master key should have a value") + .1, + ) + .expect("Should be able to serialise master key") + } + + let a = IdentityChangeDataSet::key_query_with_identity_a(); + let b = IdentityChangeDataSet::key_query_with_identity_b(); + let requested_master_key = master_key_json(&requested); + let a_master_key = master_key_json(&a); + + // Change/set their identity pin it, then change it again - this will definitely + // unpin, even if the first identity we supply is their very first, making them + // initially pinned. + if requested_master_key == a_master_key { + self.change_bob_identity(b).await; + if !self.bob_is_pinned().await { + self.pin_bob().await; + } + self.change_bob_identity(a).await; + } else { + self.change_bob_identity(a).await; + if !self.bob_is_pinned().await { + self.pin_bob().await; + } + self.change_bob_identity(b).await; } // Sanity: they are unpinned - assert!(!self.is_pinned().await); + assert!(!self.bob_is_pinned().await); } - pub(super) async fn verify(&self) { - // If they don't have an identity yet, set one up - if self.user_identity().await.is_none() { - self.change_identity(IdentityChangeDataSet::key_query_with_identity_a()).await; - } + pub(super) async fn verify_bob(&self) { + self.verify_bob_with( + IdentityChangeDataSet::key_query_with_identity_a(), + IdentityChangeDataSet::msk_a(), + IdentityChangeDataSet::ssk_a(), + ) + .await; + } + + pub(super) async fn verify_bob_with( + &self, + key_query: KeyQueryResponse, + master_signing_key: serde_json::Value, + self_signing_key: serde_json::Value, + ) { + // Make sure the requested identity is set + self.change_bob_identity(key_query).await; let my_user_id = self.client.user_id().expect("I should have a user id"); let my_identity = self @@ -543,7 +751,7 @@ mod tests { // Get the request let signature_upload_request = self - .crypto_other_identity() + .bob_crypto_other_identity() .await .verify() .await @@ -553,9 +761,9 @@ mod tests { signature_upload_request, my_identity, my_user_id, - self.user_id(), - IdentityChangeDataSet::msk_a(), - IdentityChangeDataSet::ssk_a(), + self.bob_user_id(), + master_signing_key, + self_signing_key, ); // Receive the response into our client @@ -565,15 +773,15 @@ mod tests { .unwrap(); // Sanity: they are verified - assert!(self.is_verified().await); + assert!(self.bob_is_verified().await); } - pub(super) async fn join(&mut self) { - self.membership_change(MembershipState::Join).await; + pub(super) async fn bob_joins(&mut self) { + self.bob_membership_change(MembershipState::Join).await; } - pub(super) async fn leave(&mut self) { - self.membership_change(MembershipState::Leave).await; + pub(super) async fn bob_leaves(&mut self) { + self.bob_membership_change(MembershipState::Leave).await; } pub(super) async fn subscribe_to_identity_status_changes( @@ -601,14 +809,14 @@ mod tests { // Note: if you change the user_id, you will need to change lots of hard-coded // stuff inside IdentityChangeDataSet - let user_id = owned_user_id!("@bob:localhost"); + let bob_user_id = owned_user_id!("@bob:localhost"); let sync_response_builder = SyncResponseBuilder::default(); - (client, user_id, sync_response_builder) + (client, bob_user_id, sync_response_builder) } - async fn change_identity( + async fn change_bob_identity( &self, key_query_response: get_keys::v3::Response, ) -> OtherUserIdentity { @@ -617,15 +825,15 @@ mod tests { .await .expect("Should not fail to send identity changes"); - self.crypto_other_identity().await + self.bob_crypto_other_identity().await } - async fn membership_change(&mut self, new_state: MembershipState) { + async fn bob_membership_change(&mut self, new_state: MembershipState) { let sync_response = self .sync_response_builder .add_joined_room(JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID).add_state_event( StateTestEvent::Custom(sync_response_member( - &self.user_id, + &self.bob_user_id, new_state.clone(), )), )) @@ -635,7 +843,7 @@ mod tests { // Make sure the membership stuck as expected let m = self .room - .get_member_no_sync(&self.user_id) + .get_member_no_sync(&self.bob_user_id) .await .expect("Should not fail to get member"); @@ -650,16 +858,16 @@ mod tests { }; } - async fn is_pinned(&self) -> bool { - !self.crypto_other_identity().await.identity_needs_user_approval() + async fn bob_is_pinned(&self) -> bool { + !self.bob_crypto_other_identity().await.identity_needs_user_approval() } - async fn is_verified(&self) -> bool { - self.crypto_other_identity().await.is_verified() + async fn bob_is_verified(&self) -> bool { + self.bob_crypto_other_identity().await.is_verified() } - async fn crypto_other_identity(&self) -> OtherUserIdentity { - self.user_identity() + async fn bob_crypto_other_identity(&self) -> OtherUserIdentity { + self.bob_user_identity() .await .expect("User identity should exist") .underlying_identity() @@ -667,10 +875,10 @@ mod tests { .expect("Identity should be Other, not Own") } - async fn user_identity(&self) -> Option { + async fn bob_user_identity(&self) -> Option { self.client .encryption() - .get_user_identity(&self.user_id) + .get_user_identity(&self.bob_user_id) .await .expect("Should not fail to get user identity") } From d3d7c03892e007615643ebaa9cfb51b772ef89f7 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 25 Oct 2024 10:43:07 +0100 Subject: [PATCH 360/979] doc(crypto) Update a doc comment on update_user_state_to --- .../src/identities/room_identity_state.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs index d4aa4817a0a..4e07887c7e8 100644 --- a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs +++ b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs @@ -192,9 +192,10 @@ impl RoomIdentityState { } } - // If the supplied `new_state` represents an actual change, updates our internal - // state for this user, and returns the change information we will surface to - // the UI. + /// Updates our internal state for this user to the supplied `new_state`. If + /// the change of state is significant (it requires something to change + /// in the UI, like a warning being added or removed), returns the + /// change information we will surface to the UI. fn update_user_state_to( &mut self, user_id: &UserId, From 40f4fc138b3b1f5ab764bd823be3b9a78a481896 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 25 Oct 2024 14:33:16 +0200 Subject: [PATCH 361/979] chore(room_preview): add `RoomListItem::preview_room` (#4152) This method will return a `RoomPreview` for the provided room id. Also added `fn RoomPreview::leave()` action to be able to decline invites or cancel knocks, since there wasn't a `Client::leave_room_by_id` counterpart as there is for join. The PR also deprecates `RoomListItem::invited_room`, since we have a better alternative now. Co-authored-by: Benjamin Bouvier --- bindings/matrix-sdk-ffi/src/client.rs | 23 ++- bindings/matrix-sdk-ffi/src/error.rs | 9 ++ bindings/matrix-sdk-ffi/src/room.rs | 2 +- bindings/matrix-sdk-ffi/src/room_list.rs | 63 +++++++- bindings/matrix-sdk-ffi/src/room_preview.rs | 121 ++++++++++++---- crates/matrix-sdk-base/src/sync.rs | 6 +- crates/matrix-sdk/src/client/mod.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 6 +- crates/matrix-sdk/src/room_preview.rs | 35 ++++- crates/matrix-sdk/tests/integration/main.rs | 1 + .../tests/integration/room_preview.rs | 137 ++++++++++++++++++ testing/matrix-sdk-test/src/lib.rs | 4 +- .../src/sync_builder/knocked_room.rs | 43 ++++++ .../matrix-sdk-test/src/sync_builder/mod.rs | 23 ++- 14 files changed, 411 insertions(+), 64 deletions(-) create mode 100644 crates/matrix-sdk/tests/integration/room_preview.rs create mode 100644 testing/matrix-sdk-test/src/sync_builder/knocked_room.rs diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index f5f693bae21..f44134fa0ba 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1027,7 +1027,7 @@ impl Client { &self, room_id: String, via_servers: Vec, - ) -> Result { + ) -> Result, ClientError> { let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?; let via_servers = via_servers @@ -1040,16 +1040,16 @@ impl Client { // rustc win that one fight. let room_id: &RoomId = &room_id; - let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?; + let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?; - Ok(RoomPreview::from_sdk(sdk_room_preview)) + Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview))) } /// Given a room alias, get the preview of a room, to interact with it. pub async fn get_room_preview_from_room_alias( &self, room_alias: String, - ) -> Result { + ) -> Result, ClientError> { let room_alias = RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?; @@ -1057,9 +1057,9 @@ impl Client { // rustc win that one fight. let room_alias: &RoomAliasId = &room_alias; - let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?; + let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?; - Ok(RoomPreview::from_sdk(sdk_room_preview)) + Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview))) } /// Waits until an at least partially synced room is received, and returns @@ -1804,7 +1804,7 @@ impl From for SdkOidcPrompt { } /// The rule used for users wishing to join this room. -#[derive(uniffi::Enum)] +#[derive(Debug, Clone, uniffi::Enum)] pub enum JoinRule { /// Anyone can join the room without any prior action. Public, @@ -1830,10 +1830,16 @@ pub enum JoinRule { /// conditions described in a set of [`AllowRule`]s, or they can request /// an invite to the room. KnockRestricted { rules: Vec }, + + /// A custom join rule, up for interpretation by the consumer. + Custom { + /// The string representation for this custom rule. + repr: String, + }, } /// An allow rule which defines a condition that allows joining a room. -#[derive(uniffi::Enum)] +#[derive(Debug, Clone, uniffi::Enum)] pub enum AllowRule { /// Only a member of the `room_id` Room can join the one this rule is used /// in. @@ -1857,6 +1863,7 @@ impl TryFrom for ruma::events::room::join_rules::JoinRule { let rules = allow_rules_from(rules)?; Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules))) } + JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?), } } } diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index 6262b765dc4..e644d7974d8 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -8,6 +8,9 @@ use matrix_sdk::{ }; use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline}; use uniffi::UnexpectedUniFFICallbackError; + +use crate::room_list::RoomListError; + #[derive(Debug, thiserror::Error)] pub enum ClientError { #[error("client error: {msg}")] @@ -128,6 +131,12 @@ impl From for ClientError { } } +impl From for ClientError { + fn from(e: RoomListError) -> Self { + Self::new(e) + } +} + impl From for ClientError { fn from(e: EventCacheError) -> Self { Self::new(e) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index d10aea0e4ba..9f11366a53c 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -45,7 +45,7 @@ use crate::{ TaskHandle, }; -#[derive(Debug, uniffi::Enum)] +#[derive(Debug, Clone, uniffi::Enum)] pub enum Membership { Invited, Joined, diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 5eae181a6c7..27bc20dcc34 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -1,4 +1,12 @@ -use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration}; +#![allow(deprecated)] + +use std::{ + fmt::Debug, + mem::{ManuallyDrop, MaybeUninit}, + ptr::addr_of_mut, + sync::Arc, + time::Duration, +}; use eyeball_im::VectorDiff; use futures_util::{pin_mut, StreamExt, TryFutureExt}; @@ -16,12 +24,14 @@ use matrix_sdk_ui::{ timeline::default_event_filter, unable_to_decrypt_hook::UtdHookManager, }; +use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName}; use tokio::sync::RwLock; use crate::{ error::ClientError, room::{Membership, Room}, room_info::RoomInfo, + room_preview::RoomPreview, timeline::{EventTimelineItem, Timeline}, timeline_event_filter::TimelineEventTypeFilter, TaskHandle, RUNTIME, @@ -48,7 +58,7 @@ pub enum RoomListError { #[error("Event cache ran into an error: {error}")] EventCache { error: String }, #[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")] - IncorrectRoomMembership { expected: Membership, actual: Membership }, + IncorrectRoomMembership { expected: Vec, actual: Membership }, } impl From for RoomListError { @@ -574,23 +584,60 @@ impl RoomListItem { } /// Builds a `Room` FFI from an invited room without initializing its - /// internal timeline + /// internal timeline. /// - /// An error will be returned if the room is a state different than invited + /// An error will be returned if the room is a state different than invited. /// /// ⚠️ Holding on to this room instance after it has been joined is not - /// safe. Use `full_room` instead + /// safe. Use `full_room` instead. + #[deprecated(note = "Please use `preview_room` instead.")] fn invited_room(&self) -> Result, RoomListError> { if !matches!(self.membership(), Membership::Invited) { return Err(RoomListError::IncorrectRoomMembership { - expected: Membership::Invited, + expected: vec![Membership::Invited], actual: self.membership(), }); } - Ok(Arc::new(Room::new(self.inner.inner_room().clone()))) } + /// Builds a `RoomPreview` from a room list item. This is intended for + /// invited or knocked rooms. + /// + /// An error will be returned if the room is in a state other than invited + /// or knocked. + async fn preview_room(&self, via: Vec) -> Result, ClientError> { + // Validate parameters first. + let server_names: Vec = via + .into_iter() + .map(|server| ServerName::parse(server).map_err(ClientError::from)) + .collect::>()?; + + // Validate internal room state. + let membership = self.membership(); + if !matches!(membership, Membership::Invited | Membership::Knocked) { + return Err(RoomListError::IncorrectRoomMembership { + expected: vec![Membership::Invited, Membership::Knocked], + actual: membership, + } + .into()); + } + + // Do the thing. + let client = self.inner.client(); + let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() { + let room_or_alias_id: OwnedRoomOrAliasId = alias.into(); + (room_or_alias_id, Vec::new()) + } else { + let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into(); + (room_or_alias_id, server_names) + }; + + let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?; + + Ok(Arc::new(RoomPreview::new(ManuallyDrop::new(client), room_preview))) + } + /// Build a full `Room` FFI object, filling its associated timeline. /// /// An error will be returned if the room is a state different than joined @@ -598,7 +645,7 @@ impl RoomListItem { fn full_room(&self) -> Result, RoomListError> { if !matches!(self.membership(), Membership::Joined) { return Err(RoomListError::IncorrectRoomMembership { - expected: Membership::Joined, + expected: vec![Membership::Joined], actual: self.membership(), }); } diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 68214bb11ec..08d3b626573 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -1,9 +1,78 @@ -use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState}; +use std::mem::ManuallyDrop; + +use anyhow::Context as _; +use async_compat::TOKIO1 as RUNTIME; +use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client}; use ruma::space::SpaceRoomJoinRule; +use tracing::warn; + +use crate::{client::JoinRule, error::ClientError, room::Membership}; + +/// A room preview for a room. It's intended to be used to represent rooms that +/// aren't joined yet. +#[derive(uniffi::Object)] +pub struct RoomPreview { + inner: SdkRoomPreview, + client: ManuallyDrop, +} + +impl Drop for RoomPreview { + fn drop(&mut self) { + // Dropping the inner OlmMachine must happen within a tokio context + // because deadpool drops sqlite connections in the DB pool on tokio's + // blocking threadpool to avoid blocking async worker threads. + let _guard = RUNTIME.enter(); + // SAFETY: self.client is never used again, which is the only requirement + // for ManuallyDrop::drop to be used safely. + unsafe { + ManuallyDrop::drop(&mut self.client); + } + } +} + +#[matrix_sdk_ffi_macros::export] +impl RoomPreview { + /// Returns the room info the preview contains. + pub fn info(&self) -> Result { + let info = &self.inner; + Ok(RoomPreviewInfo { + room_id: info.room_id.to_string(), + canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()), + name: info.name.clone(), + topic: info.topic.clone(), + avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()), + num_joined_members: info.num_joined_members, + room_type: info.room_type.as_ref().map(|room_type| room_type.to_string()), + is_history_world_readable: info.is_world_readable, + membership: info.state.map(|state| state.into()), + join_rule: info + .join_rule + .clone() + .try_into() + .map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?, + }) + } + + /// Leave the room if the room preview state is either joined, invited or + /// knocked. + /// + /// Will return an error otherwise. + pub async fn leave(&self) -> Result<(), ClientError> { + let room = + self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?; + room.leave().await.map_err(Into::into) + } +} + +impl RoomPreview { + pub(crate) fn new(client: ManuallyDrop, inner: SdkRoomPreview) -> Self { + Self { client, inner } + } +} /// The preview of a room, be it invited/joined/left, or not. #[derive(uniffi::Record)] -pub struct RoomPreview { +pub struct RoomPreviewInfo { /// The room id for this room. pub room_id: String, /// The canonical alias for the room. @@ -20,34 +89,28 @@ pub struct RoomPreview { pub room_type: Option, /// Is the history world-readable for this room? pub is_history_world_readable: bool, - /// Is the room joined by the current user? - pub is_joined: bool, - /// Is the current user invited to this room? - pub is_invited: bool, - /// is the join rule public for this room? - pub is_public: bool, - /// Can we knock (or restricted-knock) to this room? - pub can_knock: bool, + /// The membership state for the current user, if known. + pub membership: Option, + /// The join rule for this room (private, public, knock, etc.). + pub join_rule: JoinRule, } -impl RoomPreview { - pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self { - Self { - room_id: preview.room_id.to_string(), - canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()), - name: preview.name, - topic: preview.topic, - avatar_url: preview.avatar_url.map(|url| url.to_string()), - num_joined_members: preview.num_joined_members, - room_type: preview.room_type.map(|room_type| room_type.to_string()), - is_history_world_readable: preview.is_world_readable, - is_joined: preview.state.map_or(false, |state| state == RoomState::Joined), - is_invited: preview.state.map_or(false, |state| state == RoomState::Invited), - is_public: preview.join_rule == SpaceRoomJoinRule::Public, - can_knock: matches!( - preview.join_rule, - SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock - ), - } +impl TryFrom for JoinRule { + type Error = (); + + fn try_from(join_rule: SpaceRoomJoinRule) -> Result { + Ok(match join_rule { + SpaceRoomJoinRule::Invite => JoinRule::Invite, + SpaceRoomJoinRule::Knock => JoinRule::Knock, + SpaceRoomJoinRule::Private => JoinRule::Private, + SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() }, + SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() }, + SpaceRoomJoinRule::Public => JoinRule::Public, + SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() }, + _ => { + warn!("unhandled SpaceRoomJoinRule: {join_rule}"); + return Err(()); + } + }) } } diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index 4d47cd904ce..36ee7421ef7 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -19,7 +19,7 @@ use std::{collections::BTreeMap, fmt}; use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent}; use ruma::{ api::client::sync::sync_events::{ - v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom}, + v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate}, UnreadNotificationsCount as RumaUnreadNotificationsCount, }, events::{ @@ -78,7 +78,7 @@ pub struct RoomUpdates { /// The rooms that the user has been invited to. pub invite: BTreeMap, /// The rooms that the user has knocked on. - pub knocked: BTreeMap, + pub knocked: BTreeMap, } impl RoomUpdates { @@ -254,7 +254,7 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> { } } -struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap); +struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap); #[cfg(not(tarpaulin_include))] impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> { diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index e2304998034..7066fcba545 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -1009,7 +1009,7 @@ impl Client { }; if let Some(room) = self.get_room(&room_id) { - return Ok(RoomPreview::from_known(&room)); + return Ok(RoomPreview::from_known(&room).await); } RoomPreview::from_unknown(self, room_id, room_or_alias_id, via).await diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 1f237d6d1e1..5561deee315 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -61,7 +61,7 @@ use ruma::{ membership::{ ban_user, forget_room, get_member_events, invite_user::{self, v3::InvitationRecipient}, - join_room_by_id, kick_user, leave_room, unban_user, Invite3pid, + kick_user, leave_room, unban_user, Invite3pid, }, message::send_message_event, read_marker::set_read_marker, @@ -209,9 +209,7 @@ impl Room { false }); - let request = join_room_by_id::v3::Request::new(self.inner.room_id().to_owned()); - let response = self.client.send(request, None).await?; - self.client.base_client().room_joined(&response.room_id).await?; + self.client.join_room_by_id(self.room_id()).await?; if mark_as_direct { self.set_is_direct(true).await?; diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 9b8f1da8469..c102b6225ff 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -32,7 +32,7 @@ use tracing::{instrument, warn}; use crate::{Client, Room}; /// The preview of a room, be it invited/joined/left, or not. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RoomPreview { /// The actual room id for this room. /// @@ -69,6 +69,9 @@ pub struct RoomPreview { /// /// Set to `None` if the room is unknown to the user. pub state: Option, + + /// The `m.room.direct` state of the room, if known. + pub is_direct: Option, } impl RoomPreview { @@ -78,6 +81,7 @@ impl RoomPreview { /// we can do better than that. fn from_room_info( room_info: RoomInfo, + is_direct: Option, num_joined_members: u64, state: Option, ) -> Self { @@ -102,16 +106,23 @@ impl RoomPreview { } }, is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable, - num_joined_members, state, + is_direct, } } /// Create a room preview from a known room (i.e. one we've been invited to, /// we've joined or we've left). - pub(crate) fn from_known(room: &Room) -> Self { - Self::from_room_info(room.clone_info(), room.joined_members_count(), Some(room.state())) + pub(crate) async fn from_known(room: &Room) -> Self { + let is_direct = room.is_direct().await.ok(); + + Self::from_room_info( + room.clone_info(), + is_direct, + room.joined_members_count(), + Some(room.state()), + ) } #[instrument(skip(client))] @@ -160,12 +171,19 @@ impl RoomPreview { // The server returns a `Left` room state for rooms the user has not joined. Be // more precise than that, and set it to `None` if we haven't joined // that room. - let state = if client.get_room(&room_id).is_none() { + let cached_room = client.get_room(&room_id); + let state = if cached_room.is_none() { None } else { response.membership.map(|membership| RoomState::from(&membership)) }; + let is_direct = if let Some(cached_room) = cached_room { + cached_room.is_direct().await.ok() + } else { + None + }; + Ok(RoomPreview { room_id, canonical_alias: response.canonical_alias, @@ -177,6 +195,7 @@ impl RoomPreview { join_rule: response.join_rule, is_world_readable: response.world_readable, state, + is_direct, }) } @@ -217,8 +236,10 @@ impl RoomPreview { room_info.handle_state_event(&ev.into()); } - let state = client.get_room(room_id).map(|room| room.state()); + let room = client.get_room(room_id); + let state = room.as_ref().map(|room| room.state()); + let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None }; - Ok(Self::from_room_info(room_info, num_joined_members, state)) + Ok(Self::from_room_info(room_info, is_direct, num_joined_members, state)) } } diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 9192d0233f9..13cebccde96 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -20,6 +20,7 @@ mod media; mod notification; mod refresh_token; mod room; +mod room_preview; mod send_queue; #[cfg(feature = "experimental-widgets")] mod widget; diff --git a/crates/matrix-sdk/tests/integration/room_preview.rs b/crates/matrix-sdk/tests/integration/room_preview.rs new file mode 100644 index 00000000000..142df0b83ef --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room_preview.rs @@ -0,0 +1,137 @@ +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; +use matrix_sdk_base::RoomState; +use matrix_sdk_test::{ + async_test, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, SyncResponseBuilder, +}; +use ruma::{room_id, space::SpaceRoomJoinRule, RoomId}; +use serde_json::json; +use wiremock::{ + matchers::{header, method, path_regex}, + Mock, MockServer, ResponseTemplate, +}; + +use crate::mock_sync; + +#[async_test] +async fn test_room_preview_leave_invited() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_invited_room(InvitedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + mock_leave(room_id, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert_eq!(room_preview.state.unwrap(), RoomState::Invited); + + client.get_room(room_id).unwrap().leave().await.unwrap(); + + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); +} + +#[async_test] +async fn test_room_preview_leave_knocked() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_knocked_room(KnockedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + mock_leave(room_id, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert_eq!(room_preview.state.unwrap(), RoomState::Knocked); + + let room = client.get_room(room_id).unwrap(); + room.leave().await.unwrap(); + + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); +} + +#[async_test] +async fn test_room_preview_leave_joined() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + mock_leave(room_id, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert_eq!(room_preview.state.unwrap(), RoomState::Joined); + + let room = client.get_room(room_id).unwrap(); + room.leave().await.unwrap(); + + assert_eq!(room.state(), RoomState::Left); +} + +#[async_test] +async fn test_room_preview_leave_unknown_room_fails() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + mock_unknown_summary(room_id, None, SpaceRoomJoinRule::Knock, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert!(room_preview.state.is_none()); + + assert!(client.get_room(room_id).is_none()); +} + +async fn mock_leave(room_id: &RoomId, server: &MockServer) { + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/leave")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "room_id": room_id, + }))) + .mount(server) + .await +} + +async fn mock_unknown_summary( + room_id: &RoomId, + alias: Option, + join_rule: SpaceRoomJoinRule, + server: &MockServer, +) { + let body = if let Some(alias) = alias { + json!({ + "room_id": room_id, + "canonical_alias": alias, + "guest_can_join": true, + "num_joined_members": 1, + "world_readable": true, + "join_rule": join_rule, + }) + } else { + json!({ + "room_id": room_id, + "guest_can_join": true, + "num_joined_members": 1, + "world_readable": true, + "join_rule": join_rule, + }) + }; + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(body)) + .mount(server) + .await +} diff --git a/testing/matrix-sdk-test/src/lib.rs b/testing/matrix-sdk-test/src/lib.rs index 0a7c99e2384..1b06e142d56 100644 --- a/testing/matrix-sdk-test/src/lib.rs +++ b/testing/matrix-sdk-test/src/lib.rs @@ -122,8 +122,8 @@ pub use self::{ event_builder::EventBuilder, sync_builder::{ bulk_room_members, EphemeralTestEvent, GlobalAccountDataTestEvent, InvitedRoomBuilder, - JoinedRoomBuilder, LeftRoomBuilder, PresenceTestEvent, RoomAccountDataTestEvent, - StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, + JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, PresenceTestEvent, + RoomAccountDataTestEvent, StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, }, }; diff --git a/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs b/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs new file mode 100644 index 00000000000..0df9a4e73a6 --- /dev/null +++ b/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs @@ -0,0 +1,43 @@ +use ruma::{ + api::client::sync::sync_events::v3::KnockedRoom, events::AnyStrippedStateEvent, serde::Raw, + OwnedRoomId, RoomId, +}; + +use super::StrippedStateTestEvent; +use crate::DEFAULT_TEST_ROOM_ID; + +pub struct KnockedRoomBuilder { + pub(super) room_id: OwnedRoomId, + pub(super) inner: KnockedRoom, +} + +impl KnockedRoomBuilder { + /// Create a new `KnockedRoomBuilder` for the given room ID. + /// + /// If the room ID is [`DEFAULT_TEST_ROOM_ID`], + /// [`KnockedRoomBuilder::default()`] can be used instead. + pub fn new(room_id: &RoomId) -> Self { + Self { room_id: room_id.to_owned(), inner: Default::default() } + } + + /// Add an event to the state. + pub fn add_state_event(mut self, event: StrippedStateTestEvent) -> Self { + self.inner.knock_state.events.push(event.into_raw_event()); + self + } + + /// Add events to the state in bulk. + pub fn add_state_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.knock_state.events.extend(events); + self + } +} + +impl Default for KnockedRoomBuilder { + fn default() -> Self { + Self::new(&DEFAULT_TEST_ROOM_ID) + } +} diff --git a/testing/matrix-sdk-test/src/sync_builder/mod.rs b/testing/matrix-sdk-test/src/sync_builder/mod.rs index ea6d05bd8cb..919fe6356d6 100644 --- a/testing/matrix-sdk-test/src/sync_builder/mod.rs +++ b/testing/matrix-sdk-test/src/sync_builder/mod.rs @@ -4,7 +4,7 @@ use http::Response; use ruma::{ api::{ client::sync::sync_events::v3::{ - InvitedRoom, JoinedRoom, LeftRoom, Response as SyncResponse, + InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom, Response as SyncResponse, }, IncomingResponse, }, @@ -19,12 +19,14 @@ use super::test_json; mod bulk; mod invited_room; mod joined_room; +mod knocked_room; mod left_room; mod test_event; pub use bulk::bulk_room_members; pub use invited_room::InvitedRoomBuilder; pub use joined_room::JoinedRoomBuilder; +pub use knocked_room::KnockedRoomBuilder; pub use left_room::LeftRoomBuilder; pub use test_event::{ EphemeralTestEvent, GlobalAccountDataTestEvent, PresenceTestEvent, RoomAccountDataTestEvent, @@ -45,6 +47,8 @@ pub struct SyncResponseBuilder { invited_rooms: HashMap, /// Updates to left `Room`s. left_rooms: HashMap, + /// Updates to knocked `Room`s. + knocked_rooms: HashMap, /// Events that determine the presence state of a user. presence: Vec>, /// Global account data events. @@ -68,6 +72,7 @@ impl SyncResponseBuilder { pub fn add_joined_room(&mut self, room: JoinedRoomBuilder) -> &mut Self { self.invited_rooms.remove(&room.room_id); self.left_rooms.remove(&room.room_id); + self.knocked_rooms.remove(&room.room_id); self.joined_rooms.insert(room.room_id, room.inner); self } @@ -79,6 +84,7 @@ impl SyncResponseBuilder { pub fn add_invited_room(&mut self, room: InvitedRoomBuilder) -> &mut Self { self.joined_rooms.remove(&room.room_id); self.left_rooms.remove(&room.room_id); + self.knocked_rooms.remove(&room.room_id); self.invited_rooms.insert(room.room_id, room.inner); self } @@ -90,10 +96,23 @@ impl SyncResponseBuilder { pub fn add_left_room(&mut self, room: LeftRoomBuilder) -> &mut Self { self.joined_rooms.remove(&room.room_id); self.invited_rooms.remove(&room.room_id); + self.knocked_rooms.remove(&room.room_id); self.left_rooms.insert(room.room_id, room.inner); self } + /// Add a knocked room to the next sync response. + /// + /// If a room with the same room ID already exists, it is replaced by this + /// one. + pub fn add_knocked_room(&mut self, room: KnockedRoomBuilder) -> &mut Self { + self.joined_rooms.remove(&room.room_id); + self.invited_rooms.remove(&room.room_id); + self.left_rooms.remove(&room.room_id); + self.knocked_rooms.insert(room.room_id, room.inner); + self + } + /// Add a presence event. pub fn add_presence_event(&mut self, event: PresenceTestEvent) -> &mut Self { let val = match event { @@ -169,6 +188,7 @@ impl SyncResponseBuilder { "invite": self.invited_rooms, "join": self.joined_rooms, "leave": self.left_rooms, + "knock": self.knocked_rooms, }, "to_device": { "events": [] @@ -215,6 +235,7 @@ impl SyncResponseBuilder { self.invited_rooms.clear(); self.joined_rooms.clear(); self.left_rooms.clear(); + self.knocked_rooms.clear(); self.presence.clear(); } } From 7c39fd6ae5b129a676a77c7bc19bc4d93d49ce6c Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 25 Oct 2024 14:14:02 +0100 Subject: [PATCH 362/979] chore(ffi): Expose supported OIDC prompts in the login details. --- bindings/matrix-sdk-ffi/src/authentication.rs | 9 +++- bindings/matrix-sdk-ffi/src/client.rs | 43 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/authentication.rs b/bindings/matrix-sdk-ffi/src/authentication.rs index 8540ca3b644..a2e41a5f9cf 100644 --- a/bindings/matrix-sdk-ffi/src/authentication.rs +++ b/bindings/matrix-sdk-ffi/src/authentication.rs @@ -19,13 +19,14 @@ use matrix_sdk::{ }; use url::Url; -use crate::client::{Client, SlidingSyncVersion}; +use crate::client::{Client, OidcPrompt, SlidingSyncVersion}; #[derive(uniffi::Object)] pub struct HomeserverLoginDetails { pub(crate) url: String, pub(crate) sliding_sync_version: SlidingSyncVersion, pub(crate) supports_oidc_login: bool, + pub(crate) supported_oidc_prompts: Vec, pub(crate) supports_password_login: bool, } @@ -46,6 +47,12 @@ impl HomeserverLoginDetails { self.supports_oidc_login } + /// The prompts advertised by the authentication issuer for use in the login + /// URL. + pub fn supported_oidc_prompts(&self) -> Vec { + self.supported_oidc_prompts.clone() + } + /// Whether the current homeserver supports the password login flow. pub fn supports_password_login(&self) -> bool { self.supports_password_login diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index f44134fa0ba..79bb0bccec7 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -266,7 +266,31 @@ impl Client { impl Client { /// Information about login options for the client's homeserver. pub async fn homeserver_login_details(&self) -> Arc { - let supports_oidc_login = self.inner.oidc().fetch_authentication_issuer().await.is_ok(); + let oidc = self.inner.oidc(); + let (supports_oidc_login, supported_oidc_prompts) = match oidc + .fetch_authentication_issuer() + .await + { + Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await { + Ok(metadata) => { + let prompts = metadata + .prompt_values_supported + .as_ref() + .map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect()); + + (true, prompts) + } + Err(error) => { + error!("Failed to fetch OIDC provider metadata: {error}"); + (true, Default::default()) + } + }, + Err(error) => { + error!("Failed to fetch authentication issuer: {error}"); + (false, Default::default()) + } + }; + let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false); let sliding_sync_version = self.sliding_sync_version(); @@ -274,6 +298,7 @@ impl Client { url: self.homeserver(), sliding_sync_version, supports_oidc_login, + supported_oidc_prompts, supports_password_login, }) } @@ -1758,7 +1783,7 @@ impl TryFrom for SdkSlidingSyncVersion { } } -#[derive(uniffi::Enum)] +#[derive(Clone, uniffi::Enum)] pub enum OidcPrompt { /// The Authorization Server must not display any authentication or consent /// user interface pages. @@ -1790,6 +1815,20 @@ pub enum OidcPrompt { Unknown { value: String }, } +impl From<&SdkOidcPrompt> for OidcPrompt { + fn from(value: &SdkOidcPrompt) -> Self { + match value { + SdkOidcPrompt::None => Self::None, + SdkOidcPrompt::Login => Self::Login, + SdkOidcPrompt::Consent => Self::Consent, + SdkOidcPrompt::SelectAccount => Self::SelectAccount, + SdkOidcPrompt::Create => Self::Create, + SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() }, + _ => Self::Unknown { value: value.to_string() }, + } + } +} + impl From for SdkOidcPrompt { fn from(value: OidcPrompt) -> Self { match value { From ac7bc6461ff211cc9a8277fafd96c8390b4bd19c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 10:37:26 +0200 Subject: [PATCH 363/979] chore(sdk): Add an `Event` type alias for the sake of convenience. This patch adds an `Event` type alias to `SyncTimelineEvent` to (i) make the code shorter, (ii) remove some cognitive effort, (iii) make things more convenient. --- crates/matrix-sdk/src/event_cache/store.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/store.rs b/crates/matrix-sdk/src/event_cache/store.rs index 34565b14193..50a34c32580 100644 --- a/crates/matrix-sdk/src/event_cache/store.rs +++ b/crates/matrix-sdk/src/event_cache/store.rs @@ -18,6 +18,9 @@ use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use super::linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}; +/// An alias for the real event type. +pub(crate) type Event = SyncTimelineEvent; + #[derive(Clone, Debug)] pub struct Gap { /// The token to use in the query, extracted from a previous "from" / @@ -28,7 +31,7 @@ pub struct Gap { const DEFAULT_CHUNK_CAPACITY: usize = 128; pub struct RoomEvents { - chunks: LinkedChunk, + chunks: LinkedChunk, } impl Default for RoomEvents { @@ -52,7 +55,7 @@ impl RoomEvents { /// The last event in `events` is the most recent one. pub fn push_events(&mut self, events: I) where - I: IntoIterator, + I: IntoIterator, I::IntoIter: ExactSizeIterator, { self.chunks.push_items_back(events) @@ -66,7 +69,7 @@ impl RoomEvents { /// Insert events at a specified position. pub fn insert_events_at(&mut self, events: I, position: Position) -> Result<(), Error> where - I: IntoIterator, + I: IntoIterator, I::IntoIter: ExactSizeIterator, { self.chunks.insert_items_at(events, position) @@ -88,9 +91,9 @@ impl RoomEvents { &mut self, events: I, gap_identifier: ChunkIdentifier, - ) -> Result<&Chunk, Error> + ) -> Result<&Chunk, Error> where - I: IntoIterator, + I: IntoIterator, I::IntoIter: ExactSizeIterator, { self.chunks.replace_gap_at(events, gap_identifier) @@ -99,7 +102,7 @@ impl RoomEvents { /// Search for a chunk, and return its identifier. pub fn chunk_identifier<'a, P>(&'a self, predicate: P) -> Option where - P: FnMut(&'a Chunk) -> bool, + P: FnMut(&'a Chunk) -> bool, { self.chunks.chunk_identifier(predicate) } @@ -107,21 +110,21 @@ impl RoomEvents { /// Iterate over the chunks, forward. /// /// The oldest chunk comes first. - pub fn chunks(&self) -> Iter<'_, DEFAULT_CHUNK_CAPACITY, SyncTimelineEvent, Gap> { + pub fn chunks(&self) -> Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> { self.chunks.chunks() } /// Iterate over the events, backward. /// /// The most recent event comes first. - pub fn revents(&self) -> impl Iterator { + pub fn revents(&self) -> impl Iterator { self.chunks.ritems() } /// Iterate over the events, forward. /// /// The oldest event comes first. - pub fn events(&self) -> impl Iterator { + pub fn events(&self) -> impl Iterator { self.chunks.items() } } From cf7cb5c3500cf1c27873a3115da3ae05f28a71fd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 11:15:04 +0200 Subject: [PATCH 364/979] doc(sdk): Add more documentation for `RoomEvents`. --- crates/matrix-sdk/src/event_cache/store.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/matrix-sdk/src/event_cache/store.rs b/crates/matrix-sdk/src/event_cache/store.rs index 50a34c32580..8053afe9867 100644 --- a/crates/matrix-sdk/src/event_cache/store.rs +++ b/crates/matrix-sdk/src/event_cache/store.rs @@ -30,7 +30,9 @@ pub struct Gap { const DEFAULT_CHUNK_CAPACITY: usize = 128; +/// This type represents all events of a single room. pub struct RoomEvents { + /// The real in-memory storage for all the events. chunks: LinkedChunk, } @@ -41,6 +43,7 @@ impl Default for RoomEvents { } impl RoomEvents { + /// Build a new [`RoomEvents`] struct with zero events. pub fn new() -> Self { Self { chunks: LinkedChunk::new() } } From 77b3aa812402ae47077290a8667c3668000d352a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 12:03:22 +0200 Subject: [PATCH 365/979] test(sdk): Test the `RoomEvents`' methods. This patch adds unit tests for the `RoomEvents`' methods. --- .../src/event_cache/linked_chunk/mod.rs | 7 + crates/matrix-sdk/src/event_cache/store.rs | 383 ++++++++++++++++++ 2 files changed, 390 insertions(+) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs index 644ceb09768..ce70c12475d 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs @@ -852,6 +852,13 @@ impl ChunkIdentifierGenerator { #[repr(transparent)] pub struct ChunkIdentifier(u64); +#[cfg(test)] +impl PartialEq for ChunkIdentifier { + fn eq(&self, other: &u64) -> bool { + self.0 == *other + } +} + /// The position of something inside a [`Chunk`]. /// /// It's a pair of a chunk position and an item index. diff --git a/crates/matrix-sdk/src/event_cache/store.rs b/crates/matrix-sdk/src/event_cache/store.rs index 8053afe9867..eb7e771dbd4 100644 --- a/crates/matrix-sdk/src/event_cache/store.rs +++ b/crates/matrix-sdk/src/event_cache/store.rs @@ -137,3 +137,386 @@ impl fmt::Debug for RoomEvents { formatter.debug_struct("RoomEvents").field("chunk", &self.chunks).finish() } } + +#[cfg(test)] +mod tests { + use assert_matches2::assert_let; + use matrix_sdk_test::{EventBuilder, ALICE}; + use ruma::{events::room::message::RoomMessageEventContent, EventId, OwnedEventId}; + + use super::*; + + fn new_event(event_builder: &EventBuilder, event_id: &str) -> (OwnedEventId, Event) { + let event_id = EventId::parse(event_id).unwrap(); + + let event = SyncTimelineEvent::new(event_builder.make_sync_message_event_with_id( + *ALICE, + &event_id, + RoomMessageEventContent::text_plain("foo"), + )); + + (event_id, event) + } + + #[test] + fn test_new_room_events_has_zero_events() { + let room_events = RoomEvents::new(); + + assert_eq!(room_events.chunks.len(), 0); + } + + #[test] + fn test_push_events() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0, event_1]); + room_events.push_events([event_2]); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 1); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 2); + assert_eq!(event.event_id().unwrap(), event_id_2); + + assert!(events.next().is_none()); + } + } + + #[test] + fn test_push_events_with_duplicates() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0.clone()]); + room_events.push_events([event_0]); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 1); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert!(events.next().is_none()); + } + } + + #[test] + fn test_push_gap() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0]); + room_events.push_gap(Gap { prev_token: "hello".to_owned() }); + room_events.push_events([event_1]); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 2); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert!(events.next().is_none()); + } + + { + let mut chunks = room_events.chunks(); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_gap()); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert!(chunks.next().is_none()); + } + } + + #[test] + fn test_insert_events_at() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0, event_1]); + + let position_of_event_1 = room_events + .events() + .find_map(|(position, event)| { + (event.event_id().unwrap() == event_id_1).then_some(position) + }) + .unwrap(); + + room_events.insert_events_at([event_2], position_of_event_1).unwrap(); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 1); + assert_eq!(event.event_id().unwrap(), event_id_2); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 2); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert!(events.next().is_none()); + } + } + + #[test] + fn test_insert_events_at_with_dupicates() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0, event_1.clone()]); + + let position_of_event_1 = room_events + .events() + .find_map(|(position, event)| { + (event.event_id().unwrap() == event_id_1).then_some(position) + }) + .unwrap(); + + room_events.insert_events_at([event_1], position_of_event_1).unwrap(); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 1); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 2); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert!(events.next().is_none()); + } + } + #[test] + fn test_insert_gap_at() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0, event_1]); + + let position_of_event_1 = room_events + .events() + .find_map(|(position, event)| { + (event.event_id().unwrap() == event_id_1).then_some(position) + }) + .unwrap(); + + room_events + .insert_gap_at(Gap { prev_token: "hello".to_owned() }, position_of_event_1) + .unwrap(); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 2); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert!(events.next().is_none()); + } + + { + let mut chunks = room_events.chunks(); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_gap()); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert!(chunks.next().is_none()); + } + } + + #[test] + fn test_replace_gap_at() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0]); + room_events.push_gap(Gap { prev_token: "hello".to_owned() }); + + let chunk_identifier_of_gap = room_events + .chunks() + .find_map(|chunk| chunk.is_gap().then_some(chunk.first_position())) + .unwrap() + .chunk_identifier(); + + room_events.replace_gap_at([event_1, event_2], chunk_identifier_of_gap).unwrap(); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 2); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 2); + assert_eq!(position.index(), 1); + assert_eq!(event.event_id().unwrap(), event_id_2); + + assert!(events.next().is_none()); + } + + { + let mut chunks = room_events.chunks(); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert!(chunks.next().is_none()); + } + } + + #[test] + fn test_replace_gap_at_with_duplicates() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0.clone()]); + room_events.push_gap(Gap { prev_token: "hello".to_owned() }); + + let chunk_identifier_of_gap = room_events + .chunks() + .find_map(|chunk| chunk.is_gap().then_some(chunk.first_position())) + .unwrap() + .chunk_identifier(); + + room_events.replace_gap_at([event_0, event_1], chunk_identifier_of_gap).unwrap(); + + { + let mut events = room_events.events(); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 0); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 2); + assert_eq!(position.index(), 0); + assert_eq!(event.event_id().unwrap(), event_id_0); + + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), 2); + assert_eq!(position.index(), 1); + assert_eq!(event.event_id().unwrap(), event_id_1); + + assert!(events.next().is_none()); + } + + { + let mut chunks = room_events.chunks(); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert_let!(Some(chunk) = chunks.next()); + assert!(chunk.is_items()); + + assert!(chunks.next().is_none()); + } + } +} From b2af1eeb202b57078e5f270a9cf5e6e42f86d966 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:20:30 +0000 Subject: [PATCH 366/979] chore(deps): bump crate-ci/typos from 1.26.0 to 1.26.8 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.26.0 to 1.26.8. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.26.0...v1.26.8) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 320d71a186d..0676374898a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,7 +304,7 @@ jobs: uses: actions/checkout@v4 - name: Check the spelling of the files in our repo - uses: crate-ci/typos@v1.26.0 + uses: crate-ci/typos@v1.26.8 clippy: name: Run clippy From 660a305cfa132abea0e54dfa9f66e4b05a901e28 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 7 Oct 2024 16:41:33 +0300 Subject: [PATCH 367/979] feat(ffi): add support for receiving and working with session verification requests fixup! feat(ffi): add support for receiving and working with session verification requests --- bindings/matrix-sdk-ffi/src/client.rs | 4 +- .../src/session_verification.rs | 100 ++++++++++++++---- 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 79bb0bccec7..5e928e571f5 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -213,10 +213,10 @@ impl Client { let session_verification_controller: Arc< tokio::sync::RwLock>, > = Default::default(); - let ctrl = session_verification_controller.clone(); + let controller = session_verification_controller.clone(); sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move { - if let Some(session_verification_controller) = &*ctrl.clone().read().await { + if let Some(session_verification_controller) = &*controller.clone().read().await { session_verification_controller.process_to_device_message(ev).await; } else { debug!("received to-device message, but verification controller isn't ready"); diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index 655e92d7e5f..b40f951b3b8 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -1,6 +1,5 @@ use std::sync::{Arc, RwLock}; -use anyhow::Context as _; use futures_util::StreamExt; use matrix_sdk::{ encryption::{ @@ -10,6 +9,8 @@ use matrix_sdk::{ }, ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent}, }; +use ruma::UserId; +use tracing::{error, info}; use super::RUNTIME; use crate::error::ClientError; @@ -39,6 +40,7 @@ pub enum SessionVerificationData { #[matrix_sdk_ffi_macros::export(callback_interface)] pub trait SessionVerificationControllerDelegate: Sync + Send { + fn did_receive_verification_request(&self, sender_id: String, flow_id: String); fn did_accept_verification_request(&self); fn did_start_sas_verification(&self); fn did_receive_verification_data(&self, data: SessionVerificationData); @@ -60,17 +62,43 @@ pub struct SessionVerificationController { #[matrix_sdk_ffi_macros::export] impl SessionVerificationController { - pub async fn is_verified(&self) -> Result { - let device = - self.encryption.get_own_device().await?.context("Our own device is missing")?; + pub fn set_delegate(&self, delegate: Option>) { + *self.delegate.write().unwrap() = delegate; + } + + pub async fn accept_verification_request( + &self, + sender_id: String, + flow_id: String, + ) -> Result<(), ClientError> { + let sender_id = UserId::parse(sender_id.clone())?; + + let verification_request = self + .encryption + .get_verification_request(&sender_id, flow_id) + .await + .ok_or(ClientError::new("Unknown session verification request"))?; + + verification_request.accept().await?; + + *self.verification_request.write().unwrap() = Some(verification_request); - Ok(device.is_cross_signed_by_owner()) + Ok(()) } - pub fn set_delegate(&self, delegate: Option>) { - *self.delegate.write().unwrap() = delegate; + /// Accept the previously acknowledged verification request + pub async fn accept_verification_request(&self) -> Result<(), ClientError> { + let verification_request = self.verification_request.read().unwrap().clone(); + + if let Some(verification_request) = verification_request { + let methods = vec![VerificationMethod::SasV1]; + verification_request.accept_with_methods(methods).await?; + } + + Ok(()) } + /// Request verification for the current device pub async fn request_verification(&self) -> Result<(), ClientError> { let methods = vec![VerificationMethod::SasV1]; let verification_request = self @@ -78,6 +106,7 @@ impl SessionVerificationController { .request_verification_with_methods(methods) .await .map_err(anyhow::Error::from)?; + *self.verification_request.write().unwrap() = Some(verification_request); Ok(()) @@ -150,34 +179,61 @@ impl SessionVerificationController { pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) { match event { + AnyToDeviceEvent::KeyVerificationRequest(event) => { + info!("Received verification request: {:}", event.sender); + + let Some(request) = self + .encryption + .get_verification_request(&event.sender, &event.content.transaction_id) + .await + else { + error!("Failed retrieving verification request"); + return; + }; + + if !request.is_self_verification() { + info!("Received non-self verification request. Ignoring."); + return; + } + + if let Some(delegate) = &*self.delegate.read().unwrap() { + delegate.did_receive_verification_request( + request.other_user_id().into(), + request.flow_id().into(), + ); + } + } // TODO: Use the changes stream for this as well once we expose // VerificationRequest::changes() in the main crate. AnyToDeviceEvent::KeyVerificationStart(event) => { if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { return; } - if let Some(verification) = self + + let Some(verification) = self .encryption .get_verification( self.user_identity.user_id(), event.content.transaction_id.as_str(), ) .await - { - if let Some(sas_verification) = verification.sas() { - *self.sas_verification.write().unwrap() = Some(sas_verification.clone()); - - if sas_verification.accept().await.is_ok() { - if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_start_sas_verification() - } - - let delegate = self.delegate.clone(); - RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification)); - } else if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_fail() - } + else { + return; + }; + + let Some(sas_verification) = verification.sas() else { return }; + + *self.sas_verification.write().unwrap() = Some(sas_verification.clone()); + + if sas_verification.accept().await.is_ok() { + if let Some(delegate) = &*self.delegate.read().unwrap() { + delegate.did_start_sas_verification() } + + let delegate = self.delegate.clone(); + RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification)); + } else if let Some(delegate) = &*self.delegate.read().unwrap() { + delegate.did_fail() } } AnyToDeviceEvent::KeyVerificationReady(event) => { From 8cf0716db2f96d7b393f8388d67fff0b5977fafa Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 9 Oct 2024 14:16:03 +0300 Subject: [PATCH 368/979] refactor(ffi): switch to using `VerificationRequest::changes` instead of direct to_device events. --- .../src/session_verification.rs | 93 ++++++++++--------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index b40f951b3b8..2e173f60778 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -4,7 +4,7 @@ use futures_util::StreamExt; use matrix_sdk::{ encryption::{ identities::UserIdentity, - verification::{SasState, SasVerification, VerificationRequest}, + verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState}, Encryption, }, ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent}, @@ -107,7 +107,13 @@ impl SessionVerificationController { .await .map_err(anyhow::Error::from)?; - *self.verification_request.write().unwrap() = Some(verification_request); + *self.verification_request.write().unwrap() = Some(verification_request.clone()); + + RUNTIME.spawn(Self::listen_to_verification_request_changes( + verification_request, + self.sas_verification.clone(), + self.delegate.clone(), + )); Ok(()) } @@ -125,7 +131,7 @@ impl SessionVerificationController { } let delegate = self.delegate.clone(); - RUNTIME.spawn(Self::listen_to_changes(delegate, verification)); + RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate)); } _ => { if let Some(delegate) = &*self.delegate.read().unwrap() { @@ -203,60 +209,57 @@ impl SessionVerificationController { ); } } - // TODO: Use the changes stream for this as well once we expose - // VerificationRequest::changes() in the main crate. - AnyToDeviceEvent::KeyVerificationStart(event) => { - if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { - return; - } + _ => (), + } + } - let Some(verification) = self - .encryption - .get_verification( - self.user_identity.user_id(), - event.content.transaction_id.as_str(), - ) - .await - else { - return; - }; + async fn listen_to_verification_request_changes( + verification_request: VerificationRequest, + sas_verification: Arc>>, + delegate: Delegate, + ) { + let mut stream = verification_request.changes(); - let Some(sas_verification) = verification.sas() else { return }; + while let Some(state) = stream.next().await { + match state { + VerificationRequestState::Transitioned { verification } => { + let Some(verification) = verification.sas() else { + error!("Invalid, non-sas verification flow. Returning."); + return; + }; - *self.sas_verification.write().unwrap() = Some(sas_verification.clone()); + *sas_verification.write().unwrap() = Some(verification.clone()); - if sas_verification.accept().await.is_ok() { - if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_start_sas_verification() - } + if verification.accept().await.is_ok() { + if let Some(delegate) = &*delegate.read().unwrap() { + delegate.did_start_sas_verification() + } - let delegate = self.delegate.clone(); - RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification)); - } else if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_fail() + let delegate = delegate.clone(); + RUNTIME.spawn(Self::listen_to_sas_verification_changes( + verification, + delegate, + )); + } else if let Some(delegate) = &*delegate.read().unwrap() { + delegate.did_fail() + } } - } - AnyToDeviceEvent::KeyVerificationReady(event) => { - if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { - return; + VerificationRequestState::Ready { .. } => { + if let Some(delegate) = &*delegate.read().unwrap() { + delegate.did_accept_verification_request() + } } - - if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_accept_verification_request() + VerificationRequestState::Cancelled(..) => { + if let Some(delegate) = &*delegate.read().unwrap() { + delegate.did_cancel(); + } } + _ => {} } - _ => (), - } - } - - fn is_transaction_id_valid(&self, transaction_id: String) -> bool { - match &*self.verification_request.read().unwrap() { - Some(verification) => verification.flow_id() == transaction_id, - None => false, } } - async fn listen_to_changes(delegate: Delegate, sas: SasVerification) { + async fn listen_to_sas_verification_changes(sas: SasVerification, delegate: Delegate) { let mut stream = sas.changes(); while let Some(state) = stream.next().await { From 3a34b03726c3d9bdbb3570ff5a20de41624bb4e2 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 9 Oct 2024 15:22:22 +0300 Subject: [PATCH 369/979] Expose mechanism for registering to verification updates before actually accepting one - allows handling remote cancellations on verification requests that have not yet been accepted --- .../src/session_verification.rs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index 2e173f60778..7f51c1d740d 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -66,11 +66,10 @@ impl SessionVerificationController { *self.delegate.write().unwrap() = delegate; } - pub async fn accept_verification_request( - &self, - sender_id: String, - flow_id: String, - ) -> Result<(), ClientError> { + /// Set this particular request as the currently active one and register for events pertaining it. + /// * `sender_id` - The user requesting verification. + /// * `flow_id` - - The ID that uniquely identifies the verification flow. + pub async fn acknowledge_verification_request(&self, sender_id: String, flow_id: String) { let sender_id = UserId::parse(sender_id.clone())?; let verification_request = self @@ -79,9 +78,24 @@ impl SessionVerificationController { .await .ok_or(ClientError::new("Unknown session verification request"))?; - verification_request.accept().await?; + *self.verification_request.write().unwrap() = Some(verification_request.clone()); + + RUNTIME.spawn(Self::listen_to_verification_request_changes( + verification_request, + self.sas_verification.clone(), + self.delegate.clone(), + )); + + Ok(()) + } + + /// Accept the previously acknowledged verification request + pub async fn accept_verification_request(&self) -> Result<(), ClientError> { + let verification_request = self.verification_request.read().unwrap().clone(); - *self.verification_request.write().unwrap() = Some(verification_request); + if let Some(verification_request) = verification_request { + verification_request.accept().await?; + } Ok(()) } From 6455585f1eddc0ad7a2629b69a734741b6b4c2ab Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 9 Oct 2024 15:32:33 +0300 Subject: [PATCH 370/979] Documentation + cleanup --- .../src/session_verification.rs | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index 7f51c1d740d..fa6b56f77de 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -69,7 +69,11 @@ impl SessionVerificationController { /// Set this particular request as the currently active one and register for events pertaining it. /// * `sender_id` - The user requesting verification. /// * `flow_id` - - The ID that uniquely identifies the verification flow. - pub async fn acknowledge_verification_request(&self, sender_id: String, flow_id: String) { + pub async fn acknowledge_verification_request( + &self, + sender_id: String, + flow_id: String, + ) -> Result<(), ClientError> { let sender_id = UserId::parse(sender_id.clone())?; let verification_request = self @@ -89,17 +93,6 @@ impl SessionVerificationController { Ok(()) } - /// Accept the previously acknowledged verification request - pub async fn accept_verification_request(&self) -> Result<(), ClientError> { - let verification_request = self.verification_request.read().unwrap().clone(); - - if let Some(verification_request) = verification_request { - verification_request.accept().await?; - } - - Ok(()) - } - /// Accept the previously acknowledged verification request pub async fn accept_verification_request(&self) -> Result<(), ClientError> { let verification_request = self.verification_request.read().unwrap().clone(); @@ -132,25 +125,28 @@ impl SessionVerificationController { Ok(()) } + /// Transition the current verification request into a SAS verification flow. pub async fn start_sas_verification(&self) -> Result<(), ClientError> { let verification_request = self.verification_request.read().unwrap().clone(); - if let Some(verification) = verification_request { - match verification.start_sas().await { - Ok(Some(verification)) => { - *self.sas_verification.write().unwrap() = Some(verification.clone()); + let Some(verification_request) = verification_request else { + return Err(ClientError::new("Verification request missing.")); + }; - if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_start_sas_verification() - } + match verification_request.start_sas().await { + Ok(Some(verification)) => { + *self.sas_verification.write().unwrap() = Some(verification.clone()); - let delegate = self.delegate.clone(); - RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate)); + if let Some(delegate) = &*self.delegate.read().unwrap() { + delegate.did_start_sas_verification() } - _ => { - if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_fail() - } + + let delegate = self.delegate.clone(); + RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate)); + } + _ => { + if let Some(delegate) = &*self.delegate.read().unwrap() { + delegate.did_fail() } } } @@ -158,31 +154,37 @@ impl SessionVerificationController { Ok(()) } + /// Confirm that the short auth strings match on both sides. pub async fn approve_verification(&self) -> Result<(), ClientError> { let sas_verification = self.sas_verification.read().unwrap().clone(); - if let Some(sas_verification) = sas_verification { - sas_verification.confirm().await?; - } - Ok(()) + let Some(sas_verification) = sas_verification else { + return Err(ClientError::new("SAS verification missing")); + }; + + Ok(sas_verification.confirm().await?) } + /// Reject the short auth string pub async fn decline_verification(&self) -> Result<(), ClientError> { let sas_verification = self.sas_verification.read().unwrap().clone(); - if let Some(sas_verification) = sas_verification { - sas_verification.mismatch().await?; - } - Ok(()) + let Some(sas_verification) = sas_verification else { + return Err(ClientError::new("SAS verification missing")); + }; + + Ok(sas_verification.mismatch().await?) } + /// Cancel the current verification request pub async fn cancel_verification(&self) -> Result<(), ClientError> { let verification_request = self.verification_request.read().unwrap().clone(); - if let Some(verification) = verification_request { - verification.cancel().await?; - } - Ok(()) + let Some(verification_request) = verification_request else { + return Err(ClientError::new("Verification request missing.")); + }; + + Ok(verification_request.cancel().await?) } } From f771eec3c56ab4635aef25c1b68acebe1f971e6f Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 10 Oct 2024 12:14:43 +0300 Subject: [PATCH 371/979] Fix a clippy warning re single matching --- .../src/session_verification.rs | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index fa6b56f77de..5b65b2c4436 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -200,32 +200,29 @@ impl SessionVerificationController { } pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) { - match event { - AnyToDeviceEvent::KeyVerificationRequest(event) => { - info!("Received verification request: {:}", event.sender); - - let Some(request) = self - .encryption - .get_verification_request(&event.sender, &event.content.transaction_id) - .await - else { - error!("Failed retrieving verification request"); - return; - }; - - if !request.is_self_verification() { - info!("Received non-self verification request. Ignoring."); - return; - } + if let AnyToDeviceEvent::KeyVerificationRequest(event) = event { + info!("Received verification request: {:}", event.sender); + + let Some(request) = self + .encryption + .get_verification_request(&event.sender, &event.content.transaction_id) + .await + else { + error!("Failed retrieving verification request"); + return; + }; + + if !request.is_self_verification() { + info!("Received non-self verification request. Ignoring."); + return; + } - if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_receive_verification_request( - request.other_user_id().into(), - request.flow_id().into(), - ); - } + if let Some(delegate) = &*self.delegate.read().unwrap() { + delegate.did_receive_verification_request( + request.other_user_id().into(), + request.flow_id().into(), + ); } - _ => (), } } From 35dabf73461b3b716ae33bc406c0920a6663a54e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 10 Oct 2024 12:14:11 +0300 Subject: [PATCH 372/979] feat(crypto): store a copy of the requesting `DeviceData` within `VerificationRequestState`s --- .../src/verification/machine.rs | 8 +++ .../src/verification/requests.rs | 68 ++++++++++++++----- .../src/encryption/verification/requests.rs | 12 ++-- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index c82d3b7867f..19033209e6e 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -370,12 +370,20 @@ impl VerificationMachine { return Ok(()); } + let Some(device_data) = + self.store.get_device(event.sender(), r.from_device()).await? + else { + warn!("Could not retrieve the device data for the incoming verification request, ignoring it"); + return Ok(()); + }; + let request = VerificationRequest::from_request( self.verifications.clone(), self.store.clone(), event.sender(), flow_id, r, + device_data, ); self.insert_request(request); diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 94c2b599d59..9c1c4e202be 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -52,8 +52,8 @@ use super::{ CancelInfo, Cancelled, FlowId, Verification, VerificationStore, }; use crate::{ - olm::StaticAccountData, CryptoStoreError, OutgoingVerificationRequest, RoomMessageRequest, Sas, - ToDeviceRequest, + olm::StaticAccountData, CryptoStoreError, DeviceData, OutgoingVerificationRequest, + RoomMessageRequest, Sas, ToDeviceRequest, }; const SUPPORTED_METHODS: &[VerificationMethod] = &[ @@ -78,9 +78,9 @@ pub enum VerificationRequestState { /// The verification methods supported by the sender. their_methods: Vec, - /// The device ID of the device that responded to the verification + /// The device data of the device that responded to the verification /// request. - other_device_id: OwnedDeviceId, + other_device_data: DeviceData, }, /// The verification request is ready to start a verification flow. Ready { @@ -116,7 +116,7 @@ impl From<&InnerRequest> for VerificationRequestState { } InnerRequest::Requested(s) => Self::Requested { their_methods: s.state.their_methods.to_owned(), - other_device_id: s.state.other_device_id.to_owned(), + other_device_data: s.state.other_device_data.to_owned(), }, InnerRequest::Ready(s) => Self::Ready { their_methods: s.state.their_methods.to_owned(), @@ -281,7 +281,7 @@ impl VerificationRequest { /// The id of the other device that is participating in this verification. pub fn other_device_id(&self) -> Option { match &*self.inner.read() { - InnerRequest::Requested(r) => Some(r.state.other_device_id.to_owned()), + InnerRequest::Requested(r) => Some(r.state.other_device_data.device_id().to_owned()), InnerRequest::Ready(r) => Some(r.state.other_device_id.to_owned()), InnerRequest::Transitioned(r) => Some(r.state.ready.other_device_id.to_owned()), InnerRequest::Created(_) @@ -466,13 +466,21 @@ impl VerificationRequest { sender: &UserId, flow_id: FlowId, content: &RequestContent<'_>, + device_data: DeviceData, ) -> Self { let account = store.account.clone(); Self { verification_cache: cache.clone(), inner: SharedObservable::new(InnerRequest::Requested( - RequestState::from_request_event(cache, store, sender, &flow_id, content), + RequestState::from_request_event( + cache, + store, + sender, + &flow_id, + content, + device_data, + ), )), account, other_user_id: sender.into(), @@ -889,7 +897,7 @@ impl InnerRequest { match self { InnerRequest::Created(_) => DeviceIdOrAllDevices::AllDevices, InnerRequest::Requested(r) => { - DeviceIdOrAllDevices::DeviceId(r.state.other_device_id.to_owned()) + DeviceIdOrAllDevices::DeviceId(r.state.other_device_data.device_id().to_owned()) } InnerRequest::Ready(r) => { DeviceIdOrAllDevices::DeviceId(r.state.other_device_id.to_owned()) @@ -1061,8 +1069,9 @@ struct Requested { /// The verification methods supported by the sender. pub their_methods: Vec, - /// The device ID of the device that responded to the verification request. - pub other_device_id: OwnedDeviceId, + /// The device data of the device that responded to the verification + /// request. + pub other_device_data: DeviceData, } impl RequestState { @@ -1072,6 +1081,7 @@ impl RequestState { sender: &UserId, flow_id: &FlowId, content: &RequestContent<'_>, + device_data: DeviceData, ) -> RequestState { // TODO only create this if we support the methods RequestState { @@ -1081,7 +1091,7 @@ impl RequestState { other_user_id: sender.to_owned(), state: Requested { their_methods: content.methods().to_owned(), - other_device_id: content.from_device().into(), + other_device_data: device_data, }, } } @@ -1105,7 +1115,7 @@ impl RequestState { state: Ready { their_methods: self.state.their_methods, our_methods: methods.clone(), - other_device_id: self.state.other_device_id.clone(), + other_device_id: self.state.other_device_data.device_id().to_owned(), }, }; @@ -1652,6 +1662,12 @@ mod tests { None, ); + let device_data = alice_store + .get_device(&bob_store.account.user_id, &bob_store.account.device_id) + .await + .unwrap() + .expect("Missing device data"); + let flow_id = FlowId::InRoom(room_id, event_id); let bob_request = VerificationRequest::new( @@ -1674,6 +1690,7 @@ mod tests { bob_id(), flow_id, &(&content).into(), + device_data, ); assert_matches!(alice_request.state(), VerificationRequestState::Requested { .. }); @@ -1698,7 +1715,7 @@ mod tests { // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), None); - let alice_request = build_incoming_verification_request(&alice_store, &bob_request); + let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; let outgoing_request = alice_request.cancel().unwrap(); @@ -1740,6 +1757,13 @@ mod tests { alice_id(), None, ); + + let device_data = alice_store + .get_device(&bob_store.account.user_id, &bob_store.account.device_id) + .await + .unwrap() + .expect("Missing device data"); + let flow_id = FlowId::from((room_id, event_id)); let bob_request = VerificationRequest::new( @@ -1758,6 +1782,7 @@ mod tests { bob_id(), flow_id, &(&content).into(), + device_data, ); do_accept_request(&alice_request, &bob_request, None); @@ -1791,7 +1816,7 @@ mod tests { // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), None); - let alice_request = build_incoming_verification_request(&alice_store, &bob_request); + let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; do_accept_request(&alice_request, &bob_request, None); let (bob_sas, request) = bob_request.start_sas().await.unwrap().unwrap(); @@ -1829,7 +1854,7 @@ mod tests { alice_id(), Some(vec![VerificationMethod::QrCodeScanV1, VerificationMethod::QrCodeShowV1]), ); - let alice_request = build_incoming_verification_request(&alice_store, &bob_request); + let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; do_accept_request( &alice_request, &bob_request, @@ -1876,7 +1901,7 @@ mod tests { // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), Some(all_methods())); - let alice_request = build_incoming_verification_request(&alice_store, &bob_request); + let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; do_accept_request(&alice_request, &bob_request, Some(all_methods())); // Each side can start its own QR verification flow by generating QR code @@ -1921,7 +1946,7 @@ mod tests { // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), Some(all_methods())); - let alice_request = build_incoming_verification_request(&alice_store, &bob_request); + let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; do_accept_request(&alice_request, &bob_request, Some(all_methods())); // Bob generates a QR code @@ -1996,7 +2021,7 @@ mod tests { /// Tells the outgoing request to generate an `m.key.verification.request` /// to-device message, and uses it to build a new request for the incoming /// side. - fn build_incoming_verification_request( + async fn build_incoming_verification_request( verification_store: &VerificationStore, outgoing_request: &VerificationRequest, ) -> VerificationRequest { @@ -2004,12 +2029,19 @@ mod tests { let content: OutgoingContent = request.try_into().unwrap(); let content = RequestContent::try_from(&content).unwrap(); + let device_data = verification_store + .get_device(outgoing_request.own_user_id(), content.from_device()) + .await + .unwrap() + .expect("Missing device data"); + VerificationRequest::from_request( VerificationCache::new(), verification_store.clone(), outgoing_request.own_user_id(), outgoing_request.flow_id().clone(), &content, + device_data, ) } diff --git a/crates/matrix-sdk/src/encryption/verification/requests.rs b/crates/matrix-sdk/src/encryption/verification/requests.rs index 30e56f1f177..0e6fb35f510 100644 --- a/crates/matrix-sdk/src/encryption/verification/requests.rs +++ b/crates/matrix-sdk/src/encryption/verification/requests.rs @@ -13,7 +13,9 @@ // limitations under the License. use futures_util::{Stream, StreamExt}; -use matrix_sdk_base::crypto::{CancelInfo, VerificationRequest as BaseVerificationRequest}; +use matrix_sdk_base::crypto::{ + CancelInfo, DeviceData, VerificationRequest as BaseVerificationRequest, +}; use ruma::{events::key::verification::VerificationMethod, OwnedDeviceId, RoomId}; #[cfg(feature = "qrcode")] @@ -41,9 +43,9 @@ pub enum VerificationRequestState { /// The verification methods supported by the sender. their_methods: Vec, - /// The device ID of the device that responded to the verification + /// The device data of the device that responded to the verification /// request. - other_device_id: OwnedDeviceId, + other_device_data: DeviceData, }, /// The verification request is ready to start a verification flow. Ready { @@ -218,8 +220,8 @@ impl VerificationRequest { match state { Created { our_methods } => VerificationRequestState::Created { our_methods }, - Requested { their_methods, other_device_id } => { - VerificationRequestState::Requested { their_methods, other_device_id } + Requested { their_methods, other_device_data } => { + VerificationRequestState::Requested { their_methods, other_device_data } } Ready { their_methods, our_methods, other_device_id } => { VerificationRequestState::Ready { their_methods, our_methods, other_device_id } From bb8b0cf6b9bfebff0b2d0d1fddb3b585da28c368 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 10 Oct 2024 18:44:22 +0300 Subject: [PATCH 373/979] Expose requesting device details to the final client --- .../src/session_verification.rs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index 5b65b2c4436..6cc4dea8b76 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -38,9 +38,20 @@ pub enum SessionVerificationData { Decimals { values: Vec }, } +/// Details about the incoming verification request +#[derive(Debug, uniffi::Record)] +pub struct SessionVerificationRequestDetails { + sender_id: String, + flow_id: String, + device_id: String, + display_name: Option, + /// First time this device was seen in milliseconds since epoch. + first_seen_timestamp: u64, +} + #[matrix_sdk_ffi_macros::export(callback_interface)] pub trait SessionVerificationControllerDelegate: Sync + Send { - fn did_receive_verification_request(&self, sender_id: String, flow_id: String); + fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails); fn did_accept_verification_request(&self); fn did_start_sas_verification(&self); fn did_receive_verification_data(&self, data: SessionVerificationData); @@ -66,7 +77,8 @@ impl SessionVerificationController { *self.delegate.write().unwrap() = delegate; } - /// Set this particular request as the currently active one and register for events pertaining it. + /// Set this particular request as the currently active one and register for + /// events pertaining it. /// * `sender_id` - The user requesting verification. /// * `flow_id` - - The ID that uniquely identifies the verification flow. pub async fn acknowledge_verification_request( @@ -125,7 +137,8 @@ impl SessionVerificationController { Ok(()) } - /// Transition the current verification request into a SAS verification flow. + /// Transition the current verification request into a SAS verification + /// flow. pub async fn start_sas_verification(&self) -> Result<(), ClientError> { let verification_request = self.verification_request.read().unwrap().clone(); @@ -217,11 +230,20 @@ impl SessionVerificationController { return; } + let VerificationRequestState::Requested { other_device_data, .. } = request.state() + else { + error!("Received key verification event but the request is in the wrong state."); + return; + }; + if let Some(delegate) = &*self.delegate.read().unwrap() { - delegate.did_receive_verification_request( - request.other_user_id().into(), - request.flow_id().into(), - ); + delegate.did_receive_verification_request(SessionVerificationRequestDetails { + sender_id: request.other_user_id().into(), + flow_id: request.flow_id().into(), + device_id: other_device_data.device_id().into(), + display_name: other_device_data.display_name().map(str::to_string), + first_seen_timestamp: other_device_data.first_time_seen_ts().get().into(), + }); } } } From 8469cb11468e9bd8dd91b7ef713b7705bc36e595 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 15 Oct 2024 17:13:27 +0300 Subject: [PATCH 374/979] fix(crypto): fix incorrect `VerificationMachine` tests - the tests used to incorrectly wrap the to-device content into an event as if it was sent by alice instead of bob --- .../src/verification/machine.rs | 17 +++++++++-------- .../matrix-sdk-crypto/src/verification/mod.rs | 2 +- .../src/verification/sas/mod.rs | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index 19033209e6e..864800f1a97 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -341,6 +341,7 @@ impl VerificationMachine { match &content { AnyVerificationContent::Request(r) => { info!( + sender = ?event.sender(), from_device = r.from_device().as_str(), "Received a new verification request", ); @@ -733,12 +734,12 @@ mod tests { let content: OutgoingContent = request.try_into().unwrap(); machine - .receive_any_event(&wrap_any_to_device_content(bob_request.other_user(), content)) + .receive_any_event(&wrap_any_to_device_content(bob_request.own_user_id(), content)) .await .unwrap(); let alice_request = - machine.get_request(bob_request.other_user(), bob_request.flow_id().as_str()).unwrap(); + machine.get_request(bob_request.own_user_id(), bob_request.flow_id().as_str()).unwrap(); // We're not yet cancelled. assert!(!alice_request.is_cancelled()); @@ -757,12 +758,12 @@ mod tests { let content: OutgoingContent = request.try_into().unwrap(); machine - .receive_any_event(&wrap_any_to_device_content(bob_request.other_user(), content)) + .receive_any_event(&wrap_any_to_device_content(bob_request.own_user_id(), content)) .await .unwrap(); let second_request = - machine.get_request(bob_request.other_user(), bob_request.flow_id().as_str()).unwrap(); + machine.get_request(bob_request.own_user_id(), bob_request.flow_id().as_str()).unwrap(); // Make sure we fetched the new one. assert_eq!(second_request.flow_id().as_str(), second_transaction_id); @@ -795,12 +796,12 @@ mod tests { let content: OutgoingContent = request.try_into().unwrap(); machine - .receive_any_event(&wrap_any_to_device_content(bob_request.other_user(), content)) + .receive_any_event(&wrap_any_to_device_content(bob_request.own_user_id(), content)) .await .unwrap(); let first_request = - machine.get_request(bob_request.other_user(), bob_request.flow_id().as_str()).unwrap(); + machine.get_request(bob_request.own_user_id(), bob_request.flow_id().as_str()).unwrap(); // We're not yet cancelled. assert!(!first_request.is_cancelled()); @@ -819,12 +820,12 @@ mod tests { let content: OutgoingContent = request.try_into().unwrap(); machine - .receive_any_event(&wrap_any_to_device_content(bob_request.other_user(), content)) + .receive_any_event(&wrap_any_to_device_content(bob_request.own_user_id(), content)) .await .unwrap(); let second_request = - machine.get_request(bob_request.other_user(), bob_request.flow_id().as_str()).unwrap(); + machine.get_request(bob_request.own_user_id(), bob_request.flow_id().as_str()).unwrap(); // None of the requests are cancelled assert!(!first_request.is_cancelled()); diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 6d48d38e684..1598114fb06 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -819,7 +819,7 @@ pub(crate) mod tests { } pub fn bob_device_id() -> &'static DeviceId { - device_id!("BOBDEVCIE") + device_id!("BOBDEVICE") } pub(crate) async fn setup_stores() -> (Account, VerificationStore, Account, VerificationStore) { diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index c9a3acf961f..caf43a249ab 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -899,7 +899,7 @@ mod tests { } fn bob_device_id() -> &'static DeviceId { - device_id!("BOBDEVCIE") + device_id!("BOBDEVICE") } fn machine_pair_test_helper() -> (VerificationStore, DeviceData, VerificationStore, DeviceData) From d31f5b2a7286dde61b8e14b6960153b451ec1887 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 21 Oct 2024 12:40:33 +0300 Subject: [PATCH 375/979] chore(tests): fix verification integration tests following changes to the data associated with `VerificationRequestState::Requested` --- testing/matrix-sdk-integration-testing/src/tests/e2ee.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs index 233dbc49e7d..1c72ae8c17a 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs @@ -70,6 +70,7 @@ async fn test_mutual_sas_verification() -> Result<()> { bob.get_room(room_id).unwrap().join().await?; alice.sync_once().await?; + bob.sync_once().await?; warn!("alice and bob are both aware of each other in the e2ee room"); @@ -331,6 +332,7 @@ async fn test_mutual_qrcode_verification() -> Result<()> { bob.get_room(room_id).unwrap().join().await?; alice.sync_once().await?; + bob.sync_once().await?; warn!("alice and bob are both aware of each other in the e2ee room"); @@ -841,6 +843,9 @@ async fn test_secret_gossip_after_interactive_verification() -> Result<()> { // The first client is not verified from the point of view of the second client. assert!(!seconds_first_device.is_verified()); + // Make the first client aware of the device we're requesting verification for + first_client.sync_once().await?; + // Let's send out a request to verify with each other. let seconds_verification_request = seconds_first_device.request_verification().await?; let flow_id = seconds_verification_request.flow_id(); From 8492968792649dc91460ee23c17a5e7df013ceaf Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 24 Oct 2024 09:49:51 +0300 Subject: [PATCH 376/979] Pass a copy of the other `DeviceData` in between the (Requested, Ready) and (Created, Ready) states --- .../matrix-sdk-crypto-ffi/src/verification.rs | 2 +- .../src/verification/machine.rs | 8 +- .../src/verification/requests.rs | 162 +++++++++--------- .../src/encryption/verification/requests.rs | 10 +- 4 files changed, 90 insertions(+), 92 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/verification.rs b/bindings/matrix-sdk-crypto-ffi/src/verification.rs index e8c31c4a3a8..c522a1ffa16 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/verification.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/verification.rs @@ -752,7 +752,7 @@ impl VerificationRequest { RustVerificationRequestState::Ready { their_methods, our_methods, - other_device_id: _, + other_device_data: _, } => VerificationRequestState::Ready { their_methods: their_methods.iter().map(|m| m.to_string()).collect(), our_methods: our_methods.iter().map(|m| m.to_string()).collect(), diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index 864800f1a97..892fa255183 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -412,7 +412,13 @@ impl VerificationMachine { }; if request.flow_id() == &flow_id { - request.receive_ready(event.sender(), c); + if let Some(device_data) = + self.store.get_device(event.sender(), c.from_device()).await? + { + request.receive_ready(event.sender(), c, device_data); + } else { + warn!("Could not retrieve the data for the accepting device, ignoring it"); + } } else { flow_id_mismatch(); } diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 9c1c4e202be..7b8d7a27338 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -90,9 +90,9 @@ pub enum VerificationRequestState { /// The verification methods supported by the us. our_methods: Vec, - /// The device ID of the device that responded to the verification + /// The device data of the device that responded to the verification /// request. - other_device_id: OwnedDeviceId, + other_device_data: DeviceData, }, /// The verification request has transitioned into a concrete verification /// flow. For example it transitioned into the emoji based SAS @@ -121,7 +121,7 @@ impl From<&InnerRequest> for VerificationRequestState { InnerRequest::Ready(s) => Self::Ready { their_methods: s.state.their_methods.to_owned(), our_methods: s.state.our_methods.to_owned(), - other_device_id: s.state.other_device_id.to_owned(), + other_device_data: s.state.other_device_data.to_owned(), }, InnerRequest::Transitioned(s) => { Self::Transitioned { verification: s.state.verification.to_owned() } @@ -282,8 +282,10 @@ impl VerificationRequest { pub fn other_device_id(&self) -> Option { match &*self.inner.read() { InnerRequest::Requested(r) => Some(r.state.other_device_data.device_id().to_owned()), - InnerRequest::Ready(r) => Some(r.state.other_device_id.to_owned()), - InnerRequest::Transitioned(r) => Some(r.state.ready.other_device_id.to_owned()), + InnerRequest::Ready(r) => Some(r.state.other_device_data.device_id().to_owned()), + InnerRequest::Transitioned(r) => { + Some(r.state.ready.other_device_data.device_id().to_owned()) + } InnerRequest::Created(_) | InnerRequest::Passive(_) | InnerRequest::Done(_) @@ -666,12 +668,18 @@ impl VerificationRequest { Some(ToDeviceRequest::for_recipients(recipient, recip_devices, &c, TransactionId::new())) } - pub(crate) fn receive_ready(&self, sender: &UserId, content: &ReadyContent<'_>) { + pub(crate) fn receive_ready( + &self, + sender: &UserId, + content: &ReadyContent<'_>, + from_device_data: DeviceData, + ) { let mut guard = self.inner.write(); match &*guard { InnerRequest::Created(s) => { - let new_value = InnerRequest::Ready(s.clone().into_ready(sender, content)); + let new_value = + InnerRequest::Ready(s.clone().into_ready(sender, content, from_device_data)); ObservableWriteGuard::set(&mut guard, new_value); if let Some(request) = @@ -900,11 +908,11 @@ impl InnerRequest { DeviceIdOrAllDevices::DeviceId(r.state.other_device_data.device_id().to_owned()) } InnerRequest::Ready(r) => { - DeviceIdOrAllDevices::DeviceId(r.state.other_device_id.to_owned()) - } - InnerRequest::Transitioned(r) => { - DeviceIdOrAllDevices::DeviceId(r.state.ready.other_device_id.to_owned()) + DeviceIdOrAllDevices::DeviceId(r.state.other_device_data.device_id().to_owned()) } + InnerRequest::Transitioned(r) => DeviceIdOrAllDevices::DeviceId( + r.state.ready.other_device_data.device_id().to_owned(), + ), InnerRequest::Passive(_) => DeviceIdOrAllDevices::AllDevices, InnerRequest::Done(_) => DeviceIdOrAllDevices::AllDevices, InnerRequest::Cancelled(_) => DeviceIdOrAllDevices::AllDevices, @@ -1042,7 +1050,12 @@ impl RequestState { } } - fn into_ready(self, _sender: &UserId, content: &ReadyContent<'_>) -> RequestState { + fn into_ready( + self, + _sender: &UserId, + content: &ReadyContent<'_>, + from_device_data: DeviceData, + ) -> RequestState { // TODO check the flow id, and that the methods match what we suggested. RequestState { flow_id: self.flow_id, @@ -1052,7 +1065,7 @@ impl RequestState { state: Ready { their_methods: content.methods().to_owned(), our_methods: self.state.our_methods, - other_device_id: content.from_device().into(), + other_device_data: from_device_data, }, } } @@ -1115,7 +1128,7 @@ impl RequestState { state: Ready { their_methods: self.state.their_methods, our_methods: methods.clone(), - other_device_id: self.state.other_device_data.device_id().to_owned(), + other_device_data: self.state.other_device_data, }, }; @@ -1153,8 +1166,9 @@ struct Ready { /// The verification methods supported by the us. pub our_methods: Vec, - /// The device ID of the device that responded to the verification request. - pub other_device_id: OwnedDeviceId, + /// The device data of the device that responded to the verification + /// request. + pub other_device_data: DeviceData, } #[cfg(feature = "qrcode")] @@ -1168,7 +1182,7 @@ async fn scan_qr_code( let verification = QrVerification::from_scan( request_state.store.to_owned(), request_state.other_user_id.to_owned(), - state.other_device_id.to_owned(), + state.other_device_data.device_id().to_owned(), request_state.flow_id.as_ref().to_owned(), data, we_started, @@ -1207,21 +1221,7 @@ async fn generate_qr_code( return Ok(None); } - let Some(device) = request_state - .store - .get_device(&request_state.other_user_id, &state.other_device_id) - .await? - else { - warn!( - user_id = ?request_state.other_user_id, - device_id = ?state.other_device_id, - "Can't create a QR code, the device that accepted the \ - verification doesn't exist" - ); - return Ok(None); - }; - - let identities = request_state.store.get_identities(device).await?; + let identities = request_state.store.get_identities(state.other_device_data.clone()).await?; let verification = if let Some(identity) = &identities.identity_being_verified { match &identity { @@ -1240,7 +1240,7 @@ async fn generate_qr_code( } else { warn!( user_id = ?request_state.other_user_id, - device_id = ?state.other_device_id, + device_id = ?state.other_device_data.device_id(), "Can't create a QR code, the other device \ doesn't have a valid device key" ); @@ -1259,7 +1259,7 @@ async fn generate_qr_code( } else { warn!( user_id = ?request_state.other_user_id, - device_id = ?state.other_device_id, + device_id = ?state.other_device_data.device_id(), "Can't create a QR code, our cross signing identity \ doesn't contain a valid master key" ); @@ -1288,7 +1288,7 @@ async fn generate_qr_code( } else { warn!( user_id = ?request_state.other_user_id, - device_id = ?state.other_device_id, + device_id = ?state.other_device_data.device_id(), "Can't create a QR code, we don't trust our own \ master key" ); @@ -1297,7 +1297,7 @@ async fn generate_qr_code( } else { warn!( user_id = ?request_state.other_user_id, - device_id = ?state.other_device_id, + device_id = ?state.other_device_data.device_id(), "Can't create a QR code, the user's identity \ doesn't have a valid master key" ); @@ -1308,7 +1308,7 @@ async fn generate_qr_code( } else { warn!( user_id = ?request_state.other_user_id, - device_id = ?state.other_device_id, + device_id = ?state.other_device_data.device_id(), "Can't create a QR code, the user doesn't have a valid cross \ signing identity." ); @@ -1478,22 +1478,7 @@ async fn start_sas( return Ok(None); } - // TODO signal why starting the sas flow doesn't work? - let Some(device) = request_state - .store - .get_device(&request_state.other_user_id, &state.other_device_id) - .await? - else { - warn!( - user_id = ?request_state.other_user_id, - device_id = ?state.other_device_id, - "Can't start the SAS verification flow, the device that \ - accepted the verification doesn't exist" - ); - return Ok(None); - }; - - let identities = request_state.store.get_identities(device).await?; + let identities = request_state.store.get_identities(state.other_device_data.clone()).await?; let (state, sas, content) = match request_state.flow_id.as_ref() { FlowId::ToDevice(t) => { @@ -1653,7 +1638,10 @@ mod tests { let event_id = event_id!("$1234localhost").to_owned(); let room_id = room_id!("!test:localhost").to_owned(); - let (_alice, alice_store, _bob, bob_store) = setup_stores().await; + let (alice, alice_store, bob, bob_store) = setup_stores().await; + + let alice_device_data = DeviceData::from_account(&alice); + let bob_device_data = DeviceData::from_account(&bob); let content = VerificationRequest::request( &bob_store.account.user_id, @@ -1662,12 +1650,6 @@ mod tests { None, ); - let device_data = alice_store - .get_device(&bob_store.account.user_id, &bob_store.account.device_id) - .await - .unwrap() - .expect("Missing device data"); - let flow_id = FlowId::InRoom(room_id, event_id); let bob_request = VerificationRequest::new( @@ -1690,7 +1672,7 @@ mod tests { bob_id(), flow_id, &(&content).into(), - device_data, + bob_device_data, ); assert_matches!(alice_request.state(), VerificationRequestState::Requested { .. }); @@ -1698,7 +1680,7 @@ mod tests { let content: OutgoingContent = alice_request.accept().unwrap().try_into().unwrap(); let content = ReadyContent::try_from(&content).unwrap(); - bob_request.receive_ready(alice_id(), &content); + bob_request.receive_ready(alice_id(), &content, alice_device_data); assert_matches!(bob_request.state(), VerificationRequestState::Ready { .. }); assert_matches!(alice_request.state(), VerificationRequestState::Ready { .. }); @@ -1748,8 +1730,10 @@ mod tests { let event_id = event_id!("$1234localhost"); let room_id = room_id!("!test:localhost"); - let (_alice, alice_store, bob, bob_store) = setup_stores().await; - let bob_device = DeviceData::from_account(&bob); + let (alice, alice_store, bob, bob_store) = setup_stores().await; + + let alice_device_data = DeviceData::from_account(&alice); + let bob_device_data = DeviceData::from_account(&bob); let content = VerificationRequest::request( &bob_store.account.user_id, @@ -1758,12 +1742,6 @@ mod tests { None, ); - let device_data = alice_store - .get_device(&bob_store.account.user_id, &bob_store.account.device_id) - .await - .unwrap() - .expect("Missing device data"); - let flow_id = FlowId::from((room_id, event_id)); let bob_request = VerificationRequest::new( @@ -1782,19 +1760,19 @@ mod tests { bob_id(), flow_id, &(&content).into(), - device_data, + bob_device_data.clone(), ); - do_accept_request(&alice_request, &bob_request, None); + do_accept_request(&alice_request, alice_device_data, &bob_request, None); let (bob_sas, request) = bob_request.start_sas().await.unwrap().unwrap(); let content: OutgoingContent = request.try_into().unwrap(); let content = StartContent::try_from(&content).unwrap(); let flow_id = content.flow_id().to_owned(); - alice_request.receive_start(bob_device.user_id(), &content).await.unwrap(); + alice_request.receive_start(bob_device_data.user_id(), &content).await.unwrap(); let alice_sas = - alice_request.verification_cache.get_sas(bob_device.user_id(), &flow_id).unwrap(); + alice_request.verification_cache.get_sas(bob_device_data.user_id(), &flow_id).unwrap(); assert_matches!( alice_request.state(), @@ -1811,22 +1789,24 @@ mod tests { #[async_test] async fn test_requesting_until_sas_to_device() { - let (_alice, alice_store, bob, bob_store) = setup_stores().await; - let bob_device = DeviceData::from_account(&bob); + let (alice, alice_store, bob, bob_store) = setup_stores().await; + + let alice_device_data = DeviceData::from_account(&alice); + let bob_device_data = DeviceData::from_account(&bob); // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), None); let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; - do_accept_request(&alice_request, &bob_request, None); + do_accept_request(&alice_request, alice_device_data, &bob_request, None); let (bob_sas, request) = bob_request.start_sas().await.unwrap().unwrap(); let content: OutgoingContent = request.try_into().unwrap(); let content = StartContent::try_from(&content).unwrap(); let flow_id = content.flow_id().to_owned(); - alice_request.receive_start(bob_device.user_id(), &content).await.unwrap(); + alice_request.receive_start(bob_device_data.user_id(), &content).await.unwrap(); let alice_sas = - alice_request.verification_cache.get_sas(bob_device.user_id(), &flow_id).unwrap(); + alice_request.verification_cache.get_sas(bob_device_data.user_id(), &flow_id).unwrap(); assert_matches!( alice_request.state(), @@ -1846,7 +1826,9 @@ mod tests { #[async_test] #[cfg(feature = "qrcode")] async fn test_can_scan_another_qr_after_creating_mine() { - let (_alice, alice_store, _bob, bob_store) = setup_stores().await; + let (alice, alice_store, _bob, bob_store) = setup_stores().await; + + let alice_device_data = DeviceData::from_account(&alice); // Set up the pair of verification requests let bob_request = build_test_request( @@ -1857,6 +1839,7 @@ mod tests { let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; do_accept_request( &alice_request, + alice_device_data, &bob_request, Some(vec![VerificationMethod::QrCodeScanV1, VerificationMethod::QrCodeShowV1]), ); @@ -1897,12 +1880,14 @@ mod tests { #[async_test] #[cfg(feature = "qrcode")] async fn test_can_start_sas_after_generating_qr_code() { - let (_alice, alice_store, _bob, bob_store) = setup_stores().await; + let (alice, alice_store, _bob, bob_store) = setup_stores().await; + + let alice_device_data = DeviceData::from_account(&alice); // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), Some(all_methods())); let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; - do_accept_request(&alice_request, &bob_request, Some(all_methods())); + do_accept_request(&alice_request, alice_device_data, &bob_request, Some(all_methods())); // Each side can start its own QR verification flow by generating QR code let alice_verification = alice_request.generate_qr_code().await.unwrap(); @@ -1942,12 +1927,14 @@ mod tests { #[async_test] #[cfg(feature = "qrcode")] async fn test_start_sas_after_scan_cancels_request() { - let (_alice, alice_store, _bob, bob_store) = setup_stores().await; + let (alice, alice_store, _bob, bob_store) = setup_stores().await; + + let alice_device_data = DeviceData::from_account(&alice); // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), Some(all_methods())); let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; - do_accept_request(&alice_request, &bob_request, Some(all_methods())); + do_accept_request(&alice_request, alice_device_data, &bob_request, Some(all_methods())); // Bob generates a QR code let bob_verification = bob_request.generate_qr_code().await.unwrap().unwrap(); @@ -2057,6 +2044,7 @@ mod tests { /// default list of methods will be used. fn do_accept_request( accepting_request: &VerificationRequest, + accepting_device_data: DeviceData, initiating_request: &VerificationRequest, methods: Option>, ) { @@ -2066,7 +2054,11 @@ mod tests { }; let content: OutgoingContent = request.unwrap().try_into().unwrap(); let content = ReadyContent::try_from(&content).unwrap(); - initiating_request.receive_ready(accepting_request.own_user_id(), &content); + initiating_request.receive_ready( + accepting_request.own_user_id(), + &content, + accepting_device_data, + ); assert!(initiating_request.is_ready()); assert!(accepting_request.is_ready()); diff --git a/crates/matrix-sdk/src/encryption/verification/requests.rs b/crates/matrix-sdk/src/encryption/verification/requests.rs index 0e6fb35f510..3892826d2ee 100644 --- a/crates/matrix-sdk/src/encryption/verification/requests.rs +++ b/crates/matrix-sdk/src/encryption/verification/requests.rs @@ -16,7 +16,7 @@ use futures_util::{Stream, StreamExt}; use matrix_sdk_base::crypto::{ CancelInfo, DeviceData, VerificationRequest as BaseVerificationRequest, }; -use ruma::{events::key::verification::VerificationMethod, OwnedDeviceId, RoomId}; +use ruma::{events::key::verification::VerificationMethod, RoomId}; #[cfg(feature = "qrcode")] use super::{QrVerification, QrVerificationData}; @@ -55,9 +55,9 @@ pub enum VerificationRequestState { /// The verification methods supported by the us. our_methods: Vec, - /// The device ID of the device that responded to the verification + /// The device data of the device that responded to the verification /// request. - other_device_id: OwnedDeviceId, + other_device_data: DeviceData, }, /// The verification request has transitioned into a concrete verification /// flow. For example it transitioned into the emoji based SAS @@ -223,8 +223,8 @@ impl VerificationRequest { Requested { their_methods, other_device_data } => { VerificationRequestState::Requested { their_methods, other_device_data } } - Ready { their_methods, our_methods, other_device_id } => { - VerificationRequestState::Ready { their_methods, our_methods, other_device_id } + Ready { their_methods, our_methods, other_device_data } => { + VerificationRequestState::Ready { their_methods, our_methods, other_device_data } } Transitioned { verification } => VerificationRequestState::Transitioned { verification: match verification { From df4a5c36fc557d822ed8cbf3c9247dc8c5d9ed8a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 24 Oct 2024 17:52:51 +0300 Subject: [PATCH 377/979] Pass the `DeviceData` in between the `Ready` and `Transitioned` states instead of fetching it from the store. --- .../src/verification/requests.rs | 219 ++++++++++++------ .../src/encryption/verification/requests.rs | 2 +- 2 files changed, 152 insertions(+), 69 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 7b8d7a27338..3815ff49e5c 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -101,6 +101,10 @@ pub enum VerificationRequestState { /// The concrete [`Verification`] object the verification request /// transitioned into. verification: Verification, + + /// The device data of the device that responded to the verification + /// request. + other_device_data: DeviceData, }, /// The verification flow that was started with this request has finished. Done, @@ -123,9 +127,10 @@ impl From<&InnerRequest> for VerificationRequestState { our_methods: s.state.our_methods.to_owned(), other_device_data: s.state.other_device_data.to_owned(), }, - InnerRequest::Transitioned(s) => { - Self::Transitioned { verification: s.state.verification.to_owned() } - } + InnerRequest::Transitioned(s) => Self::Transitioned { + verification: s.state.verification.to_owned(), + other_device_data: s.state.other_device_data.to_owned(), + }, InnerRequest::Passive(_) => { Self::Cancelled(Cancelled::new(true, CancelCode::Accepted).into()) } @@ -1198,6 +1203,7 @@ async fn scan_qr_code( state: Transitioned { ready: state.to_owned(), verification: verification.to_owned().into(), + other_device_data: state.other_device_data.to_owned(), }, }; @@ -1325,6 +1331,7 @@ async fn generate_qr_code( state: Transitioned { ready: state.to_owned(), verification: verification.to_owned().into(), + other_device_data: state.other_device_data.to_owned(), }, }; @@ -1351,17 +1358,8 @@ async fn receive_start( "Received a new verification start event", ); - let Some(device) = request_state.store.get_device(sender, content.from_device()).await? else { - warn!( - ?sender, - device = ?content.from_device(), - "Received a key verification start event from an unknown device", - ); - - return Ok(None); - }; - - let identities = request_state.store.get_identities(device.clone()).await?; + let other_device_data = state.other_device_data.clone(); + let identities = request_state.store.get_identities(other_device_data.clone()).await?; let own_user_id = &request_state.store.account.user_id; let own_device_id = &request_state.store.account.device_id; @@ -1385,7 +1383,10 @@ async fn receive_start( // we're the lexicographically smaller user ID (or device ID if equal). use std::cmp::Ordering; if !matches!( - (sender.cmp(own_user_id), device.device_id().cmp(own_device_id)), + ( + sender.cmp(own_user_id), + other_device_data.device_id().cmp(own_device_id) + ), (Ordering::Greater, _) | (Ordering::Equal, Ordering::Greater) ) { info!("Started a new SAS verification, replacing an already started one."); @@ -1424,14 +1425,14 @@ async fn receive_start( } Err(c) => { warn!( - user_id = ?device.user_id(), - device_id = ?device.device_id(), + user_id = ?other_device_data.user_id(), + device_id = ?other_device_data.device_id(), content = ?c, "Can't start key verification, canceling.", ); request_state.verification_cache.queue_up_content( - device.user_id(), - device.device_id(), + other_device_data.user_id(), + other_device_data.device_id(), c, None, ); @@ -1485,8 +1486,11 @@ async fn start_sas( let (sas, content) = Sas::start(identities, t.to_owned(), we_started, Some(request_handle), None); - let state = - Transitioned { ready: state.to_owned(), verification: sas.to_owned().into() }; + let state = Transitioned { + ready: state.to_owned(), + verification: sas.to_owned().into(), + other_device_data: state.other_device_data.to_owned(), + }; (state, sas, content) } @@ -1498,8 +1502,11 @@ async fn start_sas( we_started, request_handle, ); - let state = - Transitioned { ready: state.to_owned(), verification: sas.to_owned().into() }; + let state = Transitioned { + ready: state.to_owned(), + verification: sas.to_owned().into(), + other_device_data: state.other_device_data.to_owned(), + }; (state, sas, content) } }; @@ -1555,7 +1562,11 @@ impl Ready { store: request_state.store.to_owned(), flow_id: request_state.flow_id.to_owned(), other_user_id: request_state.other_user_id.to_owned(), - state: Transitioned { ready: self.clone(), verification }, + state: Transitioned { + ready: self.clone(), + verification, + other_device_data: self.other_device_data.clone(), + }, } } } @@ -1564,6 +1575,7 @@ impl Ready { struct Transitioned { ready: Ready, verification: Verification, + other_device_data: DeviceData, } impl RequestState { @@ -1763,7 +1775,7 @@ mod tests { bob_device_data.clone(), ); - do_accept_request(&alice_request, alice_device_data, &bob_request, None); + do_accept_request(&alice_request, alice_device_data.clone(), &bob_request, None); let (bob_sas, request) = bob_request.start_sas().await.unwrap().unwrap(); @@ -1774,15 +1786,24 @@ mod tests { let alice_sas = alice_request.verification_cache.get_sas(bob_device_data.user_id(), &flow_id).unwrap(); - assert_matches!( - alice_request.state(), - VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::SasV1(_), + other_device_data + } = alice_request.state() ); - assert_matches!( - bob_request.state(), - VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + + assert_eq!(bob_device_data, other_device_data); + + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::SasV1(_), + other_device_data + } = bob_request.state() ); + assert_eq!(alice_device_data, other_device_data); + assert!(!bob_sas.is_cancelled()); assert!(!alice_sas.is_cancelled()); } @@ -1797,7 +1818,7 @@ mod tests { // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), None); let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; - do_accept_request(&alice_request, alice_device_data, &bob_request, None); + do_accept_request(&alice_request, alice_device_data.clone(), &bob_request, None); let (bob_sas, request) = bob_request.start_sas().await.unwrap().unwrap(); @@ -1808,15 +1829,24 @@ mod tests { let alice_sas = alice_request.verification_cache.get_sas(bob_device_data.user_id(), &flow_id).unwrap(); - assert_matches!( - alice_request.state(), - VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::SasV1(_), + other_device_data + } = alice_request.state() ); - assert_matches!( - bob_request.state(), - VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + + assert_eq!(bob_device_data, other_device_data); + + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::SasV1(_), + other_device_data + } = bob_request.state() ); + assert_eq!(alice_device_data, other_device_data); + assert!(!bob_sas.is_cancelled()); assert!(!alice_sas.is_cancelled()); assert!(alice_sas.started_from_request()); @@ -1826,9 +1856,10 @@ mod tests { #[async_test] #[cfg(feature = "qrcode")] async fn test_can_scan_another_qr_after_creating_mine() { - let (alice, alice_store, _bob, bob_store) = setup_stores().await; + let (alice, alice_store, bob, bob_store) = setup_stores().await; let alice_device_data = DeviceData::from_account(&alice); + let bob_device_data = DeviceData::from_account(&bob); // Set up the pair of verification requests let bob_request = build_test_request( @@ -1839,7 +1870,7 @@ mod tests { let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; do_accept_request( &alice_request, - alice_device_data, + alice_device_data.clone(), &bob_request, Some(vec![VerificationMethod::QrCodeScanV1, VerificationMethod::QrCodeShowV1]), ); @@ -1848,15 +1879,24 @@ mod tests { let alice_verification = alice_request.generate_qr_code().await.unwrap(); let bob_verification = bob_request.generate_qr_code().await.unwrap(); - assert_matches!( - alice_request.state(), - VerificationRequestState::Transitioned { verification: Verification::QrV1(_) } + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::QrV1(_), + other_device_data + } = alice_request.state() ); - assert_matches!( - bob_request.state(), - VerificationRequestState::Transitioned { verification: Verification::QrV1(_) } + + assert_eq!(bob_device_data, other_device_data); + + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::QrV1(_), + other_device_data + } = bob_request.state() ); + assert_eq!(alice_device_data, other_device_data); + assert!(alice_verification.is_some()); assert!(bob_verification.is_some()); @@ -1867,10 +1907,13 @@ mod tests { assert_let!( VerificationRequestState::Transitioned { - verification: Verification::QrV1(alice_verification) + verification: Verification::QrV1(alice_verification), + other_device_data } = alice_request.state() ); + assert_eq!(bob_device_data, other_device_data); + // Finally we assert that the verification has been reciprocated rather than // cancelled due to a duplicate verification flow assert!(!alice_verification.is_cancelled()); @@ -1880,34 +1923,48 @@ mod tests { #[async_test] #[cfg(feature = "qrcode")] async fn test_can_start_sas_after_generating_qr_code() { - let (alice, alice_store, _bob, bob_store) = setup_stores().await; + let (alice, alice_store, bob, bob_store) = setup_stores().await; let alice_device_data = DeviceData::from_account(&alice); + let bob_device_data = DeviceData::from_account(&bob); // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), Some(all_methods())); let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; - do_accept_request(&alice_request, alice_device_data, &bob_request, Some(all_methods())); + do_accept_request( + &alice_request, + alice_device_data.clone(), + &bob_request, + Some(all_methods()), + ); // Each side can start its own QR verification flow by generating QR code let alice_verification = alice_request.generate_qr_code().await.unwrap(); let bob_verification = bob_request.generate_qr_code().await.unwrap(); - assert_matches!( - alice_request.state(), - VerificationRequestState::Transitioned { verification: Verification::QrV1(_) } + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::QrV1(_), + other_device_data + } = alice_request.state() ); + assert_eq!(bob_device_data, other_device_data); + assert!(alice_verification.is_some()); assert!(bob_verification.is_some()); // Alice can now start SAS verification flow instead of QR without cancelling // the request let (sas, request) = alice_request.start_sas().await.unwrap().unwrap(); - assert_matches!( - alice_request.state(), - VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::SasV1(_), + other_device_data + } = alice_request.state() ); + + assert_eq!(bob_device_data, other_device_data); assert!(!sas.is_cancelled()); // Bob receives the SAS start @@ -1917,9 +1974,14 @@ mod tests { // Bob should now have transitioned to SAS... assert_let!( - VerificationRequestState::Transitioned { verification: Verification::SasV1(bob_sas) } = - bob_request.state() + VerificationRequestState::Transitioned { + verification: Verification::SasV1(bob_sas), + other_device_data + } = bob_request.state() ); + + assert_eq!(alice_device_data, other_device_data); + // ... and, more to the point, it should not be cancelled. assert!(!bob_sas.is_cancelled()); } @@ -1927,40 +1989,58 @@ mod tests { #[async_test] #[cfg(feature = "qrcode")] async fn test_start_sas_after_scan_cancels_request() { - let (alice, alice_store, _bob, bob_store) = setup_stores().await; + let (alice, alice_store, bob, bob_store) = setup_stores().await; let alice_device_data = DeviceData::from_account(&alice); + let bob_device_data = DeviceData::from_account(&bob); // Set up the pair of verification requests let bob_request = build_test_request(&bob_store, alice_id(), Some(all_methods())); let alice_request = build_incoming_verification_request(&alice_store, &bob_request).await; - do_accept_request(&alice_request, alice_device_data, &bob_request, Some(all_methods())); + do_accept_request( + &alice_request, + alice_device_data.clone(), + &bob_request, + Some(all_methods()), + ); // Bob generates a QR code let bob_verification = bob_request.generate_qr_code().await.unwrap().unwrap(); - assert_matches!( - bob_request.state(), - VerificationRequestState::Transitioned { verification: Verification::QrV1(_) } + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::QrV1(_), + other_device_data + } = bob_request.state() ); + assert_eq!(alice_device_data, other_device_data); + // Now Alice scans Bob's code let bob_qr_code = bob_verification.to_bytes().unwrap(); let bob_qr_code = QrVerificationData::from_bytes(bob_qr_code).unwrap(); let _ = alice_request.scan_qr_code(bob_qr_code).await.unwrap().unwrap(); assert_let!( - VerificationRequestState::Transitioned { verification: Verification::QrV1(alice_qr) } = - alice_request.state() + VerificationRequestState::Transitioned { + verification: Verification::QrV1(alice_qr), + other_device_data + } = alice_request.state() ); + + assert_eq!(bob_device_data, other_device_data); assert!(alice_qr.reciprocated()); // But Bob wants to do an SAS verification! let (_, request) = bob_request.start_sas().await.unwrap().unwrap(); - assert_matches!( - bob_request.state(), - VerificationRequestState::Transitioned { verification: Verification::SasV1(_) } + assert_let!( + VerificationRequestState::Transitioned { + verification: Verification::SasV1(_), + other_device_data + } = bob_request.state() ); + assert_eq!(alice_device_data, other_device_data); + // Alice receives the SAS start let content: OutgoingContent = request.try_into().unwrap(); let content = StartContent::try_from(&content).unwrap(); @@ -1972,9 +2052,12 @@ mod tests { // and she should now have a *cancelled* SAS verification assert_let!( VerificationRequestState::Transitioned { - verification: Verification::SasV1(alice_sas) + verification: Verification::SasV1(alice_sas), + other_device_data } = alice_request.state() ); + + assert_eq!(bob_device_data, other_device_data); assert!(alice_sas.is_cancelled()); } diff --git a/crates/matrix-sdk/src/encryption/verification/requests.rs b/crates/matrix-sdk/src/encryption/verification/requests.rs index 3892826d2ee..49b8a51b438 100644 --- a/crates/matrix-sdk/src/encryption/verification/requests.rs +++ b/crates/matrix-sdk/src/encryption/verification/requests.rs @@ -226,7 +226,7 @@ impl VerificationRequest { Ready { their_methods, our_methods, other_device_data } => { VerificationRequestState::Ready { their_methods, our_methods, other_device_data } } - Transitioned { verification } => VerificationRequestState::Transitioned { + Transitioned { verification, .. } => VerificationRequestState::Transitioned { verification: match verification { matrix_sdk_base::crypto::Verification::SasV1(s) => { Verification::SasV1(SasVerification { inner: s, client }) From 01cbce907cdce8fb770cf5b63892261bf8b3e37f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 22 Oct 2024 16:35:33 +0200 Subject: [PATCH 378/979] feat(sdk): Add `LinkedChunk::remove_item_at`. This patch adds the `LinkedChunk::remove_item_at` method, along with `Update::RemoveItem` variant. --- .../src/event_cache/linked_chunk/as_vector.rs | 4 + .../src/event_cache/linked_chunk/mod.rs | 273 ++++++++++++++++++ .../src/event_cache/linked_chunk/updates.rs | 6 + 3 files changed, 283 insertions(+) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs index bff2eecde6b..d3b81abfde2 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs @@ -379,6 +379,10 @@ impl UpdateToVectorDiff { } } + Update::RemoveItem { at } => { + todo!() + } + Update::DetachLastItems { at } => { let expected_chunk_identifier = at.chunk_identifier(); let new_length = at.index(); diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs index ce70c12475d..1bfda5f9df7 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs @@ -406,6 +406,79 @@ impl LinkedChunk { Ok(()) } + /// Remove item at a specified position in the [`LinkedChunk`]. + /// + /// Because the `position` can be invalid, this method returns a + /// `Result`. + pub fn remove_item_at(&mut self, position: Position) -> Result { + let chunk_identifier = position.chunk_identifier(); + let item_index = position.index(); + + let mut chunk_ptr = None; + let removed_item; + + { + let chunk = self + .links + .chunk_mut(chunk_identifier) + .ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?; + + let can_unlink_chunk = match &mut chunk.content { + ChunkContent::Gap(..) => { + return Err(Error::ChunkIsAGap { identifier: chunk_identifier }) + } + + ChunkContent::Items(current_items) => { + let current_items_length = current_items.len(); + + if item_index > current_items_length { + return Err(Error::InvalidItemIndex { index: item_index }); + } + + removed_item = current_items.remove(item_index); + + if let Some(updates) = self.updates.as_mut() { + updates + .push(Update::RemoveItem { at: Position(chunk_identifier, item_index) }) + } + + current_items.is_empty() + } + }; + + // If the `chunk` can be unlinked, and if the `chunk` is not the first one, we + // can remove it. + if can_unlink_chunk && chunk.is_first_chunk().not() { + // Unlink `chunk`. + chunk.unlink(&mut self.updates); + + chunk_ptr = Some(chunk.as_ptr()); + + // We need to update `self.last` if and only if `chunk` _is_ the last chunk. The + // new last chunk is the chunk before `chunk`. + if chunk.is_last_chunk() { + self.links.last = chunk.previous; + } + } + + self.length -= 1; + + // Stop borrowing `chunk`. + } + + if let Some(chunk_ptr) = chunk_ptr { + // `chunk` has been unlinked. + + // Re-box the chunk, and let Rust does its job. + // + // SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't + // use it anymore, it's a leak. It is time to re-`Box` it and drop it. + let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) }; + } + + Ok(removed_item) + } + /// Insert a gap at a specified position in the [`LinkedChunk`]. /// /// Because the `position` can be invalid, this method returns a @@ -1852,6 +1925,206 @@ mod tests { Ok(()) } + #[test] + fn test_remove_item_at() -> Result<(), Error> { + use super::Update::*; + + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 11); + + // Ignore previous updates. + let _ = linked_chunk.updates().unwrap().take(); + + // Remove the last item of the middle chunk, 3 times. The chunk is empty after + // that. The chunk is removed. + { + let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap(); + let removed_item = linked_chunk.remove_item_at(position_of_f)?; + + assert_eq!(removed_item, 'f'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 10); + + let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap(); + let removed_item = linked_chunk.remove_item_at(position_of_e)?; + + assert_eq!(removed_item, 'e'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 9); + + let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap(); + let removed_item = linked_chunk.remove_item_at(position_of_d)?; + + assert_eq!(removed_item, 'd'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 8); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(1), 2) }, + RemoveItem { at: Position(ChunkIdentifier(1), 1) }, + RemoveItem { at: Position(ChunkIdentifier(1), 0) }, + RemoveChunk(ChunkIdentifier(1)), + ] + ); + } + + // Remove the first item of the first chunk, 3 times. The chunk is empty after + // that. The chunk is NOT removed because it's the first chunk. + { + let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap(); + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'a'); + assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 7); + + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'b'); + assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 6); + + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'c'); + assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 5); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + ] + ); + } + + // Remove the first item of the middle chunk, 3 times. The chunk is empty after + // that. The chunk is removed. + { + let first_position = linked_chunk.item_position(|item| *item == 'g').unwrap(); + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'g'); + assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 4); + + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'h'); + assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']); + assert_eq!(linked_chunk.len(), 3); + + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'i'); + assert_items_eq!(linked_chunk, [] ['j', 'k']); + assert_eq!(linked_chunk.len(), 2); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(2), 0) }, + RemoveItem { at: Position(ChunkIdentifier(2), 0) }, + RemoveItem { at: Position(ChunkIdentifier(2), 0) }, + RemoveChunk(ChunkIdentifier(2)), + ] + ); + } + + // Remove the last item of the last chunk, twice. The chunk is empty after that. + // The chunk is removed. + { + let position_of_k = linked_chunk.item_position(|item| *item == 'k').unwrap(); + let removed_item = linked_chunk.remove_item_at(position_of_k)?; + + assert_eq!(removed_item, 'k'); + #[rustfmt::skip] + assert_items_eq!(linked_chunk, [] ['j']); + assert_eq!(linked_chunk.len(), 1); + + let position_of_j = linked_chunk.item_position(|item| *item == 'j').unwrap(); + let removed_item = linked_chunk.remove_item_at(position_of_j)?; + + assert_eq!(removed_item, 'j'); + assert_items_eq!(linked_chunk, []); + assert_eq!(linked_chunk.len(), 0); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(3), 1) }, + RemoveItem { at: Position(ChunkIdentifier(3), 0) }, + RemoveChunk(ChunkIdentifier(3)), + ] + ); + } + + // Add a couple more items, delete one, add a gap, and delete more items. + { + linked_chunk.push_items_back(['a', 'b', 'c', 'd']); + + #[rustfmt::skip] + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']); + assert_eq!(linked_chunk.len(), 4); + + let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap(); + linked_chunk.insert_gap_at((), position_of_c)?; + + assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] ['d']); + assert_eq!(linked_chunk.len(), 4); + + // Ignore updates. + let _ = linked_chunk.updates().unwrap().take(); + + let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap(); + let removed_item = linked_chunk.remove_item_at(position_of_c)?; + + assert_eq!(removed_item, 'c'); + assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']); + assert_eq!(linked_chunk.len(), 3); + + let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap(); + let removed_item = linked_chunk.remove_item_at(position_of_d)?; + + assert_eq!(removed_item, 'd'); + assert_items_eq!(linked_chunk, ['a', 'b'] [-]); + assert_eq!(linked_chunk.len(), 2); + + let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap(); + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'a'); + assert_items_eq!(linked_chunk, ['b'] [-]); + assert_eq!(linked_chunk.len(), 1); + + let removed_item = linked_chunk.remove_item_at(first_position)?; + + assert_eq!(removed_item, 'b'); + assert_items_eq!(linked_chunk, [] [-]); + assert_eq!(linked_chunk.len(), 0); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(6), 0) }, + RemoveChunk(ChunkIdentifier(6)), + RemoveItem { at: Position(ChunkIdentifier(4), 0) }, + RemoveChunk(ChunkIdentifier(4)), + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + ] + ); + } + + Ok(()) + } + #[test] fn test_insert_gap_at() -> Result<(), Error> { use super::Update::*; diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs index 68da551c640..5a143b94486 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs @@ -76,6 +76,12 @@ pub enum Update { items: Vec, }, + /// An item has been removed inside a chunk of kind Items. + RemoveItem { + /// The [`Position`] of the item. + at: Position, + }, + /// The last items of a chunk have been detached, i.e. the chunk has been /// truncated. DetachLastItems { From 135c448f2dd2580939049f82402b937b1f166a54 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 23 Oct 2024 11:13:07 +0200 Subject: [PATCH 379/979] chore(sdk): Extract code into a `map_to_offset` method. This is only code move, nothing has changed. --- .../src/event_cache/linked_chunk/as_vector.rs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs index d3b81abfde2..e68da3758ab 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs @@ -22,7 +22,7 @@ use eyeball_im::VectorDiff; use super::{ updates::{ReaderToken, Update, UpdatesInner}, - ChunkContent, ChunkIdentifier, Iter, + ChunkContent, ChunkIdentifier, Iter, Position, }; /// A type alias to represent a chunk's length. This is purely for commodity. @@ -329,37 +329,9 @@ impl UpdateToVectorDiff { } Update::PushItems { at: position, items } => { - let expected_chunk_identifier = position.chunk_identifier(); - - let (chunk_index, offset, chunk_length) = { - let control_flow = self.chunks.iter_mut().enumerate().try_fold( - position.index(), - |offset, (chunk_index, (chunk_identifier, chunk_length))| { - if chunk_identifier == &expected_chunk_identifier { - ControlFlow::Break((chunk_index, offset, chunk_length)) - } else { - ControlFlow::Continue(offset + *chunk_length) - } - }, - ); - - match control_flow { - // Chunk has been found, and all values have been calculated as - // expected. - ControlFlow::Break(values) => values, - - // Chunk has not been found. - ControlFlow::Continue(..) => { - // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not - // buggy, and assuming `Self::chunks` is correctly initialized, it - // is not possible to push items on a chunk that does not exist. If - // this predicate fails, it means `LinkedChunk` or - // `ObservableUpdates` contain a bug. - panic!("Pushing items: The chunk is not found"); - } - } - }; + let (offset, (chunk_index, chunk_length)) = self.map_to_offset(position); + // Update the length of the chunk in `self.chunks`. *chunk_length += items.len(); // See `mute_push_items` to learn more. @@ -416,6 +388,43 @@ impl UpdateToVectorDiff { diffs } + + fn map_to_offset(&mut self, position: &Position) -> (usize, (usize, &mut usize)) { + let expected_chunk_identifier = position.chunk_identifier(); + + let (offset, (chunk_index, chunk_length)) = { + let control_flow = self.chunks.iter_mut().enumerate().try_fold( + position.index(), + |offset, (chunk_index, (chunk_identifier, chunk_length))| { + if chunk_identifier == &expected_chunk_identifier { + ControlFlow::Break((offset, (chunk_index, chunk_length))) + } else { + ControlFlow::Continue(offset + *chunk_length) + } + }, + ); + + match control_flow { + // Chunk has been found, and all values have been calculated as + // expected. + ControlFlow::Break(values) => values, + + // Chunk has not been found. + ControlFlow::Continue(..) => { + // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are + // not buggy, and assuming + // `Self::chunks` is correctly initialized, it + // is not possible to push items on a chunk that does not exist. + // If this predicate fails, + // it means `LinkedChunk` or + // `ObservableUpdates` contain a bug. + panic!("Pushing items: The chunk is not found"); + } + } + }; + + (offset, (chunk_index, chunk_length)) + } } #[cfg(test)] From ca3d5693b4d8fb73a85e3cb7609935b30e9cbbc2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 23 Oct 2024 14:13:17 +0200 Subject: [PATCH 380/979] chore(sdk): Rename a couple of variables. This is a clean up patch, nothing fancy. --- .../src/event_cache/linked_chunk/as_vector.rs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs index e68da3758ab..8334119c1e5 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs @@ -253,7 +253,7 @@ impl UpdateToVectorDiff { // // From the `VectorDiff` “point of view”, this optimisation aims at avoiding // removing items to push them again later. - let mut mute_push_items = false; + let mut reattaching = false; for update in updates { match update { @@ -329,18 +329,22 @@ impl UpdateToVectorDiff { } Update::PushItems { at: position, items } => { + let number_of_chunks = self.chunks.len(); let (offset, (chunk_index, chunk_length)) = self.map_to_offset(position); - // Update the length of the chunk in `self.chunks`. + let is_pushing_back = + chunk_index + 1 == number_of_chunks && position.index() >= *chunk_length; + + // Add the number of items to the chunk in `self.chunks`. *chunk_length += items.len(); - // See `mute_push_items` to learn more. - if mute_push_items { + // See `reattaching` to learn more. + if reattaching { continue; } // Optimisation: we can emit a `VectorDiff::Append` in this particular case. - if chunk_index + 1 == self.chunks.len() { + if is_pushing_back { diffs.push(VectorDiff::Append { values: items.into() }); } // No optimisation: let's emit `VectorDiff::Insert`. @@ -375,13 +379,13 @@ impl UpdateToVectorDiff { } Update::StartReattachItems => { - // Entering the `reattaching` mode. - mute_push_items = true; + // Entering the _reattaching_ mode. + reattaching = true; } Update::EndReattachItems => { - // Exiting the `reattaching` mode. - mute_push_items = false; + // Exiting the _reattaching_ mode. + reattaching = false; } } } @@ -411,14 +415,11 @@ impl UpdateToVectorDiff { // Chunk has not been found. ControlFlow::Continue(..) => { - // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are - // not buggy, and assuming - // `Self::chunks` is correctly initialized, it - // is not possible to push items on a chunk that does not exist. - // If this predicate fails, - // it means `LinkedChunk` or - // `ObservableUpdates` contain a bug. - panic!("Pushing items: The chunk is not found"); + // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and + // assuming `Self::chunks` is correctly initialized, it is not possible to work + // on a chunk that does not exist. If this predicate fails, it means + // `LinkedChunk` or `ObservableUpdates` contain a bug. + panic!("The chunk is not found"); } } }; From c23c3b9558af0edea092e8e74f3e1d77edd781c8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 23 Oct 2024 14:14:45 +0200 Subject: [PATCH 381/979] chore(sdk): Rename a couple of variables. This is another clean up patch. --- .../src/event_cache/linked_chunk/as_vector.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs index 8334119c1e5..a7c5416ce19 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs @@ -359,15 +359,15 @@ impl UpdateToVectorDiff { todo!() } - Update::DetachLastItems { at } => { - let expected_chunk_identifier = at.chunk_identifier(); - let new_length = at.index(); + Update::DetachLastItems { at: position } => { + let expected_chunk_identifier = position.chunk_identifier(); + let new_length = position.index(); - let length = self + let chunk_length = self .chunks .iter_mut() - .find_map(|(chunk_identifier, length)| { - (*chunk_identifier == expected_chunk_identifier).then_some(length) + .find_map(|(chunk_identifier, chunk_length)| { + (*chunk_identifier == expected_chunk_identifier).then_some(chunk_length) }) // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and // assuming `Self::chunks` is correctly initialized, it is not possible to @@ -375,7 +375,7 @@ impl UpdateToVectorDiff { // it means `LinkedChunk` or `ObservableUpdates` contain a bug. .expect("Detach last items: The chunk is not found"); - *length = new_length; + *chunk_length = new_length; } Update::StartReattachItems => { From e0be1e8e32e68af87bf1914dfd58cd3fc387a906 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 23 Oct 2024 14:15:48 +0200 Subject: [PATCH 382/979] fix(sdk): Fix a bug in an optimisation of `UpdatetoVectorDiff`. This patch fixes a bug in an optimisation inside `UpdateToVectorDiff` when an `Update::PushItems` is handled. It can sometimes create `VectorDiff::Append` instead of a `VectorDiff::Insert`. The tests will be part of the next patch. --- .../src/event_cache/linked_chunk/as_vector.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs index a7c5416ce19..a42cee50882 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs @@ -14,7 +14,7 @@ use std::{ collections::VecDeque, - ops::ControlFlow, + ops::{ControlFlow, Not}, sync::{Arc, RwLock}, }; @@ -254,6 +254,7 @@ impl UpdateToVectorDiff { // From the `VectorDiff` “point of view”, this optimisation aims at avoiding // removing items to push them again later. let mut reattaching = false; + let mut detaching = false; for update in updates { match update { @@ -344,7 +345,7 @@ impl UpdateToVectorDiff { } // Optimisation: we can emit a `VectorDiff::Append` in this particular case. - if is_pushing_back { + if is_pushing_back && detaching.not() { diffs.push(VectorDiff::Append { values: items.into() }); } // No optimisation: let's emit `VectorDiff::Insert`. @@ -376,6 +377,9 @@ impl UpdateToVectorDiff { .expect("Detach last items: The chunk is not found"); *chunk_length = new_length; + + // Entering the _detaching_ mode. + detaching = true; } Update::StartReattachItems => { @@ -386,6 +390,9 @@ impl UpdateToVectorDiff { Update::EndReattachItems => { // Exiting the _reattaching_ mode. reattaching = false; + + // Exiting the _detaching_ mode. + detaching = false; } } } From b62661bc702ac744a9da24b130f623a7dd6bed37 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 23 Oct 2024 14:16:58 +0200 Subject: [PATCH 383/979] feat(sdk): Map `Update::RemoveItem` into `VectorDiff::Remove` in `UpdateToVectorDiff`. This patch implements the support of `Update::RemoveItem` inside `UpdateToVectorDiff` to emit a `VectorDiff::Remove`. --- .../src/event_cache/linked_chunk/as_vector.rs | 107 ++++++++++++++++-- 1 file changed, 100 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs index a42cee50882..4a39e5d5a74 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs @@ -356,8 +356,19 @@ impl UpdateToVectorDiff { } } - Update::RemoveItem { at } => { - todo!() + Update::RemoveItem { at: position } => { + let (offset, (_chunk_index, chunk_length)) = self.map_to_offset(position); + + // Remove one item to the chunk in `self.chunks`. + *chunk_length -= 1; + + // See `reattaching` to learn more. + if reattaching { + continue; + } + + // Let's emit a `VectorDiff::Remove`. + diffs.push(VectorDiff::Remove { index: offset }); } Update::DetachLastItems { at: position } => { @@ -456,6 +467,9 @@ mod tests { match diff { VectorDiff::Insert { index, value } => accumulator.insert(index, value), VectorDiff::Append { values } => accumulator.append(values), + VectorDiff::Remove { index } => { + accumulator.remove(index); + } diff => unimplemented!("{diff:?}"), } } @@ -599,15 +613,77 @@ mod tests { &[VectorDiff::Insert { index: 0, value: 'm' }], ); + let removed_item = linked_chunk + .remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap()) + .unwrap(); + assert_eq!(removed_item, 'c'); + assert_items_eq!( + linked_chunk, + ['m', 'a', 'w'] ['x'] ['y', 'z', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h'] + ); + + // From an `ObservableVector` point of view, it would look like: + // + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // | m | a | w | x | y | z | b | d | i | j | k | l | e | f | g | h | + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // ^ + // | + // `c` has been removed + apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]); + + let removed_item = linked_chunk + .remove_item_at(linked_chunk.item_position(|item| *item == 'z').unwrap()) + .unwrap(); + assert_eq!(removed_item, 'z'); + assert_items_eq!( + linked_chunk, + ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h'] + ); + + // From an `ObservableVector` point of view, it would look like: + // + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // | m | a | w | x | y | b | d | i | j | k | l | e | f | g | h | + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // ^ + // | + // `z` has been removed + apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 5 }]); + + linked_chunk + .insert_items_at(['z'], linked_chunk.item_position(|item| *item == 'h').unwrap()) + .unwrap(); + + assert_items_eq!( + linked_chunk, + ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h'] + ); + + // From an `ObservableVector` point of view, it would look like: + // + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // | m | a | w | x | y | b | d | i | j | k | l | e | f | g | z | h | + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // ^^^^ + // | + // new! + apply_and_assert_eq( + &mut accumulator, + as_vector.take(), + &[VectorDiff::Insert { index: 14, value: 'z' }], + ); + drop(linked_chunk); assert!(as_vector.take().is_empty()); // Finally, ensure the “reconstitued” vector is the one expected. assert_eq!( accumulator, - vector![ - 'm', 'a', 'w', 'x', 'y', 'z', 'b', 'c', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'h' - ] + vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h'] ); } @@ -643,6 +719,7 @@ mod tests { PushItems { items: Vec }, PushGap, ReplaceLastGap { items: Vec }, + RemoveItem { item: char }, } fn as_vector_operation_strategy() -> impl Strategy { @@ -654,13 +731,16 @@ mod tests { 1 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25) .prop_map(|items| AsVectorOperation::ReplaceLastGap { items }), + + 1 => prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()) + .prop_map(|item| AsVectorOperation::RemoveItem { item }), ] } proptest! { #[test] fn as_vector_is_correct( - operations in prop::collection::vec(as_vector_operation_strategy(), 10..=50) + operations in prop::collection::vec(as_vector_operation_strategy(), 50..=200) ) { let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history(); let mut as_vector = linked_chunk.as_vector().unwrap(); @@ -683,7 +763,17 @@ mod tests { continue; }; - linked_chunk.replace_gap_at(items, gap_identifier).unwrap(); + linked_chunk.replace_gap_at(items, gap_identifier).expect("Failed to replace a gap"); + } + + AsVectorOperation::RemoveItem { item: expected_item } => { + let Some(position) = linked_chunk + .items().find_map(|(position, item)| (*item == expected_item).then_some(position)) + else { + continue; + }; + + linked_chunk.remove_item_at(position).expect("Failed to remove an item"); } } } @@ -699,6 +789,9 @@ mod tests { vector_from_diffs.append(&mut values); } + VectorDiff::Remove { index } => { + vector_from_diffs.remove(index); + } _ => unreachable!(), } } From 131921c045fe7551de51655c2499020ef0ce24b4 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 22 Oct 2024 12:08:31 +0100 Subject: [PATCH 384/979] fix(tests) Increase a test timeout to fix occasional flakes I saw locally --- crates/matrix-sdk/src/room/identity_status_changes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index a1b69072ec7..41d043739e4 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -791,7 +791,7 @@ mod tests { .subscribe_to_identity_status_changes() .await .expect("Should be able to subscribe") - .timeout(Duration::from_secs(2)) + .timeout(Duration::from_secs(5)) } async fn init() -> (Client, OwnedUserId, SyncResponseBuilder) { From a1a4ce0a95e61c708f0cec22bf25affeffff9737 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 21 Oct 2024 17:27:50 +0100 Subject: [PATCH 385/979] refactor(crypto) Tidy IdentityChangeDataSet test data --- .../src/test_json/keys_query.rs | 204 ++++++++++++++++++ .../src/test_json/keys_query_sets.rs | 189 +++------------- testing/matrix-sdk-test/src/test_json/mod.rs | 1 + 3 files changed, 230 insertions(+), 164 deletions(-) create mode 100644 testing/matrix-sdk-test/src/test_json/keys_query.rs diff --git a/testing/matrix-sdk-test/src/test_json/keys_query.rs b/testing/matrix-sdk-test/src/test_json/keys_query.rs new file mode 100644 index 00000000000..e9b25c7d480 --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/keys_query.rs @@ -0,0 +1,204 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Templates for simulating responses to +//! `POST /_matrix/client/v3/keys/query` +//! requests. + +use std::{collections::HashMap, iter}; + +use ruma::{ + api::client::keys::get_keys::v3::Response as KeyQueryResponse, device_id, user_id, DeviceId, + UserId, +}; +use serde_json::json; + +use crate::ruma_response_from_json; + +pub struct KeysQueryUser { + pub user_id: &'static UserId, + pub device_id: &'static DeviceId, + device_key_curve25519: &'static str, + device_key_ed22519: &'static str, + device_signature: &'static str, + device_signature_2_name: Option<&'static str>, + device_signature_2_signature: Option<&'static str>, + master_key_name: Option<&'static str>, + master_key_signature: Option<&'static str>, + master_key_device_signature: Option<&'static str>, + self_signing_key_name: Option<&'static str>, + self_signing_key_signature: Option<&'static str>, +} + +impl KeysQueryUser { + pub(crate) fn bob_a() -> Self { + Self { + user_id: user_id!("@bob:localhost"), + device_id: device_id!("GYKSNAWLVK"), + device_key_curve25519: "dBcZBzQaiQYWf6rBPh2QypIOB/dxSoTeyaFaxNNbeHs", + device_key_ed22519: "6melQNnhoI9sT2b4VzNPAwa8aB179ym45fON8Yo7kVk", + device_signature: "Fk45zHAbrd+1j9wZXLjL2Y/+DU/Mnz9yuvlfYBOOT7qExN2Jdud+5BAuNs8nZ/caS4wTF39Kg3zQpzaGERoCBg", + device_signature_2_name: Some("dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw"), + device_signature_2_signature: Some("md0Pa1MYlneFb1fp6KCsvZpi2ySb6/G+ULoCbQDWBeDxNEcoNMzf7PEKY04UToCZKUU4LifvRWmiWFDanOlkCQ"), + master_key_name: Some("/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY"), + master_key_signature: Some("6vGDbPO5XzlcwbU3aV+kcck+iHHEBtX85ow2gW5U05/DZdtda/JNVa5Nn7B9lQHNnnrMqt1sX00y/JrIkSS1Aw"), + master_key_device_signature: Some("jLxmUPr0Ny2Ai9+NGKGhed9BAuKikOc7r6gr7MQVawePYS95w8NJ8Tzaq9zFFOmIiojACNdQ/ksy3QAdwD6vBQ"), + self_signing_key_name: Some("dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw"), + self_signing_key_signature: Some("7md6mwjUK8zjintmffJ0+kImC59/Y8PdySy99EZz5Neu+VMX3LT7txhKO2gC/hmDduRw+JGfGXIiDxR7GmQqDw"), + } + } + + pub(crate) fn bob_b() -> Self { + Self { + user_id: user_id!("@bob:localhost"), + device_id: device_id!("ATWKQFSFRN"), + device_key_curve25519: "CY0TWVK1/Kj3ZADuBcGe3UKvpT+IKAPMUsMeJhSDqno", + device_key_ed22519: "TyTQqd6j2JlWZh97r+kTYuCbvqnPoNwO6EGovYsjY00", + device_signature: "BQ9Gp0p+6srF+c8OyruqKKd9R4yaub3THYAyyBB/7X/rG8BwcAqFynzl1aGyFYun4Q+087a5OSiglCXI+/kQAA", + device_signature_2_name: Some("At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc"), + device_signature_2_signature: Some("TWmDPaG7t0rZ6luauonELD3dmBDTIRryqXhgsIQRiGint2rJdic8RVyZ6a61bgu6mtBjfvU3prqMNp6sVi16Cg"), + master_key_name: Some("NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4"), + master_key_signature: Some("xqLhC3sIUci1W2CNVW7HZWXreQApgjv2RDwB0WPiMd1P4vbZ/qJM0KWqK2piGPWliPi8YVREMrg216KXM3IhCA"), + master_key_device_signature: Some("MBOzCKYPQLQMpBY2lFZJ4c8451xJfQCdhPBb1AHlTUSxKFiWi6V+k1oRRnhQein/PjkIY7ZO+HoOrIeOtbRMAw"), + self_signing_key_name: Some("At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc"), + self_signing_key_signature: Some("Ls6CeoA4LoPCHuSwG96kbhd1dEV09TgdMROIZi6vFz/MT9Wtik6joQi/tQ3zCwIZCSR53ksLO4jG1DD31AiBAA"), + } + } + + pub(crate) fn bob_c() -> Self { + Self { + user_id: user_id!("@bob:localhost"), + device_id: device_id!("OPABMDDXGX"), + device_key_curve25519: "O6bwa9Op0E+PQPCrbTOfdYwU+j95RRPhXIHuNpe94ns", + device_key_ed22519: "DvjkSNOM9XrR1gWrr2YSDvTnwnLIgKDMRr5v8HgMKak", + device_signature: "o+BBnw/SIJWxSf799Adq6jEl9X3lwCg5MJkS8GlfId+pW3ReEETK0l+9bhCAgBsNSKRtB/fmZQBhjMx4FJr+BA", + device_signature_2_name: None, + device_signature_2_signature: None, + master_key_name: None, + master_key_signature: None, + master_key_device_signature: None, + self_signing_key_name: None, + self_signing_key_signature: None, + } + } +} + +pub fn keys_query(user: &KeysQueryUser, signed_device_users: &[KeysQueryUser]) -> KeyQueryResponse { + let device_keys = iter::once((user.user_id, device_keys(user, signed_device_users))); + + let data = json!({ + "device_keys": to_object(device_keys), + "failures": {}, + "master_keys": master_keys(user), + "self_signing_keys": self_signing_keys(user), + "user_signing_keys": {} + }); + ruma_response_from_json(&data) +} + +pub fn self_signing_keys(user: &KeysQueryUser) -> serde_json::Value { + if let (Some(self_signing_key_name), Some(self_signing_key_signature)) = + (user.self_signing_key_name, user.self_signing_key_signature) + { + let master_key_name = user + .master_key_name + .expect("Missing master key name when we have self_signing_key_name"); + + json!({ + user.user_id: { + "keys": { + &format!("ed25519:{}", self_signing_key_name): self_signing_key_name + }, + "signatures": { + "@bob:localhost": { + &format!("ed25519:{}", master_key_name): self_signing_key_signature, + } + }, + "usage": [ "self_signing" ], + "user_id": "@bob:localhost" + } + }) + } else { + json!({}) + } +} + +pub fn master_keys(user: &KeysQueryUser) -> serde_json::Value { + if let (Some(master_key_name), Some(master_key_signature), Some(master_key_device_signature)) = + (user.master_key_name, user.master_key_signature, user.master_key_device_signature) + { + json!({ + user.user_id: { + "keys": { &format!("ed25519:{}", master_key_name): master_key_name }, + "signatures": { + user.user_id: { + &format!("ed25519:{}", master_key_name): master_key_signature, + &format!("ed25519:{}", user.device_id): master_key_device_signature + } + }, + "usage": [ "master" ], + "user_id": user.user_id + } + }) + } else { + json!({}) + } +} + +pub fn device_keys_payload(user: &KeysQueryUser) -> serde_json::Value { + let mut signatures = HashMap::new(); + signatures.insert(format!("ed25519:{}", user.device_id), user.device_signature); + + if let (Some(device_signature_2_name), Some(device_signature_2_signature)) = + (user.device_signature_2_name, user.device_signature_2_signature) + { + signatures + .insert(format!("ed25519:{}", device_signature_2_name), device_signature_2_signature); + } + + json!({ + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": user.device_id, + "keys": { + &format!("curve25519:{}", user.device_id): user.device_key_curve25519, + &format!("ed25519:{}", user.device_id): user.device_key_ed22519, + }, + "signatures": { + user.user_id: signatures + }, + "user_id": user.user_id, + }) +} + +fn device_keys(user: &KeysQueryUser, signed_device_users: &[KeysQueryUser]) -> serde_json::Value { + let mut ret = HashMap::new(); + + ret.insert(user.device_id, device_keys_payload(user)); + + for other in signed_device_users { + ret.insert(other.device_id, device_keys_payload(other)); + } + + json!(ret) +} + +fn to_object( + items: impl Iterator, +) -> serde_json::Value { + let mp: HashMap<&'static UserId, serde_json::Value> = items.collect(); + serde_json::to_value(mp).unwrap() +} diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 4c357acc78e..d0ac75929c4 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -4,7 +4,11 @@ use ruma::{ }; use serde_json::{json, Value}; -use crate::ruma_response_from_json; +use super::keys_query::{keys_query, master_keys, KeysQueryUser}; +use crate::{ + ruma_response_from_json, + test_json::keys_query::{device_keys_payload, self_signing_keys}, +}; /// This set of keys/query response was generated using a local synapse. /// Each users was created, device added according to needs and the payload @@ -447,207 +451,64 @@ impl KeyDistributionTestData { /// For user @bob, several payloads with no identities then identity A and B. pub struct IdentityChangeDataSet {} -#[allow(dead_code)] impl IdentityChangeDataSet { pub fn user_id() -> &'static UserId { - user_id!("@bob:localhost") - } + // All 3 bobs have the same user id + assert_eq!(KeysQueryUser::bob_a().user_id, KeysQueryUser::bob_b().user_id); + assert_eq!(KeysQueryUser::bob_a().user_id, KeysQueryUser::bob_c().user_id); - pub fn first_device_id() -> &'static DeviceId { - device_id!("GYKSNAWLVK") + KeysQueryUser::bob_a().user_id } - pub fn second_device_id() -> &'static DeviceId { - device_id!("ATWKQFSFRN") + pub fn device_a() -> &'static DeviceId { + KeysQueryUser::bob_a().device_id } - pub fn third_device_id() -> &'static DeviceId { - device_id!("OPABMDDXGX") + pub fn device_b() -> &'static DeviceId { + KeysQueryUser::bob_b().device_id } - fn device_keys_payload_1_signed_by_a() -> Value { - json!({ - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "GYKSNAWLVK", - "keys": { - "curve25519:GYKSNAWLVK": "dBcZBzQaiQYWf6rBPh2QypIOB/dxSoTeyaFaxNNbeHs", - "ed25519:GYKSNAWLVK": "6melQNnhoI9sT2b4VzNPAwa8aB179ym45fON8Yo7kVk" - }, - "signatures": { - "@bob:localhost": { - "ed25519:GYKSNAWLVK": "Fk45zHAbrd+1j9wZXLjL2Y/+DU/Mnz9yuvlfYBOOT7qExN2Jdud+5BAuNs8nZ/caS4wTF39Kg3zQpzaGERoCBg", - "ed25519:dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw": "md0Pa1MYlneFb1fp6KCsvZpi2ySb6/G+ULoCbQDWBeDxNEcoNMzf7PEKY04UToCZKUU4LifvRWmiWFDanOlkCQ" - } - }, - "user_id": "@bob:localhost", - }) + pub fn device_c() -> &'static DeviceId { + KeysQueryUser::bob_c().device_id } pub fn msk_a() -> Value { - json!({ - "@bob:localhost": { - "keys": { - "ed25519:/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY": "/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY" - }, - "signatures": { - "@bob:localhost": { - "ed25519:/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY": "6vGDbPO5XzlcwbU3aV+kcck+iHHEBtX85ow2gW5U05/DZdtda/JNVa5Nn7B9lQHNnnrMqt1sX00y/JrIkSS1Aw", - "ed25519:GYKSNAWLVK": "jLxmUPr0Ny2Ai9+NGKGhed9BAuKikOc7r6gr7MQVawePYS95w8NJ8Tzaq9zFFOmIiojACNdQ/ksy3QAdwD6vBQ" - } - }, - "usage": [ - "master" - ], - "user_id": "@bob:localhost" - } - }) + master_keys(&KeysQueryUser::bob_a()) } pub fn ssk_a() -> Value { - json!({ - "@bob:localhost": { - "keys": { - "ed25519:dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw": "dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw" - }, - "signatures": { - "@bob:localhost": { - "ed25519:/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY": "7md6mwjUK8zjintmffJ0+kImC59/Y8PdySy99EZz5Neu+VMX3LT7txhKO2gC/hmDduRw+JGfGXIiDxR7GmQqDw" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@bob:localhost" - } - }) + self_signing_keys(&KeysQueryUser::bob_a()) } + /// A key query with an identity (Ia), and a first device `GYKSNAWLVK` /// signed by Ia. pub fn key_query_with_identity_a() -> KeyQueryResponse { - let data = json!({ - "device_keys": { - "@bob:localhost": { - "GYKSNAWLVK": Self::device_keys_payload_1_signed_by_a() - } - }, - "failures": {}, - "master_keys": Self::msk_a(), - "self_signing_keys": Self::ssk_a(), - "user_signing_keys": {} - }); - ruma_response_from_json(&data) + keys_query(&KeysQueryUser::bob_a(), &[]) } pub fn msk_b() -> Value { - json!({ - "@bob:localhost": { - "keys": { - "ed25519:NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4": "NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4" - }, - "signatures": { - "@bob:localhost": { - "ed25519:ATWKQFSFRN": "MBOzCKYPQLQMpBY2lFZJ4c8451xJfQCdhPBb1AHlTUSxKFiWi6V+k1oRRnhQein/PjkIY7ZO+HoOrIeOtbRMAw", - "ed25519:NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4": "xqLhC3sIUci1W2CNVW7HZWXreQApgjv2RDwB0WPiMd1P4vbZ/qJM0KWqK2piGPWliPi8YVREMrg216KXM3IhCA" - } - }, - "usage": [ - "master" - ], - "user_id": "@bob:localhost" - } - }) + master_keys(&KeysQueryUser::bob_b()) } pub fn ssk_b() -> Value { - json!({ - "@bob:localhost": { - "keys": { - "ed25519:At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc": "At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc" - }, - "signatures": { - "@bob:localhost": { - "ed25519:NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4": "Ls6CeoA4LoPCHuSwG96kbhd1dEV09TgdMROIZi6vFz/MT9Wtik6joQi/tQ3zCwIZCSR53ksLO4jG1DD31AiBAA" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@bob:localhost" - } - }) + self_signing_keys(&KeysQueryUser::bob_b()) } pub fn device_keys_payload_2_signed_by_b() -> Value { - json!({ - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "ATWKQFSFRN", - "keys": { - "curve25519:ATWKQFSFRN": "CY0TWVK1/Kj3ZADuBcGe3UKvpT+IKAPMUsMeJhSDqno", - "ed25519:ATWKQFSFRN": "TyTQqd6j2JlWZh97r+kTYuCbvqnPoNwO6EGovYsjY00" - }, - "signatures": { - "@bob:localhost": { - "ed25519:ATWKQFSFRN": "BQ9Gp0p+6srF+c8OyruqKKd9R4yaub3THYAyyBB/7X/rG8BwcAqFynzl1aGyFYun4Q+087a5OSiglCXI+/kQAA", - "ed25519:At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc": "TWmDPaG7t0rZ6luauonELD3dmBDTIRryqXhgsIQRiGint2rJdic8RVyZ6a61bgu6mtBjfvU3prqMNp6sVi16Cg" - } - }, - "user_id": "@bob:localhost", - }) + device_keys_payload(&KeysQueryUser::bob_b()) } + /// A key query with a new identity (Ib) and a new device `ATWKQFSFRN`. /// `ATWKQFSFRN` is signed with the new identity but `GYKSNAWLVK` is still /// signed by the old identity (Ia). pub fn key_query_with_identity_b() -> KeyQueryResponse { - let data = json!({ - "device_keys": { - "@bob:localhost": { - "ATWKQFSFRN": Self::device_keys_payload_2_signed_by_b(), - "GYKSNAWLVK": Self::device_keys_payload_1_signed_by_a(), - } - }, - "failures": {}, - "master_keys": Self::msk_b(), - "self_signing_keys": Self::ssk_b(), - }); - ruma_response_from_json(&data) + keys_query(&KeysQueryUser::bob_b(), &[KeysQueryUser::bob_a()]) } /// A key query with no identity and a new device `OPABMDDXGX` (not /// cross-signed). pub fn key_query_with_identity_no_identity() -> KeyQueryResponse { - let data = json!({ - "device_keys": { - "@bob:localhost": { - "ATWKQFSFRN": Self::device_keys_payload_2_signed_by_b(), - "GYKSNAWLVK": Self::device_keys_payload_1_signed_by_a(), - "OPABMDDXGX": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "OPABMDDXGX", - "keys": { - "curve25519:OPABMDDXGX": "O6bwa9Op0E+PQPCrbTOfdYwU+j95RRPhXIHuNpe94ns", - "ed25519:OPABMDDXGX": "DvjkSNOM9XrR1gWrr2YSDvTnwnLIgKDMRr5v8HgMKak" - }, - "signatures": { - "@bob:localhost": { - "ed25519:OPABMDDXGX": "o+BBnw/SIJWxSf799Adq6jEl9X3lwCg5MJkS8GlfId+pW3ReEETK0l+9bhCAgBsNSKRtB/fmZQBhjMx4FJr+BA" - } - }, - "user_id": "@bob:localhost", - } - } - }, - "failures": {}, - }); - ruma_response_from_json(&data) + keys_query(&KeysQueryUser::bob_c(), &[KeysQueryUser::bob_a(), KeysQueryUser::bob_b()]) } } diff --git a/testing/matrix-sdk-test/src/test_json/mod.rs b/testing/matrix-sdk-test/src/test_json/mod.rs index beab57cc64e..f3c677c76f4 100644 --- a/testing/matrix-sdk-test/src/test_json/mod.rs +++ b/testing/matrix-sdk-test/src/test_json/mod.rs @@ -10,6 +10,7 @@ use serde_json::{json, Value as JsonValue}; use crate::DEFAULT_TEST_ROOM_ID; pub mod api_responses; +pub mod keys_query; pub mod keys_query_sets; pub mod members; pub mod search_users; From 91fa1669becae268f3577317947abfcd8d76b4ec Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 22 Oct 2024 12:17:10 +0100 Subject: [PATCH 386/979] refactor(crypto) Rename device methods in IdentityChangeDataSet to match identity names --- .../src/identities/manager.rs | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index bd43f005a27..e4d24aeeea3 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -2098,12 +2098,8 @@ pub(crate) mod tests { // We should now have an identity for the user but no pin violation // (pinned master key is the current one) assert!(!other_identity.has_pin_violation()); - let first_device = manager - .store - .get_device_data(other_user, DataSet::first_device_id()) - .await - .unwrap() - .unwrap(); + let first_device = + manager.store.get_device_data(other_user, DataSet::device_a()).await.unwrap().unwrap(); assert!(first_device.is_cross_signed_by_owner(&identity)); // We receive a new keys update for that user, with a new identity @@ -2122,23 +2118,15 @@ pub(crate) mod tests { // violation assert!(other_identity.has_pin_violation()); - let second_device = manager - .store - .get_device_data(other_user, DataSet::second_device_id()) - .await - .unwrap() - .unwrap(); + let second_device = + manager.store.get_device_data(other_user, DataSet::device_b()).await.unwrap().unwrap(); // There is a new device signed by the new identity assert!(second_device.is_cross_signed_by_owner(&identity)); // The first device should not be signed by the new identity - let first_device = manager - .store - .get_device_data(other_user, DataSet::first_device_id()) - .await - .unwrap() - .unwrap(); + let first_device = + manager.store.get_device_data(other_user, DataSet::device_a()).await.unwrap().unwrap(); assert!(!first_device.is_cross_signed_by_owner(&identity)); let remember_previous_identity = other_identity.clone(); From 5f0ba1e7df8c0146045159171e23507a6df5ad69 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 22 Oct 2024 12:34:20 +0100 Subject: [PATCH 387/979] refactor(crypto) Avoid msk and ssk abbreviations in test data --- crates/matrix-sdk-crypto/src/identities/user.rs | 4 ++-- crates/matrix-sdk/src/room/identity_status_changes.rs | 8 ++++---- testing/matrix-sdk-test/src/test_json/keys_query_sets.rs | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 02c080a413b..bbfdf54c4ee 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -1757,8 +1757,8 @@ pub(crate) mod tests { my_id, my_user_id, other_user_id, - DataSet::msk_b(), - DataSet::ssk_b(), + DataSet::master_signing_keys_b(), + DataSet::self_signing_keys_b(), ); machine.mark_request_as_sent(&TransactionId::new(), &kq_response).await.unwrap(); diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index 41d043739e4..6e7a8602d39 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -334,8 +334,8 @@ mod tests { // And Bob is in verification violation t.verify_bob_with( IdentityChangeDataSet::key_query_with_identity_b(), - IdentityChangeDataSet::msk_b(), - IdentityChangeDataSet::ssk_b(), + IdentityChangeDataSet::master_signing_keys_b(), + IdentityChangeDataSet::self_signing_keys_b(), ) .await; t.unpin_bob().await; @@ -722,8 +722,8 @@ mod tests { pub(super) async fn verify_bob(&self) { self.verify_bob_with( IdentityChangeDataSet::key_query_with_identity_a(), - IdentityChangeDataSet::msk_a(), - IdentityChangeDataSet::ssk_a(), + IdentityChangeDataSet::master_signing_keys_a(), + IdentityChangeDataSet::self_signing_keys_a(), ) .await; } diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index d0ac75929c4..1ee8591162c 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -472,11 +472,11 @@ impl IdentityChangeDataSet { KeysQueryUser::bob_c().device_id } - pub fn msk_a() -> Value { + pub fn master_signing_keys_a() -> Value { master_keys(&KeysQueryUser::bob_a()) } - pub fn ssk_a() -> Value { + pub fn self_signing_keys_a() -> Value { self_signing_keys(&KeysQueryUser::bob_a()) } @@ -486,11 +486,11 @@ impl IdentityChangeDataSet { keys_query(&KeysQueryUser::bob_a(), &[]) } - pub fn msk_b() -> Value { + pub fn master_signing_keys_b() -> Value { master_keys(&KeysQueryUser::bob_b()) } - pub fn ssk_b() -> Value { + pub fn self_signing_keys_b() -> Value { self_signing_keys(&KeysQueryUser::bob_b()) } From c48bb13159c90ee83b620bb91aacc6a4df6b36f3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 29 Oct 2024 10:39:42 +0100 Subject: [PATCH 388/979] doc: Deal with paragraphes in trailers (#4179) Git trailers have a funny format. --------- Signed-off-by: Ivan Enderlin Co-authored-by: Benjamin Bouvier --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f56afd7618e..2e00b7eafad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,12 +93,14 @@ stringify Ed25519 and thus present them to users. It's also commonly used when Ed25519 keys need to be inserted into JSON. Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to -stringify the Ed25519 public key. + stringify the Ed25519 public key. ``` In this commit message, the content specified in the `Changelog` trailer will be used for the changelog entry. +Be careful to add at least one whitespace after new lines to create a paragraph. + ### Security fixes Commits addressing security vulnerabilities must include specific trailers for From b66024c3864e15dc60c862d6841cf5876173ed49 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 29 Oct 2024 10:33:50 +0100 Subject: [PATCH 389/979] test: Update Synapse from 1.115 to 1.117. This patch updates Synapse in our CI infrastructure and in the `matrix-sdk-integration-testing` crate. --- .github/workflows/ci.yml | 2 +- .github/workflows/coverage.yml | 2 +- testing/matrix-sdk-integration-testing/assets/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0676374898a..5fbe6964e6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -371,7 +371,7 @@ jobs: # tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the # latter does not provide networking for services to communicate with it. synapse: - image: ghcr.io/matrix-org/synapse-service:v1.115.0 # keep in sync with ./coverage.yml + image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./coverage.yml env: SYNAPSE_COMPLEMENT_DATABASE: sqlite SERVER_NAME: synapse diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8ab837cd2ad..8b1408582a9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -49,7 +49,7 @@ jobs: # tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the # latter does not provide networking for services to communicate with it. synapse: - image: ghcr.io/matrix-org/synapse-service:v1.115.0 # keep in sync with ./ci.yml + image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./ci.yml env: SYNAPSE_COMPLEMENT_DATABASE: sqlite SERVER_NAME: synapse diff --git a/testing/matrix-sdk-integration-testing/assets/Dockerfile b/testing/matrix-sdk-integration-testing/assets/Dockerfile index e34ca68958a..5032ac7a20e 100644 --- a/testing/matrix-sdk-integration-testing/assets/Dockerfile +++ b/testing/matrix-sdk-integration-testing/assets/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/matrixdotorg/synapse:v1.115.0 +FROM docker.io/matrixdotorg/synapse:v1.117.0 ADD ci-start.sh /ci-start.sh RUN chmod 770 /ci-start.sh ENTRYPOINT /ci-start.sh From 2f19e2b762a0dafdb7a1c72c0ca00bf2bf297e8f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 28 Oct 2024 15:27:23 +0100 Subject: [PATCH 390/979] chore(sdk): Rename `event_cache/store.rs` to `event_cache/room/events.rs`. This patch renames the `store.rs` file to `room/events.rs`. --- crates/matrix-sdk/src/event_cache/mod.rs | 4 ++-- crates/matrix-sdk/src/event_cache/pagination.rs | 6 +++--- .../src/event_cache/{store.rs => room/events.rs} | 11 ++--------- crates/matrix-sdk/src/event_cache/room/mod.rs | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 14 deletions(-) rename crates/matrix-sdk/src/event_cache/{store.rs => room/events.rs} (98%) create mode 100644 crates/matrix-sdk/src/event_cache/room/mod.rs diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 391c6d71012..3d50627ac2f 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -58,13 +58,13 @@ use tracing::{error, info_span, instrument, trace, warn, Instrument as _, Span}; use self::{ paginator::PaginatorError, - store::{Gap, RoomEvents}, + room::events::{Gap, RoomEvents}, }; use crate::{client::WeakClient, room::WeakRoom, Client}; mod linked_chunk; mod pagination; -mod store; +mod room; pub mod paginator; pub use pagination::{RoomPagination, TimelineHasBeenResetWhilePaginating}; diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 74e943aaebd..ce994048c24 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -22,11 +22,11 @@ use tokio::time::timeout; use tracing::{debug, instrument, trace}; use super::{ + linked_chunk::ChunkContent, paginator::{PaginationResult, PaginatorState}, - store::Gap, + room::events::{Gap, RoomEvents}, BackPaginationOutcome, Result, RoomEventCacheInner, }; -use crate::event_cache::{linked_chunk::ChunkContent, store::RoomEvents}; /// An API object to run pagination queries on a [`super::RoomEventCache`]. /// @@ -321,7 +321,7 @@ mod tests { use tokio::{spawn, time::sleep}; use crate::{ - deserialized_responses::SyncTimelineEvent, event_cache::store::Gap, + deserialized_responses::SyncTimelineEvent, event_cache::room::events::Gap, test_utils::logged_in_client, }; diff --git a/crates/matrix-sdk/src/event_cache/store.rs b/crates/matrix-sdk/src/event_cache/room/events.rs similarity index 98% rename from crates/matrix-sdk/src/event_cache/store.rs rename to crates/matrix-sdk/src/event_cache/room/events.rs index eb7e771dbd4..f1b4eba7a0a 100644 --- a/crates/matrix-sdk/src/event_cache/store.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; -use super::linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}; +use super::super::linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}; /// An alias for the real event type. pub(crate) type Event = SyncTimelineEvent; @@ -31,6 +29,7 @@ pub struct Gap { const DEFAULT_CHUNK_CAPACITY: usize = 128; /// This type represents all events of a single room. +#[derive(Debug)] pub struct RoomEvents { /// The real in-memory storage for all the events. chunks: LinkedChunk, @@ -132,12 +131,6 @@ impl RoomEvents { } } -impl fmt::Debug for RoomEvents { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - formatter.debug_struct("RoomEvents").field("chunk", &self.chunks).finish() - } -} - #[cfg(test)] mod tests { use assert_matches2::assert_let; diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs new file mode 100644 index 00000000000..0fbadbfee17 --- /dev/null +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(super) mod events; From e87bed8ef44787500a7df8ee7945da698ee9c66c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 28 Oct 2024 16:02:27 +0100 Subject: [PATCH 391/979] chore(sdk): Move all `RoomEventCache` types from `mod.rs` to `room/mod.rs`. --- crates/matrix-sdk/src/event_cache/mod.rs | 532 +---------------- .../matrix-sdk/src/event_cache/pagination.rs | 7 +- crates/matrix-sdk/src/event_cache/room/mod.rs | 542 ++++++++++++++++++ 3 files changed, 554 insertions(+), 527 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 3d50627ac2f..605c1967e71 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -36,31 +36,22 @@ use std::{ use eyeball::Subscriber; use matrix_sdk_base::{ deserialized_responses::{AmbiguityChange, SyncTimelineEvent, TimelineEvent}, - sync::{JoinedRoomUpdate, LeftRoomUpdate, RoomUpdates, Timeline}, + sync::RoomUpdates, }; use matrix_sdk_common::executor::{spawn, JoinHandle}; -use paginator::{Paginator, PaginatorState}; use ruma::{ - events::{ - relation::RelationType, - room::{message::Relation, redaction::SyncRoomRedactionEvent}, - AnyMessageLikeEventContent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent, - AnySyncMessageLikeEvent, AnySyncTimelineEvent, - }, + events::{relation::RelationType, AnySyncEphemeralRoomEvent}, serde::Raw, EventId, OwnedEventId, OwnedRoomId, RoomId, }; use tokio::sync::{ - broadcast::{error::RecvError, Receiver, Sender}, - Mutex, Notify, RwLock, RwLockReadGuard, RwLockWriteGuard, + broadcast::{error::RecvError, Receiver}, + Mutex, RwLock, }; use tracing::{error, info_span, instrument, trace, warn, Instrument as _, Span}; -use self::{ - paginator::PaginatorError, - room::events::{Gap, RoomEvents}, -}; -use crate::{client::WeakClient, room::WeakRoom, Client}; +use self::paginator::PaginatorError; +use crate::{client::WeakClient, Client}; mod linked_chunk; mod pagination; @@ -68,6 +59,7 @@ mod room; pub mod paginator; pub use pagination::{RoomPagination, TimelineHasBeenResetWhilePaginating}; +pub use room::RoomEventCache; /// An error observed in the [`EventCache`]. #[derive(thiserror::Error, Debug)] @@ -443,516 +435,6 @@ impl EventCacheInner { } } -/// A subset of an event cache, for a room. -/// -/// Cloning is shallow, and thus is cheap to do. -#[derive(Clone)] -pub struct RoomEventCache { - inner: Arc, -} - -impl Debug for RoomEventCache { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RoomEventCache").finish_non_exhaustive() - } -} - -impl RoomEventCache { - /// Create a new [`RoomEventCache`] using the given room and store. - fn new( - client: WeakClient, - room_id: OwnedRoomId, - all_events_cache: Arc>, - ) -> Self { - Self { inner: Arc::new(RoomEventCacheInner::new(client, room_id, all_events_cache)) } - } - - /// Subscribe to room updates for this room, after getting the initial list - /// of events. XXX: Could/should it use some kind of `Observable` - /// instead? Or not something async, like explicit handlers as our event - /// handlers? - pub async fn subscribe( - &self, - ) -> Result<(Vec, Receiver)> { - let state = self.inner.state.read().await; - let events = state.events.events().map(|(_position, item)| item.clone()).collect(); - - Ok((events, self.inner.sender.subscribe())) - } - - /// Return a [`RoomPagination`] API object useful for running - /// back-pagination queries in the current room. - pub fn pagination(&self) -> RoomPagination { - RoomPagination { inner: self.inner.clone() } - } - - /// Try to find an event by id in this room. - pub async fn event(&self, event_id: &EventId) -> Option { - if let Some((room_id, event)) = - self.inner.all_events.read().await.events.get(event_id).cloned() - { - if room_id == self.inner.room_id { - return Some(event); - } - } - - let state = self.inner.state.read().await; - for (_pos, event) in state.events.revents() { - if event.event_id().as_deref() == Some(event_id) { - return Some(event.clone()); - } - } - None - } - - /// Try to find an event by id in this room, along with its related events. - /// - /// You can filter which types of related events to retrieve using - /// `filter`. `None` will retrieve related events of any type. - pub async fn event_with_relations( - &self, - event_id: &EventId, - filter: Option>, - ) -> Option<(SyncTimelineEvent, Vec)> { - let mut relation_events = Vec::new(); - - let cache = self.inner.all_events.read().await; - if let Some((_, event)) = cache.events.get(event_id) { - Self::collect_related_events(&cache, event_id, &filter, &mut relation_events); - Some((event.clone(), relation_events)) - } else { - None - } - } - - /// Looks for related event ids for the passed event id, and appends them to - /// the `results` parameter. Then it'll recursively get the related - /// event ids for those too. - fn collect_related_events( - cache: &RwLockReadGuard<'_, AllEventsCache>, - event_id: &EventId, - filter: &Option>, - results: &mut Vec, - ) { - if let Some(related_event_ids) = cache.relations.get(event_id) { - for (related_event_id, relation_type) in related_event_ids { - if let Some(filter) = filter { - if !filter.contains(relation_type) { - continue; - } - } - - // If the event was already added to the related ones, skip it. - if results.iter().any(|e| { - e.event_id().is_some_and(|added_related_event_id| { - added_related_event_id == *related_event_id - }) - }) { - continue; - } - if let Some((_, ev)) = cache.events.get(related_event_id) { - results.push(ev.clone()); - Self::collect_related_events(cache, related_event_id, filter, results); - } - } - } - } - - /// Save a single event in the event cache, for further retrieval with - /// [`Self::event`]. - // TODO: This doesn't insert the event into the linked chunk. In the future - // there'll be no distinction between the linked chunk and the separate - // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. - pub(crate) async fn save_event(&self, event: SyncTimelineEvent) { - if let Some(event_id) = event.event_id() { - let mut cache = self.inner.all_events.write().await; - - self.inner.append_related_event(&mut cache, &event); - cache.events.insert(event_id, (self.inner.room_id.clone(), event)); - } else { - warn!("couldn't save event without event id in the event cache"); - } - } - - /// Save some events in the event cache, for further retrieval with - /// [`Self::event`]. This function will save them using a single lock, - /// as opposed to [`Self::save_event`]. - // TODO: This doesn't insert the event into the linked chunk. In the future - // there'll be no distinction between the linked chunk and the separate - // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. - pub(crate) async fn save_events(&self, events: impl IntoIterator) { - let mut cache = self.inner.all_events.write().await; - for event in events { - if let Some(event_id) = event.event_id() { - self.inner.append_related_event(&mut cache, &event); - cache.events.insert(event_id, (self.inner.room_id.clone(), event)); - } else { - warn!("couldn't save event without event id in the event cache"); - } - } - } -} - -/// State for a single room's event cache. -/// -/// This contains all inner mutable state that ought to be updated at the same -/// time. -struct RoomEventCacheState { - /// The events of the room. - events: RoomEvents, - - /// Have we ever waited for a previous-batch-token to come from sync, in the - /// context of pagination? We do this at most once per room, the first - /// time we try to run backward pagination. We reset that upon clearing - /// the timeline events. - waited_for_initial_prev_token: bool, -} - -impl RoomEventCacheState { - /// Resets this data structure as if it were brand new. - fn reset(&mut self) { - self.events.reset(); - self.waited_for_initial_prev_token = false; - } -} - -/// The (non-cloneable) details of the `RoomEventCache`. -struct RoomEventCacheInner { - /// The room id for this room. - room_id: OwnedRoomId, - - /// Sender part for subscribers to this room. - sender: Sender, - - /// State for this room's event cache. - state: RwLock, - - /// See comment of [`EventCacheInner::all_events`]. - /// - /// This is shared between the [`EventCacheInner`] singleton and all - /// [`RoomEventCacheInner`] instances. - all_events: Arc>, - - /// A notifier that we received a new pagination token. - pub pagination_batch_token_notifier: Notify, - - /// A paginator instance, that's configured to run back-pagination on our - /// behalf. - /// - /// Note: forward-paginations are still run "out-of-band", that is, - /// disconnected from the event cache, as we don't implement matching - /// events received from those kinds of pagination with the cache. This - /// paginator is only used for queries that interact with the actual event - /// cache. - pub paginator: Paginator, -} - -impl RoomEventCacheInner { - /// Creates a new cache for a room, and subscribes to room updates, so as - /// to handle new timeline events. - fn new( - client: WeakClient, - room_id: OwnedRoomId, - all_events_cache: Arc>, - ) -> Self { - let sender = Sender::new(32); - - let weak_room = WeakRoom::new(client, room_id); - - Self { - room_id: weak_room.room_id().to_owned(), - state: RwLock::new(RoomEventCacheState { - events: RoomEvents::default(), - waited_for_initial_prev_token: false, - }), - all_events: all_events_cache, - sender, - pagination_batch_token_notifier: Default::default(), - paginator: Paginator::new(weak_room), - } - } - - fn handle_account_data(&self, account_data: Vec>) { - let mut handled_read_marker = false; - - trace!("Handling account data"); - for raw_event in account_data { - match raw_event.deserialize() { - Ok(AnyRoomAccountDataEvent::FullyRead(ev)) => { - // Sometimes the sliding sync proxy sends many duplicates of the read marker - // event. Don't forward it multiple times to avoid clutter - // the update channel. - // - // NOTE: SS proxy workaround. - if handled_read_marker { - continue; - } - - handled_read_marker = true; - - // Propagate to observers. (We ignore the error if there aren't any.) - let _ = self.sender.send(RoomEventCacheUpdate::MoveReadMarkerTo { - event_id: ev.content.event_id, - }); - } - - Ok(_) => { - // We're not interested in other room account data updates, - // at this point. - } - - Err(e) => { - let event_type = raw_event.get_field::("type").ok().flatten(); - warn!(event_type, "Failed to deserialize account data: {e}"); - } - } - } - } - - async fn handle_joined_room_update(&self, updates: JoinedRoomUpdate) -> Result<()> { - self.handle_timeline( - updates.timeline, - updates.ephemeral.clone(), - updates.ambiguity_changes, - ) - .await?; - - self.handle_account_data(updates.account_data); - - Ok(()) - } - - async fn handle_timeline( - &self, - timeline: Timeline, - ephemeral_events: Vec>, - ambiguity_changes: BTreeMap, - ) -> Result<()> { - if timeline.limited { - // Ideally we'd try to reconcile existing events against those received in the - // timeline, but we're not there yet. In the meanwhile, clear the - // items from the room. TODO: implement Smart Matching™. - trace!("limited timeline, clearing all previous events and pushing new events"); - - self.replace_all_events_by( - timeline.events, - timeline.prev_batch, - ephemeral_events, - ambiguity_changes, - ) - .await?; - } else { - // Add all the events to the backend. - trace!("adding new events"); - - self.append_new_events( - timeline.events, - timeline.prev_batch, - ephemeral_events, - ambiguity_changes, - ) - .await?; - } - - Ok(()) - } - - async fn handle_left_room_update(&self, updates: LeftRoomUpdate) -> Result<()> { - self.handle_timeline(updates.timeline, Vec::new(), updates.ambiguity_changes).await?; - Ok(()) - } - - /// Remove existing events, and append a set of events to the room cache and - /// storage, notifying observers. - async fn replace_all_events_by( - &self, - sync_timeline_events: Vec, - prev_batch: Option, - ephemeral_events: Vec>, - ambiguity_changes: BTreeMap, - ) -> Result<()> { - // Acquire the lock. - let mut state = self.state.write().await; - - // Reset the room's state. - state.reset(); - - // Propagate to observers. - let _ = self.sender.send(RoomEventCacheUpdate::Clear); - - // Push the new events. - self.append_events_locked_impl( - &mut state.events, - sync_timeline_events, - prev_batch.clone(), - ephemeral_events, - ambiguity_changes, - ) - .await?; - - // Reset the paginator status to initial. - self.paginator.set_idle_state(PaginatorState::Initial, prev_batch, None)?; - - Ok(()) - } - - /// Append a set of events to the room cache and storage, notifying - /// observers. - async fn append_new_events( - &self, - sync_timeline_events: Vec, - prev_batch: Option, - ephemeral_events: Vec>, - ambiguity_changes: BTreeMap, - ) -> Result<()> { - self.append_events_locked_impl( - &mut self.state.write().await.events, - sync_timeline_events, - prev_batch, - ephemeral_events, - ambiguity_changes, - ) - .await - } - - /// If the event is related to another one, its id is added to the - /// relations map. - fn append_related_event( - &self, - cache: &mut RwLockWriteGuard<'_, AllEventsCache>, - event: &SyncTimelineEvent, - ) { - // Handle and cache events and relations. - if let Ok(AnySyncTimelineEvent::MessageLike(ev)) = event.raw().deserialize() { - // Handle redactions separately, as their logic is slightly different. - if let AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(ev)) = - &ev - { - if let Some(redacted_event_id) = ev.content.redacts.as_ref().or(ev.redacts.as_ref()) - { - cache - .relations - .entry(redacted_event_id.to_owned()) - .or_default() - .insert(ev.event_id.to_owned(), RelationType::Replacement); - } - } else { - let relationship = match ev.original_content() { - Some(AnyMessageLikeEventContent::RoomMessage(c)) => { - if let Some(relation) = c.relates_to { - match relation { - Relation::Replacement(replacement) => { - Some((replacement.event_id, RelationType::Replacement)) - } - Relation::Reply { in_reply_to } => { - Some((in_reply_to.event_id, RelationType::Reference)) - } - Relation::Thread(thread) => { - Some((thread.event_id, RelationType::Thread)) - } - // Do nothing for custom - _ => None, - } - } else { - None - } - } - Some(AnyMessageLikeEventContent::PollResponse(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::PollEnd(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::UnstablePollResponse(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::UnstablePollEnd(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::Reaction(c)) => { - Some((c.relates_to.event_id, RelationType::Annotation)) - } - _ => None, - }; - - if let Some(relationship) = relationship { - cache - .relations - .entry(relationship.0) - .or_default() - .insert(ev.event_id().to_owned(), relationship.1); - } - } - } - } - - /// Append a set of events and associated room data. - /// - /// This is a private implementation. It must not be exposed publicly. - async fn append_events_locked_impl( - &self, - room_events: &mut RoomEvents, - sync_timeline_events: Vec, - prev_batch: Option, - ephemeral_events: Vec>, - ambiguity_changes: BTreeMap, - ) -> Result<()> { - if sync_timeline_events.is_empty() - && prev_batch.is_none() - && ephemeral_events.is_empty() - && ambiguity_changes.is_empty() - { - return Ok(()); - } - - // Add the previous back-pagination token (if present), followed by the timeline - // events themselves. - { - if let Some(prev_token) = &prev_batch { - room_events.push_gap(Gap { prev_token: prev_token.clone() }); - } - - room_events.push_events(sync_timeline_events.clone()); - - let mut cache = self.all_events.write().await; - for ev in &sync_timeline_events { - if let Some(event_id) = ev.event_id() { - self.append_related_event(&mut cache, ev); - cache.events.insert(event_id.to_owned(), (self.room_id.clone(), ev.clone())); - } - } - } - - // Now that all events have been added, we can trigger the - // `pagination_token_notifier`. - if prev_batch.is_some() { - self.pagination_batch_token_notifier.notify_one(); - } - - // The order of `RoomEventCacheUpdate`s is **really** important here. - { - if !sync_timeline_events.is_empty() { - let _ = self.sender.send(RoomEventCacheUpdate::AddTimelineEvents { - events: sync_timeline_events, - origin: EventsOrigin::Sync, - }); - } - - if !ephemeral_events.is_empty() { - let _ = self - .sender - .send(RoomEventCacheUpdate::AddEphemeralEvents { events: ephemeral_events }); - } - - if !ambiguity_changes.is_empty() { - let _ = self.sender.send(RoomEventCacheUpdate::UpdateMembers { ambiguity_changes }); - } - } - - Ok(()) - } -} - /// The result of a single back-pagination request. #[derive(Debug)] pub struct BackPaginationOutcome { diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index ce994048c24..7eb5d62a84e 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -24,8 +24,11 @@ use tracing::{debug, instrument, trace}; use super::{ linked_chunk::ChunkContent, paginator::{PaginationResult, PaginatorState}, - room::events::{Gap, RoomEvents}, - BackPaginationOutcome, Result, RoomEventCacheInner, + room::{ + events::{Gap, RoomEvents}, + RoomEventCacheInner, + }, + BackPaginationOutcome, Result, }; /// An API object to run pagination queries on a [`super::RoomEventCache`]. diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 0fbadbfee17..67be8b68784 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -12,4 +12,546 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! All event cache types for a single room. + +use std::{collections::BTreeMap, fmt, sync::Arc}; + +use events::{Gap, RoomEvents}; +use matrix_sdk_base::{ + deserialized_responses::{AmbiguityChange, SyncTimelineEvent}, + sync::{JoinedRoomUpdate, LeftRoomUpdate, Timeline}, +}; +use ruma::{ + events::{ + relation::RelationType, + room::{message::Relation, redaction::SyncRoomRedactionEvent}, + AnyMessageLikeEventContent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent, + AnySyncMessageLikeEvent, AnySyncTimelineEvent, + }, + serde::Raw, + EventId, OwnedEventId, OwnedRoomId, +}; +use tokio::sync::{ + broadcast::{Receiver, Sender}, + Notify, RwLock, RwLockReadGuard, RwLockWriteGuard, +}; +use tracing::{trace, warn}; + +use super::{ + paginator::{Paginator, PaginatorState}, + AllEventsCache, EventsOrigin, Result, RoomEventCacheUpdate, RoomPagination, +}; +use crate::{client::WeakClient, room::WeakRoom}; + pub(super) mod events; + +/// A subset of an event cache, for a room. +/// +/// Cloning is shallow, and thus is cheap to do. +#[derive(Clone)] +pub struct RoomEventCache { + pub(super) inner: Arc, +} + +impl fmt::Debug for RoomEventCache { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RoomEventCache").finish_non_exhaustive() + } +} + +impl RoomEventCache { + /// Create a new [`RoomEventCache`] using the given room and store. + pub(super) fn new( + client: WeakClient, + room_id: OwnedRoomId, + all_events_cache: Arc>, + ) -> Self { + Self { inner: Arc::new(RoomEventCacheInner::new(client, room_id, all_events_cache)) } + } + + /// Subscribe to room updates for this room, after getting the initial list + /// of events. XXX: Could/should it use some kind of `Observable` + /// instead? Or not something async, like explicit handlers as our event + /// handlers? + pub async fn subscribe( + &self, + ) -> Result<(Vec, Receiver)> { + let state = self.inner.state.read().await; + let events = state.events.events().map(|(_position, item)| item.clone()).collect(); + + Ok((events, self.inner.sender.subscribe())) + } + + /// Return a [`RoomPagination`] API object useful for running + /// back-pagination queries in the current room. + pub fn pagination(&self) -> RoomPagination { + RoomPagination { inner: self.inner.clone() } + } + + /// Try to find an event by id in this room. + pub async fn event(&self, event_id: &EventId) -> Option { + if let Some((room_id, event)) = + self.inner.all_events.read().await.events.get(event_id).cloned() + { + if room_id == self.inner.room_id { + return Some(event); + } + } + + let state = self.inner.state.read().await; + for (_pos, event) in state.events.revents() { + if event.event_id().as_deref() == Some(event_id) { + return Some(event.clone()); + } + } + None + } + + /// Try to find an event by id in this room, along with its related events. + /// + /// You can filter which types of related events to retrieve using + /// `filter`. `None` will retrieve related events of any type. + pub async fn event_with_relations( + &self, + event_id: &EventId, + filter: Option>, + ) -> Option<(SyncTimelineEvent, Vec)> { + let mut relation_events = Vec::new(); + + let cache = self.inner.all_events.read().await; + if let Some((_, event)) = cache.events.get(event_id) { + Self::collect_related_events(&cache, event_id, &filter, &mut relation_events); + Some((event.clone(), relation_events)) + } else { + None + } + } + + /// Looks for related event ids for the passed event id, and appends them to + /// the `results` parameter. Then it'll recursively get the related + /// event ids for those too. + fn collect_related_events( + cache: &RwLockReadGuard<'_, AllEventsCache>, + event_id: &EventId, + filter: &Option>, + results: &mut Vec, + ) { + if let Some(related_event_ids) = cache.relations.get(event_id) { + for (related_event_id, relation_type) in related_event_ids { + if let Some(filter) = filter { + if !filter.contains(relation_type) { + continue; + } + } + + // If the event was already added to the related ones, skip it. + if results.iter().any(|e| { + e.event_id().is_some_and(|added_related_event_id| { + added_related_event_id == *related_event_id + }) + }) { + continue; + } + if let Some((_, ev)) = cache.events.get(related_event_id) { + results.push(ev.clone()); + Self::collect_related_events(cache, related_event_id, filter, results); + } + } + } + } + + /// Save a single event in the event cache, for further retrieval with + /// [`Self::event`]. + // TODO: This doesn't insert the event into the linked chunk. In the future + // there'll be no distinction between the linked chunk and the separate + // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. + pub(crate) async fn save_event(&self, event: SyncTimelineEvent) { + if let Some(event_id) = event.event_id() { + let mut cache = self.inner.all_events.write().await; + + self.inner.append_related_event(&mut cache, &event); + cache.events.insert(event_id, (self.inner.room_id.clone(), event)); + } else { + warn!("couldn't save event without event id in the event cache"); + } + } + + /// Save some events in the event cache, for further retrieval with + /// [`Self::event`]. This function will save them using a single lock, + /// as opposed to [`Self::save_event`]. + // TODO: This doesn't insert the event into the linked chunk. In the future + // there'll be no distinction between the linked chunk and the separate + // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. + pub(crate) async fn save_events(&self, events: impl IntoIterator) { + let mut cache = self.inner.all_events.write().await; + for event in events { + if let Some(event_id) = event.event_id() { + self.inner.append_related_event(&mut cache, &event); + cache.events.insert(event_id, (self.inner.room_id.clone(), event)); + } else { + warn!("couldn't save event without event id in the event cache"); + } + } + } +} + +/// The (non-cloneable) details of the `RoomEventCache`. +pub(super) struct RoomEventCacheInner { + /// The room id for this room. + room_id: OwnedRoomId, + + /// Sender part for subscribers to this room. + pub sender: Sender, + + /// State for this room's event cache. + pub state: RwLock, + + /// See comment of [`EventCacheInner::all_events`]. + /// + /// This is shared between the [`EventCacheInner`] singleton and all + /// [`RoomEventCacheInner`] instances. + all_events: Arc>, + + /// A notifier that we received a new pagination token. + pub pagination_batch_token_notifier: Notify, + + /// A paginator instance, that's configured to run back-pagination on our + /// behalf. + /// + /// Note: forward-paginations are still run "out-of-band", that is, + /// disconnected from the event cache, as we don't implement matching + /// events received from those kinds of pagination with the cache. This + /// paginator is only used for queries that interact with the actual event + /// cache. + pub paginator: Paginator, +} + +impl RoomEventCacheInner { + /// Creates a new cache for a room, and subscribes to room updates, so as + /// to handle new timeline events. + fn new( + client: WeakClient, + room_id: OwnedRoomId, + all_events_cache: Arc>, + ) -> Self { + let sender = Sender::new(32); + + let weak_room = WeakRoom::new(client, room_id); + + Self { + room_id: weak_room.room_id().to_owned(), + state: RwLock::new(RoomEventCacheState { + events: RoomEvents::default(), + waited_for_initial_prev_token: false, + }), + all_events: all_events_cache, + sender, + pagination_batch_token_notifier: Default::default(), + paginator: Paginator::new(weak_room), + } + } + + fn handle_account_data(&self, account_data: Vec>) { + let mut handled_read_marker = false; + + trace!("Handling account data"); + + for raw_event in account_data { + match raw_event.deserialize() { + Ok(AnyRoomAccountDataEvent::FullyRead(ev)) => { + // Sometimes the sliding sync proxy sends many duplicates of the read marker + // event. Don't forward it multiple times to avoid clutter + // the update channel. + // + // NOTE: SS proxy workaround. + if handled_read_marker { + continue; + } + + handled_read_marker = true; + + // Propagate to observers. (We ignore the error if there aren't any.) + let _ = self.sender.send(RoomEventCacheUpdate::MoveReadMarkerTo { + event_id: ev.content.event_id, + }); + } + + Ok(_) => { + // We're not interested in other room account data updates, + // at this point. + } + + Err(e) => { + let event_type = raw_event.get_field::("type").ok().flatten(); + warn!(event_type, "Failed to deserialize account data: {e}"); + } + } + } + } + + pub(super) async fn handle_joined_room_update(&self, updates: JoinedRoomUpdate) -> Result<()> { + self.handle_timeline( + updates.timeline, + updates.ephemeral.clone(), + updates.ambiguity_changes, + ) + .await?; + + self.handle_account_data(updates.account_data); + + Ok(()) + } + + async fn handle_timeline( + &self, + timeline: Timeline, + ephemeral_events: Vec>, + ambiguity_changes: BTreeMap, + ) -> Result<()> { + if timeline.limited { + // Ideally we'd try to reconcile existing events against those received in the + // timeline, but we're not there yet. In the meanwhile, clear the + // items from the room. TODO: implement Smart Matching™. + trace!("limited timeline, clearing all previous events and pushing new events"); + + self.replace_all_events_by( + timeline.events, + timeline.prev_batch, + ephemeral_events, + ambiguity_changes, + ) + .await?; + } else { + // Add all the events to the backend. + trace!("adding new events"); + + self.append_new_events( + timeline.events, + timeline.prev_batch, + ephemeral_events, + ambiguity_changes, + ) + .await?; + } + + Ok(()) + } + + pub(super) async fn handle_left_room_update(&self, updates: LeftRoomUpdate) -> Result<()> { + self.handle_timeline(updates.timeline, Vec::new(), updates.ambiguity_changes).await?; + Ok(()) + } + + /// Remove existing events, and append a set of events to the room cache and + /// storage, notifying observers. + pub(super) async fn replace_all_events_by( + &self, + sync_timeline_events: Vec, + prev_batch: Option, + ephemeral_events: Vec>, + ambiguity_changes: BTreeMap, + ) -> Result<()> { + // Acquire the lock. + let mut state = self.state.write().await; + + // Reset the room's state. + state.reset(); + + // Propagate to observers. + let _ = self.sender.send(RoomEventCacheUpdate::Clear); + + // Push the new events. + self.append_events_locked_impl( + &mut state.events, + sync_timeline_events, + prev_batch.clone(), + ephemeral_events, + ambiguity_changes, + ) + .await?; + + // Reset the paginator status to initial. + self.paginator.set_idle_state(PaginatorState::Initial, prev_batch, None)?; + + Ok(()) + } + + /// Append a set of events to the room cache and storage, notifying + /// observers. + async fn append_new_events( + &self, + sync_timeline_events: Vec, + prev_batch: Option, + ephemeral_events: Vec>, + ambiguity_changes: BTreeMap, + ) -> Result<()> { + self.append_events_locked_impl( + &mut self.state.write().await.events, + sync_timeline_events, + prev_batch, + ephemeral_events, + ambiguity_changes, + ) + .await + } + + /// If the event is related to another one, its id is added to the + /// relations map. + fn append_related_event( + &self, + cache: &mut RwLockWriteGuard<'_, AllEventsCache>, + event: &SyncTimelineEvent, + ) { + // Handle and cache events and relations. + if let Ok(AnySyncTimelineEvent::MessageLike(ev)) = event.raw().deserialize() { + // Handle redactions separately, as their logic is slightly different. + if let AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(ev)) = + &ev + { + if let Some(redacted_event_id) = ev.content.redacts.as_ref().or(ev.redacts.as_ref()) + { + cache + .relations + .entry(redacted_event_id.to_owned()) + .or_default() + .insert(ev.event_id.to_owned(), RelationType::Replacement); + } + } else { + let relationship = match ev.original_content() { + Some(AnyMessageLikeEventContent::RoomMessage(c)) => { + if let Some(relation) = c.relates_to { + match relation { + Relation::Replacement(replacement) => { + Some((replacement.event_id, RelationType::Replacement)) + } + Relation::Reply { in_reply_to } => { + Some((in_reply_to.event_id, RelationType::Reference)) + } + Relation::Thread(thread) => { + Some((thread.event_id, RelationType::Thread)) + } + // Do nothing for custom + _ => None, + } + } else { + None + } + } + Some(AnyMessageLikeEventContent::PollResponse(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::PollEnd(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::UnstablePollResponse(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::UnstablePollEnd(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::Reaction(c)) => { + Some((c.relates_to.event_id, RelationType::Annotation)) + } + _ => None, + }; + + if let Some(relationship) = relationship { + cache + .relations + .entry(relationship.0) + .or_default() + .insert(ev.event_id().to_owned(), relationship.1); + } + } + } + } + + /// Append a set of events and associated room data. + /// + /// This is a private implementation. It must not be exposed publicly. + async fn append_events_locked_impl( + &self, + room_events: &mut RoomEvents, + sync_timeline_events: Vec, + prev_batch: Option, + ephemeral_events: Vec>, + ambiguity_changes: BTreeMap, + ) -> Result<()> { + if sync_timeline_events.is_empty() + && prev_batch.is_none() + && ephemeral_events.is_empty() + && ambiguity_changes.is_empty() + { + return Ok(()); + } + + // Add the previous back-pagination token (if present), followed by the timeline + // events themselves. + { + if let Some(prev_token) = &prev_batch { + room_events.push_gap(Gap { prev_token: prev_token.clone() }); + } + + room_events.push_events(sync_timeline_events.clone()); + + let mut cache = self.all_events.write().await; + for ev in &sync_timeline_events { + if let Some(event_id) = ev.event_id() { + self.append_related_event(&mut cache, ev); + cache.events.insert(event_id.to_owned(), (self.room_id.clone(), ev.clone())); + } + } + } + + // Now that all events have been added, we can trigger the + // `pagination_token_notifier`. + if prev_batch.is_some() { + self.pagination_batch_token_notifier.notify_one(); + } + + // The order of `RoomEventCacheUpdate`s is **really** important here. + { + if !sync_timeline_events.is_empty() { + let _ = self.sender.send(RoomEventCacheUpdate::AddTimelineEvents { + events: sync_timeline_events, + origin: EventsOrigin::Sync, + }); + } + + if !ephemeral_events.is_empty() { + let _ = self + .sender + .send(RoomEventCacheUpdate::AddEphemeralEvents { events: ephemeral_events }); + } + + if !ambiguity_changes.is_empty() { + let _ = self.sender.send(RoomEventCacheUpdate::UpdateMembers { ambiguity_changes }); + } + } + + Ok(()) + } +} + +/// State for a single room's event cache. +/// +/// This contains all inner mutable state that ought to be updated at the same +/// time. +pub(super) struct RoomEventCacheState { + /// The events of the room. + pub events: RoomEvents, + + /// Have we ever waited for a previous-batch-token to come from sync, in the + /// context of pagination? We do this at most once per room, the first + /// time we try to run backward pagination. We reset that upon clearing + /// the timeline events. + pub waited_for_initial_prev_token: bool, +} + +impl RoomEventCacheState { + /// Resets this data structure as if it were brand new. + pub(super) fn reset(&mut self) { + self.events.reset(); + self.waited_for_initial_prev_token = false; + } +} From 6752cf73df944ad8e62d8e1b9056680d4e1ce346 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 29 Oct 2024 10:11:28 +0100 Subject: [PATCH 392/979] test(sdk): Move tests into their correct module. --- crates/matrix-sdk/src/event_cache/mod.rs | 286 +---------------- crates/matrix-sdk/src/event_cache/room/mod.rs | 290 ++++++++++++++++++ 2 files changed, 291 insertions(+), 285 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 605c1967e71..ff23b2c4e81 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -502,15 +502,8 @@ mod tests { use assert_matches::assert_matches; use futures_util::FutureExt as _; use matrix_sdk_base::sync::{JoinedRoomUpdate, RoomUpdates, Timeline}; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::async_test; - use ruma::{ - event_id, - events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation}, - room_id, - serde::Raw, - user_id, RoomId, - }; + use ruma::{event_id, room_id, serde::Raw, user_id}; use serde_json::json; use super::{EventCacheError, RoomEventCacheUpdate}; @@ -679,281 +672,4 @@ mod tests { assert!(room_event_cache.event(event_id).await.is_none()); assert!(event_cache.event(event_id).await.is_none()); } - - #[async_test] - async fn test_event_with_redaction_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - assert_relations( - room_id, - f.text_msg("Original event").event_id(original_id).into(), - f.redaction(original_id).event_id(related_id).into(), - f, - ) - .await; - } - - #[async_test] - async fn test_event_with_edit_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - assert_relations( - room_id, - f.text_msg("Original event").event_id(original_id).into(), - f.text_msg("* An edited event") - .edit( - original_id, - RoomMessageEventContentWithoutRelation::text_plain("And edited event"), - ) - .event_id(related_id) - .into(), - f, - ) - .await; - } - - #[async_test] - async fn test_event_with_reply_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - assert_relations( - room_id, - f.text_msg("Original event").event_id(original_id).into(), - f.text_msg("A reply").reply_to(original_id).event_id(related_id).into(), - f, - ) - .await; - } - - #[async_test] - async fn test_event_with_thread_reply_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - assert_relations( - room_id, - f.text_msg("Original event").event_id(original_id).into(), - f.text_msg("A reply").in_thread(original_id, related_id).event_id(related_id).into(), - f, - ) - .await; - } - - #[async_test] - async fn test_event_with_reaction_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - assert_relations( - room_id, - f.text_msg("Original event").event_id(original_id).into(), - f.reaction(original_id, ":D".to_owned()).event_id(related_id).into(), - f, - ) - .await; - } - - #[async_test] - async fn test_event_with_poll_response_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - assert_relations( - room_id, - f.poll_start("Poll start event", "A poll question", vec!["An answer"]) - .event_id(original_id) - .into(), - f.poll_response("1", original_id).event_id(related_id).into(), - f, - ) - .await; - } - - #[async_test] - async fn test_event_with_poll_end_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - assert_relations( - room_id, - f.poll_start("Poll start event", "A poll question", vec!["An answer"]) - .event_id(original_id) - .into(), - f.poll_end("Poll ended", original_id).event_id(related_id).into(), - f, - ) - .await; - } - - #[async_test] - async fn test_event_with_filtered_relationships() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let associated_related_id = event_id!("$recursive_related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let event_factory = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - let original_event = event_factory.text_msg("Original event").event_id(original_id).into(); - let related_event = event_factory - .text_msg("* Edited event") - .edit(original_id, RoomMessageEventContentWithoutRelation::text_plain("Edited event")) - .event_id(related_id) - .into(); - let associated_related_event = - event_factory.redaction(related_id).event_id(associated_related_id).into(); - - let client = logged_in_client(None).await; - - let event_cache = client.event_cache(); - event_cache.subscribe().unwrap(); - - client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); - let room = client.get_room(room_id).unwrap(); - - let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); - - // Save the original event. - room_event_cache.save_event(original_event).await; - - // Save the related event. - room_event_cache.save_event(related_event).await; - - // Save the associated related event, which redacts the related event. - room_event_cache.save_event(associated_related_event).await; - - let filter = Some(vec![RelationType::Replacement]); - let (event, related_events) = - room_event_cache.event_with_relations(original_id, filter).await.unwrap(); - // Fetched event is the right one. - let cached_event_id = event.event_id().unwrap(); - assert_eq!(cached_event_id, original_id); - - // There are both the related id and the associatively related id - assert_eq!(related_events.len(), 2); - - let related_event_id = related_events[0].event_id().unwrap(); - assert_eq!(related_event_id, related_id); - let related_event_id = related_events[1].event_id().unwrap(); - assert_eq!(related_event_id, associated_related_id); - - // Now we'll filter threads instead, there should be no related events - let filter = Some(vec![RelationType::Thread]); - let (event, related_events) = - room_event_cache.event_with_relations(original_id, filter).await.unwrap(); - // Fetched event is the right one. - let cached_event_id = event.event_id().unwrap(); - assert_eq!(cached_event_id, original_id); - // No Thread related events found - assert!(related_events.is_empty()); - } - - #[async_test] - async fn test_event_with_recursive_relation() { - let original_id = event_id!("$original"); - let related_id = event_id!("$related"); - let associated_related_id = event_id!("$recursive_related"); - let room_id = room_id!("!galette:saucisse.bzh"); - let event_factory = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); - - let original_event = event_factory.text_msg("Original event").event_id(original_id).into(); - let related_event = event_factory - .text_msg("* Edited event") - .edit(original_id, RoomMessageEventContentWithoutRelation::text_plain("Edited event")) - .event_id(related_id) - .into(); - let associated_related_event = - event_factory.redaction(related_id).event_id(associated_related_id).into(); - - let client = logged_in_client(None).await; - - let event_cache = client.event_cache(); - event_cache.subscribe().unwrap(); - - client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); - let room = client.get_room(room_id).unwrap(); - - let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); - - // Save the original event. - room_event_cache.save_event(original_event).await; - - // Save the related event. - room_event_cache.save_event(related_event).await; - - // Save the associated related event, which redacts the related event. - room_event_cache.save_event(associated_related_event).await; - - let (event, related_events) = - room_event_cache.event_with_relations(original_id, None).await.unwrap(); - // Fetched event is the right one. - let cached_event_id = event.event_id().unwrap(); - assert_eq!(cached_event_id, original_id); - - // There are both the related id and the associatively related id - assert_eq!(related_events.len(), 2); - - let related_event_id = related_events[0].event_id().unwrap(); - assert_eq!(related_event_id, related_id); - let related_event_id = related_events[1].event_id().unwrap(); - assert_eq!(related_event_id, associated_related_id); - } - - async fn assert_relations( - room_id: &RoomId, - original_event: SyncTimelineEvent, - related_event: SyncTimelineEvent, - event_factory: EventFactory, - ) { - let client = logged_in_client(None).await; - - let event_cache = client.event_cache(); - event_cache.subscribe().unwrap(); - - client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); - let room = client.get_room(room_id).unwrap(); - - let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); - - // Save the original event. - let original_event_id = original_event.event_id().unwrap(); - room_event_cache.save_event(original_event).await; - - // Save an unrelated event to check it's not in the related events list. - let unrelated_id = event_id!("$2"); - room_event_cache - .save_event(event_factory.text_msg("An unrelated event").event_id(unrelated_id).into()) - .await; - - // Save the related event. - let related_id = related_event.event_id().unwrap(); - room_event_cache.save_event(related_event).await; - - let (event, related_events) = - room_event_cache.event_with_relations(&original_event_id, None).await.unwrap(); - // Fetched event is the right one. - let cached_event_id = event.event_id().unwrap(); - assert_eq!(cached_event_id, original_event_id); - - // There is only the actually related event in the related ones - assert_eq!(related_events.len(), 1); - let related_event_id = related_events[0].event_id().unwrap(); - assert_eq!(related_event_id, related_id); - } } diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 67be8b68784..7a3e8476869 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -555,3 +555,293 @@ impl RoomEventCacheState { self.waited_for_initial_prev_token = false; } } + +#[cfg(test)] +mod tests { + use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_test::async_test; + use ruma::{ + event_id, + events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation}, + room_id, user_id, RoomId, + }; + + use crate::test_utils::{events::EventFactory, logged_in_client}; + + #[async_test] + async fn test_event_with_redaction_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + assert_relations( + room_id, + f.text_msg("Original event").event_id(original_id).into(), + f.redaction(original_id).event_id(related_id).into(), + f, + ) + .await; + } + + #[async_test] + async fn test_event_with_edit_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + assert_relations( + room_id, + f.text_msg("Original event").event_id(original_id).into(), + f.text_msg("* An edited event") + .edit( + original_id, + RoomMessageEventContentWithoutRelation::text_plain("And edited event"), + ) + .event_id(related_id) + .into(), + f, + ) + .await; + } + + #[async_test] + async fn test_event_with_reply_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + assert_relations( + room_id, + f.text_msg("Original event").event_id(original_id).into(), + f.text_msg("A reply").reply_to(original_id).event_id(related_id).into(), + f, + ) + .await; + } + + #[async_test] + async fn test_event_with_thread_reply_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + assert_relations( + room_id, + f.text_msg("Original event").event_id(original_id).into(), + f.text_msg("A reply").in_thread(original_id, related_id).event_id(related_id).into(), + f, + ) + .await; + } + + #[async_test] + async fn test_event_with_reaction_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + assert_relations( + room_id, + f.text_msg("Original event").event_id(original_id).into(), + f.reaction(original_id, ":D".to_owned()).event_id(related_id).into(), + f, + ) + .await; + } + + #[async_test] + async fn test_event_with_poll_response_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + assert_relations( + room_id, + f.poll_start("Poll start event", "A poll question", vec!["An answer"]) + .event_id(original_id) + .into(), + f.poll_response("1", original_id).event_id(related_id).into(), + f, + ) + .await; + } + + #[async_test] + async fn test_event_with_poll_end_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + assert_relations( + room_id, + f.poll_start("Poll start event", "A poll question", vec!["An answer"]) + .event_id(original_id) + .into(), + f.poll_end("Poll ended", original_id).event_id(related_id).into(), + f, + ) + .await; + } + + #[async_test] + async fn test_event_with_filtered_relationships() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let associated_related_id = event_id!("$recursive_related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let event_factory = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + let original_event = event_factory.text_msg("Original event").event_id(original_id).into(); + let related_event = event_factory + .text_msg("* Edited event") + .edit(original_id, RoomMessageEventContentWithoutRelation::text_plain("Edited event")) + .event_id(related_id) + .into(); + let associated_related_event = + event_factory.redaction(related_id).event_id(associated_related_id).into(); + + let client = logged_in_client(None).await; + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + // Save the original event. + room_event_cache.save_event(original_event).await; + + // Save the related event. + room_event_cache.save_event(related_event).await; + + // Save the associated related event, which redacts the related event. + room_event_cache.save_event(associated_related_event).await; + + let filter = Some(vec![RelationType::Replacement]); + let (event, related_events) = + room_event_cache.event_with_relations(original_id, filter).await.unwrap(); + // Fetched event is the right one. + let cached_event_id = event.event_id().unwrap(); + assert_eq!(cached_event_id, original_id); + + // There are both the related id and the associatively related id + assert_eq!(related_events.len(), 2); + + let related_event_id = related_events[0].event_id().unwrap(); + assert_eq!(related_event_id, related_id); + let related_event_id = related_events[1].event_id().unwrap(); + assert_eq!(related_event_id, associated_related_id); + + // Now we'll filter threads instead, there should be no related events + let filter = Some(vec![RelationType::Thread]); + let (event, related_events) = + room_event_cache.event_with_relations(original_id, filter).await.unwrap(); + // Fetched event is the right one. + let cached_event_id = event.event_id().unwrap(); + assert_eq!(cached_event_id, original_id); + // No Thread related events found + assert!(related_events.is_empty()); + } + + #[async_test] + async fn test_event_with_recursive_relation() { + let original_id = event_id!("$original"); + let related_id = event_id!("$related"); + let associated_related_id = event_id!("$recursive_related"); + let room_id = room_id!("!galette:saucisse.bzh"); + let event_factory = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + let original_event = event_factory.text_msg("Original event").event_id(original_id).into(); + let related_event = event_factory + .text_msg("* Edited event") + .edit(original_id, RoomMessageEventContentWithoutRelation::text_plain("Edited event")) + .event_id(related_id) + .into(); + let associated_related_event = + event_factory.redaction(related_id).event_id(associated_related_id).into(); + + let client = logged_in_client(None).await; + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + // Save the original event. + room_event_cache.save_event(original_event).await; + + // Save the related event. + room_event_cache.save_event(related_event).await; + + // Save the associated related event, which redacts the related event. + room_event_cache.save_event(associated_related_event).await; + + let (event, related_events) = + room_event_cache.event_with_relations(original_id, None).await.unwrap(); + // Fetched event is the right one. + let cached_event_id = event.event_id().unwrap(); + assert_eq!(cached_event_id, original_id); + + // There are both the related id and the associatively related id + assert_eq!(related_events.len(), 2); + + let related_event_id = related_events[0].event_id().unwrap(); + assert_eq!(related_event_id, related_id); + let related_event_id = related_events[1].event_id().unwrap(); + assert_eq!(related_event_id, associated_related_id); + } + + async fn assert_relations( + room_id: &RoomId, + original_event: SyncTimelineEvent, + related_event: SyncTimelineEvent, + event_factory: EventFactory, + ) { + let client = logged_in_client(None).await; + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + // Save the original event. + let original_event_id = original_event.event_id().unwrap(); + room_event_cache.save_event(original_event).await; + + // Save an unrelated event to check it's not in the related events list. + let unrelated_id = event_id!("$2"); + room_event_cache + .save_event(event_factory.text_msg("An unrelated event").event_id(unrelated_id).into()) + .await; + + // Save the related event. + let related_id = related_event.event_id().unwrap(); + room_event_cache.save_event(related_event).await; + + let (event, related_events) = + room_event_cache.event_with_relations(&original_event_id, None).await.unwrap(); + // Fetched event is the right one. + let cached_event_id = event.event_id().unwrap(); + assert_eq!(cached_event_id, original_event_id); + + // There is only the actually related event in the related ones + assert_eq!(related_events.len(), 1); + let related_event_id = related_events[0].event_id().unwrap(); + assert_eq!(related_event_id, related_id); + } +} From f4a18989fbef8d73890f88e457a2d59f17952c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 25 Oct 2024 13:49:26 +0200 Subject: [PATCH 393/979] feat(room_list): allow knock state event as `latest_event` This allows clients to display pending knocking requests in the room list items. --- crates/matrix-sdk-base/src/client.rs | 3 +- crates/matrix-sdk-base/src/latest_event.rs | 24 +++++++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 60 ++++++++++++++++++- .../src/timeline/event_item/content/mod.rs | 22 ++++++- .../src/timeline/event_item/mod.rs | 43 ++++++++++++- 5 files changed, 143 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index fdb798fdc09..ecc3a81a873 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -807,7 +807,8 @@ impl BaseClient { | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) | PossibleLatestEvent::YesCallNotify(_) - | PossibleLatestEvent::YesSticker(_) => { + | PossibleLatestEvent::YesSticker(_) + | PossibleLatestEvent::YesKnockedStateEvent(_) => { // The event is the right type for us to use as latest_event return Some((Box::new(LatestEvent::new(decrypted)), i)); } diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index ecdd490dccf..ed94c447a00 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -12,7 +12,14 @@ use ruma::events::{ room::message::SyncRoomMessageEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, }; -use ruma::{events::sticker::SyncStickerEvent, MxcUri, OwnedEventId}; +use ruma::{ + events::{ + room::member::{MembershipState, SyncRoomMemberEvent}, + sticker::SyncStickerEvent, + AnySyncStateEvent, + }, + MxcUri, OwnedEventId, +}; use serde::{Deserialize, Serialize}; use crate::MinimalRoomMemberEvent; @@ -37,6 +44,9 @@ pub enum PossibleLatestEvent<'a> { /// This message is suitable - it's a call notification YesCallNotify(&'a SyncCallNotifyEvent), + /// This state event is suitable - it's a knock membership change + YesKnockedStateEvent(&'a SyncRoomMemberEvent), + // Later: YesState(), // Later: YesReaction(), /// Not suitable - it's a state event @@ -102,8 +112,16 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat // suitable AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType, - // We don't currently support state events - AnySyncTimelineEvent::State(_) => PossibleLatestEvent::NoUnsupportedEventType, + // We don't currently support most state events + AnySyncTimelineEvent::State(state) => { + // But we make an exception for knocked state events + if let AnySyncStateEvent::RoomMember(member) = state { + if matches!(member.membership(), MembershipState::Knock) { + return PossibleLatestEvent::YesKnockedStateEvent(member); + } + } + PossibleLatestEvent::NoUnsupportedEventType + } } } diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 17efc70323e..a6f711b0d28 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -705,7 +705,8 @@ async fn cache_latest_events( | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) | PossibleLatestEvent::YesCallNotify(_) - | PossibleLatestEvent::YesSticker(_) => { + | PossibleLatestEvent::YesSticker(_) + | PossibleLatestEvent::YesKnockedStateEvent(_) => { // We found a suitable latest event. Store it. // In order to make the latest event fast to read, we want to keep the @@ -1738,6 +1739,63 @@ mod tests { ); } + #[async_test] + async fn test_last_knock_member_state_event_from_sliding_sync_is_cached() { + // Given a logged-in client + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + // And a knock member state event + let knock_event = json!({ + "sender":"@alice:example.com", + "state_key":"@alice:example.com", + "type":"m.room.member", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"membership": "knock"}, + "room_id": room_id, + }); + + // When the sliding sync response contains a timeline + let events = &[knock_event]; + let room = room_with_timeline(events); + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // Then the room holds the latest knock state event + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!( + ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())), + "$ida" + ); + } + + #[async_test] + async fn test_last_member_state_event_from_sliding_sync_is_not_cached() { + // Given a logged-in client + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + // And a join member state event + let join_event = json!({ + "sender":"@alice:example.com", + "state_key":"@alice:example.com", + "type":"m.room.member", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"membership": "join"}, + "room_id": room_id, + }); + + // When the sliding sync response contains a timeline + let events = &[join_event]; + let room = room_with_timeline(events); + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // Then the room doesn't hold the join state event as the latest event + let client_room = client.get_room(room_id).expect("No room found"); + assert!(client_room.latest_event().is_none()); + } + #[async_test] async fn test_cached_latest_event_can_be_redacted() { // Given a logged-in client diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 7df383701b6..d85b4d2c448 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -39,7 +39,7 @@ use ruma::{ guest_access::RoomGuestAccessEventContent, history_visibility::RoomHistoryVisibilityEventContent, join_rules::RoomJoinRulesEventContent, - member::{Change, RoomMemberEventContent}, + member::{Change, RoomMemberEventContent, SyncRoomMemberEvent}, message::{ Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, SyncRoomMessageEvent, @@ -173,6 +173,9 @@ impl TimelineItemContent { ); None } + PossibleLatestEvent::YesKnockedStateEvent(member) => { + Some(Self::from_suitable_latest_knock_state_event_content(member)) + } PossibleLatestEvent::NoEncrypted => { warn!("Found an encrypted event cached as latest_event! ID={}", event.event_id()); None @@ -220,6 +223,23 @@ impl TimelineItemContent { } } + fn from_suitable_latest_knock_state_event_content( + event: &SyncRoomMemberEvent, + ) -> TimelineItemContent { + match event { + SyncRoomMemberEvent::Original(event) => { + let content = event.content.clone(); + let prev_content = event.prev_content().cloned(); + TimelineItemContent::room_member( + event.state_key.to_owned(), + FullStateEventContent::Original { content, prev_content }, + event.sender.to_owned(), + ) + } + SyncRoomMemberEvent::Redacted(_) => TimelineItemContent::RedactedMessage, + } + } + /// Given some sticker 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`. 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 9c28bad5b59..c94c858cdf7 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -733,7 +733,7 @@ mod tests { }; use super::{EventTimelineItem, Profile}; - use crate::timeline::TimelineDetails; + use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent}; #[async_test] async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() { @@ -764,6 +764,41 @@ mod tests { } } + #[async_test] + async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() { + // Given a sync knock member state 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 raw_event = member_event_as_state_event( + room_id, + user_id, + "knock", + "Alice Margatroid", + "mxc://e.org/SEs", + ); + let client = logged_in_client(None).await; + + // When we construct a timeline event from it + let event = SyncTimelineEvent::new(raw_event.cast()); + let timeline_item = + EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) + .await + .unwrap(); + + // 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(143273583).unwrap()); + if let TimelineItemContent::MembershipChange(change) = timeline_item.content { + assert_eq!(change.user_id, user_id); + assert_matches!(change.change, Some(MembershipChange::Knocked)); + } else { + panic!("Unexpected state event type"); + } + } + #[async_test] async fn test_latest_message_includes_bundled_edit() { // Given a sync event that is suitable to be used as a latest_event, and @@ -885,6 +920,7 @@ mod tests { room.required_state.push(member_event_as_state_event( room_id, user_id, + "join", "Alice Margatroid", "mxc://e.org/SEs", )); @@ -987,6 +1023,7 @@ mod tests { fn member_event_as_state_event( room_id: &RoomId, user_id: &UserId, + membership: &str, display_name: &str, avatar_url: &str, ) -> Raw { @@ -995,13 +1032,13 @@ mod tests { "content": { "avatar_url": avatar_url, "displayname": display_name, - "membership": "join", + "membership": membership, "reason": "" }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 143273583, "room_id": room_id, - "sender": "@example:example.org", + "sender": user_id, "state_key": user_id, "type": "m.room.member", "unsigned": { From c143f981bd5e6547681bebf56cc164a8dacdc17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 28 Oct 2024 13:23:58 +0100 Subject: [PATCH 394/979] refactor(room_list): only display the knock state events if the current user can act on them That is, if their power level allows them to either invite or kick users. --- bindings/matrix-sdk-ffi/src/room.rs | 2 +- crates/matrix-sdk-base/src/client.rs | 5 +- crates/matrix-sdk-base/src/error.rs | 8 ++ crates/matrix-sdk-base/src/latest_event.rs | 53 +++++++---- crates/matrix-sdk-base/src/rooms/normal.rs | 14 ++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 87 +++++++++++++++++-- .../src/timeline/event_item/content/mod.rs | 5 +- .../src/timeline/event_item/mod.rs | 33 ++++++- crates/matrix-sdk/src/room/mod.rs | 28 +++--- .../tests/integration/room/joined.rs | 2 +- 10 files changed, 193 insertions(+), 44 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 9f11366a53c..9b01c4f6814 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -634,7 +634,7 @@ impl Room { } pub async fn get_power_levels(&self) -> Result { - let power_levels = self.inner.room_power_levels().await?; + let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?; Ok(RoomPowerLevels::from(power_levels)) } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index ecc3a81a873..1033345a80b 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -789,6 +789,8 @@ impl BaseClient { room: &Room, ) -> Option<(Box, usize)> { let enc_events = room.latest_encrypted_events(); + let power_levels = room.power_levels().await.ok(); + let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); // Walk backwards through the encrypted events, looking for one we can decrypt for (i, event) in enc_events.iter().enumerate().rev() { @@ -802,14 +804,13 @@ impl BaseClient { // We found an event we can decrypt if let Ok(any_sync_event) = decrypted.raw().deserialize() { // We can deserialize it to find its type - match is_suitable_for_latest_event(&any_sync_event) { + match is_suitable_for_latest_event(&any_sync_event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) | PossibleLatestEvent::YesCallNotify(_) | PossibleLatestEvent::YesSticker(_) | PossibleLatestEvent::YesKnockedStateEvent(_) => { - // The event is the right type for us to use as latest_event return Some((Box::new(LatestEvent::new(decrypted)), i)); } _ => (), diff --git a/crates/matrix-sdk-base/src/error.rs b/crates/matrix-sdk-base/src/error.rs index bd7a8f1f1a1..8d1a2dd3e22 100644 --- a/crates/matrix-sdk-base/src/error.rs +++ b/crates/matrix-sdk-base/src/error.rs @@ -61,4 +61,12 @@ pub enum Error { /// function with invalid parameters #[error("receive_all_members function was called with invalid parameters")] InvalidReceiveMembersParameters, + + /// This request failed because the local data wasn't sufficient. + #[error("Local cache doesn't contain all necessary data to perform the action.")] + InsufficientData, + + /// There was a [`serde_json`] deserialization error. + #[error(transparent)] + DeserializationError(#[from] serde_json::error::Error), } diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index ed94c447a00..4b2f1ff9b13 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -14,11 +14,14 @@ use ruma::events::{ }; use ruma::{ events::{ - room::member::{MembershipState, SyncRoomMemberEvent}, + room::{ + member::{MembershipState, SyncRoomMemberEvent}, + power_levels::RoomPowerLevels, + }, sticker::SyncStickerEvent, AnySyncStateEvent, }, - MxcUri, OwnedEventId, + MxcUri, OwnedEventId, UserId, }; use serde::{Deserialize, Serialize}; @@ -45,6 +48,7 @@ pub enum PossibleLatestEvent<'a> { YesCallNotify(&'a SyncCallNotifyEvent), /// This state event is suitable - it's a knock membership change + /// that can be handled by the current user. YesKnockedStateEvent(&'a SyncRoomMemberEvent), // Later: YesState(), @@ -60,7 +64,10 @@ 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(feature = "e2e-encryption")] -pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> { +pub fn is_suitable_for_latest_event<'a>( + event: &'a AnySyncTimelineEvent, + power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>, +) -> PossibleLatestEvent<'a> { match event { // Suitable - we have an m.room.message that was not redacted or edited AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => { @@ -114,10 +121,23 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat // We don't currently support most state events AnySyncTimelineEvent::State(state) => { - // But we make an exception for knocked state events + // But we make an exception for knocked state events *if* the current user + // can either accept or decline them if let AnySyncStateEvent::RoomMember(member) = state { if matches!(member.membership(), MembershipState::Knock) { - return PossibleLatestEvent::YesKnockedStateEvent(member); + let can_accept_or_decline_knocks = match power_levels_info { + Some((own_user_id, room_power_levels)) => { + room_power_levels.user_can_invite(own_user_id) + || room_power_levels.user_can_kick(own_user_id) + } + _ => false, + }; + + // The current user can act on the knock changes, so they should be + // displayed + if can_accept_or_decline_knocks { + return PossibleLatestEvent::YesKnockedStateEvent(member); + } } } PossibleLatestEvent::NoUnsupportedEventType @@ -345,7 +365,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.msgtype.msgtype(), "m.image"); @@ -368,7 +388,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.poll_start().question.text, "do you like rust?"); @@ -392,7 +412,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -414,7 +434,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -435,7 +455,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_)) ); } @@ -457,7 +477,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } @@ -485,7 +505,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_)) ); } @@ -507,7 +527,10 @@ mod tests { }), )); - assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted); + assert_matches!( + is_suitable_for_latest_event(&event, None), + PossibleLatestEvent::NoEncrypted + ); } #[test] @@ -524,7 +547,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedEventType ); } @@ -548,7 +571,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 3d426261f2b..cfe1625b7af 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -43,6 +43,7 @@ use ruma::{ join_rules::JoinRule, member::{MembershipState, RoomMemberEventContent}, pinned_events::RoomPinnedEventsEventContent, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, redaction::SyncRoomRedactionEvent, tombstone::RoomTombstoneEventContent, }, @@ -71,7 +72,7 @@ use crate::{ read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, sync::UnreadNotificationsCount, - MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, + Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, }; /// Indicates that a notable update of `RoomInfo` has been applied, and why. @@ -508,6 +509,17 @@ impl Room { self.inner.read().base_info.max_power_level } + /// Get the current power levels of this room. + pub async fn power_levels(&self) -> Result { + Ok(self + .store + .get_state_event_static::(self.room_id()) + .await? + .ok_or(Error::InsufficientData)? + .deserialize()? + .power_levels()) + } + /// Get the `m.room.name` of this room. /// /// The returned string may be empty if the event has been redacted, or it's diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index a6f711b0d28..f613d177b6e 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -30,7 +30,7 @@ use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom}, events::{ room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent, - AnySyncStateEvent, + AnySyncStateEvent, StateEventType, }, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, UserId, @@ -698,9 +698,28 @@ async fn cache_latest_events( let mut encrypted_events = Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); + // Try to get room power levels from the current changes + let power_levels_from_changes = || { + let state_changes = changes?.state.get(room_info.room_id())?; + let room_power_levels_state = + state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?; + match room_power_levels_state.deserialize().ok()? { + AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()), + _ => None, + } + }; + + // If we didn't get any info, try getting it from local data + let power_levels = match power_levels_from_changes() { + Some(power_levels) => Some(power_levels), + None => room.power_levels().await.ok(), + }; + + let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); + for event in events.iter().rev() { if let Ok(timeline_event) = event.raw().deserialize() { - match is_suitable_for_latest_event(&timeline_event) { + match is_suitable_for_latest_event(&timeline_event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) @@ -1740,10 +1759,23 @@ mod tests { } #[async_test] - async fn test_last_knock_member_state_event_from_sliding_sync_is_cached() { + async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() { + let own_user_id = user_id!("@me:e.uk"); // Given a logged-in client - let client = logged_in_base_client(None).await; + let client = logged_in_base_client(Some(own_user_id)).await; let room_id = room_id!("!r:e.uk"); + + // Give the current user invite or kick permissions in this room + let power_levels = json!({ + "sender":"@alice:example.com", + "state_key":"", + "type":"m.room.power_levels", + "event_id": "$idb", + "origin_server_ts": 12344445, + "content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } }, + "room_id": room_id, + }); + // And a knock member state event let knock_event = json!({ "sender":"@alice:example.com", @@ -1757,7 +1789,8 @@ mod tests { // When the sliding sync response contains a timeline let events = &[knock_event]; - let room = room_with_timeline(events); + let mut room = room_with_timeline(events); + room.required_state.push(Raw::new(&power_levels).unwrap().cast()); let response = response_with_room(room_id, room); client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); @@ -1770,7 +1803,49 @@ mod tests { } #[async_test] - async fn test_last_member_state_event_from_sliding_sync_is_not_cached() { + async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() { + let own_user_id = user_id!("@me:e.uk"); + // Given a logged-in client + let client = logged_in_base_client(Some(own_user_id)).await; + let room_id = room_id!("!r:e.uk"); + + // Set the user as a user with no permission to invite or kick other users in + // this room + let power_levels = json!({ + "sender":"@alice:example.com", + "state_key":"", + "type":"m.room.power_levels", + "event_id": "$idb", + "origin_server_ts": 12344445, + "content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } }, + "room_id": room_id, + }); + + // And a knock member state event + let knock_event = json!({ + "sender":"@alice:example.com", + "state_key":"@alice:example.com", + "type":"m.room.member", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"membership": "knock"}, + "room_id": room_id, + }); + + // When the sliding sync response contains a timeline + let events = &[knock_event]; + let mut room = room_with_timeline(events); + room.required_state.push(Raw::new(&power_levels).unwrap().cast()); + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // Then the room doesn't hold the knock state event as the latest event + let client_room = client.get_room(room_id).expect("No room found"); + assert!(client_room.latest_event().is_none()); + } + + #[async_test] + async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() { // Given a logged-in client let client = logged_in_base_client(None).await; let room_id = room_id!("!r:e.uk"); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index d85b4d2c448..992ba6e6808 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -46,7 +46,7 @@ use ruma::{ }, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, - power_levels::RoomPowerLevelsEventContent, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, server_acl::RoomServerAclEventContent, third_party_invite::RoomThirdPartyInviteEventContent, tombstone::RoomTombstoneEventContent, @@ -141,8 +141,9 @@ impl TimelineItemContent { /// `TimelineItemContent`. pub(crate) fn from_latest_event_content( event: AnySyncTimelineEvent, + power_levels_info: Option<(&UserId, &RoomPowerLevels)>, ) -> Option { - match is_suitable_for_latest_event(&event) { + match is_suitable_for_latest_event(&event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(m) => { Some(Self::from_suitable_latest_event_content(m)) } 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 c94c858cdf7..b91dbb72abb 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -158,9 +158,18 @@ impl EventTimelineItem { let event_id = event.event_id().to_owned(); let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false); + // Get the room's power levels for calculating the latest event + let power_levels = if let Some(room) = client.get_room(room_id) { + room.power_levels().await.ok() + } else { + None + }; + let room_power_levels_info = client.user_id().zip(power_levels.as_ref()); + // 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 content = TimelineItemContent::from_latest_event_content(event)?; + let content = + TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?; // We don't currently bundle any reactions with the main event. This could // conceivably be wanted in the message preview in future. @@ -780,6 +789,27 @@ mod tests { ); let client = logged_in_client(None).await; + // Add power levels state event, otherwise the knock state event can't be used + // as the latest event + let power_level_event = sync_state_event!({ + "type": "m.room.power_levels", + "content": {}, + "event_id": "$143278582443PhrSn:example.org", + "origin_server_ts": 143273581, + "room_id": room_id, + "sender": user_id, + "state_key": "", + "unsigned": { + "age": 1234 + } + }); + let mut room = http::response::Room::new(); + room.required_state.push(power_level_event); + + // And the room is stored in the client so it can be extracted when needed + let response = response_with_room(room_id, room); + client.process_sliding_sync_test_helper(&response).await.unwrap(); + // When we construct a timeline event from it let event = SyncTimelineEvent::new(raw_event.cast()); let timeline_item = @@ -1040,7 +1070,6 @@ mod tests { "room_id": room_id, "sender": user_id, "state_key": user_id, - "type": "m.room.member", "unsigned": { "age": 1234 } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 5561deee315..d6a4624ecff 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2139,7 +2139,7 @@ impl Room { &self, updates: Vec<(&UserId, Int)>, ) -> Result { - let mut power_levels = self.room_power_levels().await?; + let mut power_levels = self.power_levels().await?; for (user_id, new_level) in updates { if new_level == power_levels.users_default { @@ -2157,7 +2157,7 @@ impl Room { /// Any values that are `None` in the given `RoomPowerLevelChanges` will /// remain unchanged. pub async fn apply_power_level_changes(&self, changes: RoomPowerLevelChanges) -> Result<()> { - let mut power_levels = self.room_power_levels().await?; + let mut power_levels = self.power_levels().await?; power_levels.apply(changes)?; self.send_state_event(RoomPowerLevelsEventContent::from(power_levels)).await?; Ok(()) @@ -2180,7 +2180,7 @@ impl Room { let default_power_levels = RoomPowerLevels::from(RoomPowerLevelsEventContent::new()); let changes = RoomPowerLevelChanges::from(default_power_levels); self.apply_power_level_changes(changes).await?; - self.room_power_levels().await + Ok(self.power_levels().await?) } /// Gets the suggested role for the user with the provided `user_id`. @@ -2197,14 +2197,14 @@ impl Room { /// This method checks the `RoomPowerLevels` events instead of loading the /// member list and looking for the member. pub async fn get_user_power_level(&self, user_id: &UserId) -> Result { - let event = self.room_power_levels().await?; + let event = self.power_levels().await?; Ok(event.for_user(user_id).into()) } /// Gets a map with the `UserId` of users with power levels other than `0` /// and this power level. pub async fn users_with_power_levels(&self) -> HashMap { - let power_levels = self.room_power_levels().await.ok(); + let power_levels = self.power_levels().await.ok(); let mut user_power_levels = HashMap::::new(); if let Some(power_levels) = power_levels { for (id, level) in power_levels.users.into_iter() { @@ -2487,7 +2487,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_redact_own(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_redact_own_event(user_id)) + Ok(self.power_levels().await?.user_can_redact_own_event(user_id)) } /// Returns true if the user with the given user_id is able to redact @@ -2495,7 +2495,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_redact_other(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_redact_event_of_other(user_id)) + Ok(self.power_levels().await?.user_can_redact_event_of_other(user_id)) } /// Returns true if the user with the given user_id is able to ban in the @@ -2503,7 +2503,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_ban(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_ban(user_id)) + Ok(self.power_levels().await?.user_can_ban(user_id)) } /// Returns true if the user with the given user_id is able to kick in the @@ -2511,7 +2511,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_invite(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_invite(user_id)) + Ok(self.power_levels().await?.user_can_invite(user_id)) } /// Returns true if the user with the given user_id is able to kick in the @@ -2519,7 +2519,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_kick(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_kick(user_id)) + Ok(self.power_levels().await?.user_can_kick(user_id)) } /// Returns true if the user with the given user_id is able to send a @@ -2531,7 +2531,7 @@ impl Room { user_id: &UserId, state_event: StateEventType, ) -> Result { - Ok(self.room_power_levels().await?.user_can_send_state(user_id, state_event)) + Ok(self.power_levels().await?.user_can_send_state(user_id, state_event)) } /// Returns true if the user with the given user_id is able to send a @@ -2543,7 +2543,7 @@ impl Room { user_id: &UserId, message: MessageLikeEventType, ) -> Result { - Ok(self.room_power_levels().await?.user_can_send_message(user_id, message)) + Ok(self.power_levels().await?.user_can_send_message(user_id, message)) } /// Returns true if the user with the given user_id is able to pin or unpin @@ -2552,7 +2552,7 @@ impl Room { /// The call may fail if there is an error in getting the power levels. pub async fn can_user_pin_unpin(&self, user_id: &UserId) -> Result { Ok(self - .room_power_levels() + .power_levels() .await? .user_can_send_state(user_id, StateEventType::RoomPinnedEvents)) } @@ -2562,7 +2562,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_trigger_room_notification(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_trigger_room_notification(user_id)) + Ok(self.power_levels().await?.user_can_trigger_room_notification(user_id)) } /// Get a list of servers that should know this room. diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 497df17e03c..e8f294bb0d0 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -624,7 +624,7 @@ async fn test_reset_power_levels() { .mount(&server) .await; - let initial_power_levels = room.room_power_levels().await.unwrap(); + let initial_power_levels = room.power_levels().await.unwrap(); assert_eq!(initial_power_levels.events[&TimelineEventType::RoomAvatar], int!(100)); room.reset_power_levels().await.unwrap(); From 03535832ecf375d3decbcf44529a15b88312079c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 29 Oct 2024 12:38:47 +0100 Subject: [PATCH 395/979] refactor(room): remove `sdk::Room::room_power_levels` function This has been replaced by `sdk_base::Room::power_levels`, which can also be used from `sdk::Room` --- crates/matrix-sdk/src/room/mod.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index d6a4624ecff..420aaf8eb5d 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2163,16 +2163,6 @@ impl Room { Ok(()) } - /// Get the current power levels of this room. - pub async fn room_power_levels(&self) -> Result { - Ok(self - .get_state_event_static::() - .await? - .ok_or(Error::InsufficientData)? - .deserialize()? - .power_levels()) - } - /// Resets the room's power levels to the default values /// /// [spec]: https://spec.matrix.org/v1.9/client-server-api/#mroompower_levels From ce9dc73376b4ee5a59d761c34c23018a7070cf21 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 29 Oct 2024 11:11:20 +0000 Subject: [PATCH 396/979] doc(crypto) Crypto changelog documenting VerificationRequestState::Transitioned interface change --- crates/matrix-sdk-crypto/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index dd3ac96a215..51dc8caf79f 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -59,6 +59,10 @@ Changes: Breaking changes: +- `VerificationRequestState::Transitioned` now includes a new field + `other_device_data` of type `DeviceData`. + ([#4153](https://github.com/matrix-org/matrix-rust-sdk/pull/4153)) + - `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type, instead of the more generic `TimelineEvent` type. From de3a667eb9c08cbf6ab28021b524831f9fdd771c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 29 Oct 2024 17:02:51 +0100 Subject: [PATCH 397/979] chore: Add an empty line between struct fields. --- crates/matrix-sdk-base/src/client.rs | 4 ++++ crates/matrix-sdk/src/client/mod.rs | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 1033345a80b..d7ff49ea7f4 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -93,19 +93,23 @@ use crate::{ pub struct BaseClient { /// Database pub(crate) store: Store, + /// The store used by the event cache. event_cache_store: Arc, + /// The store used for encryption. /// /// This field is only meant to be used for `OlmMachine` initialization. /// All operations on it happen inside the `OlmMachine`. #[cfg(feature = "e2e-encryption")] crypto_store: Arc, + /// The olm-machine that is created once the /// [`SessionMeta`][crate::session::SessionMeta] is set via /// [`BaseClient::set_session_meta`] #[cfg(feature = "e2e-encryption")] olm_machine: Arc>>, + /// Observable of when a user is ignored/unignored. pub(crate) ignore_user_list_changes: SharedObservable>, diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 7066fcba545..20268e75d39 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -160,6 +160,7 @@ pub(crate) struct ClientLocks { /// Look at the [`Account::mark_as_dm()`] method for a more detailed /// explanation. pub(crate) mark_as_dm_lock: Mutex<()>, + /// Lock ensuring that only a single secret store is getting opened at the /// same time. /// @@ -167,6 +168,7 @@ pub(crate) struct ClientLocks { /// default secret storage keys. #[cfg(feature = "e2e-encryption")] pub(crate) open_secret_store_lock: Mutex<()>, + /// Lock ensuring that we're only storing a single secret at a time. /// /// Take a look at the [`SecretStore::put_secret`] method for a more @@ -175,23 +177,29 @@ pub(crate) struct ClientLocks { /// [`SecretStore::put_secret`]: crate::encryption::secret_storage::SecretStore::put_secret #[cfg(feature = "e2e-encryption")] pub(crate) store_secret_lock: Mutex<()>, + /// Lock ensuring that only one method at a time might modify our backup. #[cfg(feature = "e2e-encryption")] pub(crate) backup_modify_lock: Mutex<()>, + /// Lock ensuring that we're going to attempt to upload backups for a single /// requester. #[cfg(feature = "e2e-encryption")] pub(crate) backup_upload_lock: Mutex<()>, + /// Handler making sure we only have one group session sharing request in /// flight per room. #[cfg(feature = "e2e-encryption")] pub(crate) group_session_deduplicated_handler: DeduplicatingHandler, + /// Lock making sure we're only doing one key claim request at a time. #[cfg(feature = "e2e-encryption")] pub(crate) key_claim_lock: Mutex<()>, + /// Handler to ensure that only one members request is running at a time, /// given a room. pub(crate) members_request_deduplicated_handler: DeduplicatingHandler, + /// Handler to ensure that only one encryption state request is running at a /// time, given a room. pub(crate) encryption_state_deduplicated_handler: DeduplicatingHandler, @@ -203,6 +211,7 @@ pub(crate) struct ClientLocks { #[cfg(feature = "e2e-encryption")] pub(crate) cross_process_crypto_store_lock: OnceCell>, + /// Latest "generation" of data known by the crypto store. /// /// This is a counter that only increments, set in the database (and can From ee80291c412bf26a1c9c29b654948930f31dd56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 29 Oct 2024 17:17:09 +0100 Subject: [PATCH 398/979] chore: Never skip breaking changes with git-cliff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- cliff.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cliff.toml b/cliff.toml index b81994cffeb..a3589a536c4 100644 --- a/cliff.toml +++ b/cliff.toml @@ -75,6 +75,8 @@ commit_parsers = [ { message = "^test", skip = true }, { message = "^ci", skip = true }, ] +# forbid parsers from skipping breaking changes +protect_breaking_commits = true # filter out the commits that are not matched by commit parsers filter_commits = true # glob pattern for matching git tags From 888f992df06ecdc6c8e3cc3c68c142b2558cce4a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 25 Oct 2024 18:24:01 +0200 Subject: [PATCH 399/979] refactor(base): Renamed `StateStore::list_dependend_send_queue_events` to `load_dependent_send_queue_events` --- .../matrix-sdk-base/src/store/integration_tests.rs | 14 +++++++------- crates/matrix-sdk-base/src/store/memory_store.rs | 2 +- crates/matrix-sdk-base/src/store/traits.rs | 6 +++--- crates/matrix-sdk-indexeddb/src/state_store/mod.rs | 2 +- crates/matrix-sdk-sqlite/src/state_store.rs | 2 +- crates/matrix-sdk/src/send_queue.rs | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index eccebdc9190..f59c81e28e9 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1369,7 +1369,7 @@ impl StateStoreIntegrationTests for DynStateStore { self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap(); // No dependents, to start with. - assert!(self.list_dependent_send_queue_events(room_id).await.unwrap().is_empty()); + assert!(self.load_dependent_send_queue_events(room_id).await.unwrap().is_empty()); // Save a redaction for that event. let child_txn = ChildTransactionId::new(); @@ -1383,7 +1383,7 @@ impl StateStoreIntegrationTests for DynStateStore { .unwrap(); // It worked. - let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap(); + let dependents = self.load_dependent_send_queue_events(room_id).await.unwrap(); assert_eq!(dependents.len(), 1); assert_eq!(dependents[0].parent_transaction_id, txn0); assert_eq!(dependents[0].own_transaction_id, child_txn); @@ -1397,7 +1397,7 @@ impl StateStoreIntegrationTests for DynStateStore { assert_eq!(num_updated, 1); // It worked. - let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap(); + let dependents = self.load_dependent_send_queue_events(room_id).await.unwrap(); assert_eq!(dependents.len(), 1); assert_eq!(dependents[0].parent_transaction_id, txn0); assert_eq!(dependents[0].own_transaction_id, child_txn); @@ -1412,7 +1412,7 @@ impl StateStoreIntegrationTests for DynStateStore { assert!(removed); // It worked. - assert!(self.list_dependent_send_queue_events(room_id).await.unwrap().is_empty()); + assert!(self.load_dependent_send_queue_events(room_id).await.unwrap().is_empty()); // Now, inserting a dependent event and removing the original send queue event // will NOT remove the dependent event. @@ -1430,7 +1430,7 @@ impl StateStoreIntegrationTests for DynStateStore { ) .await .unwrap(); - assert_eq!(self.list_dependent_send_queue_events(room_id).await.unwrap().len(), 1); + assert_eq!(self.load_dependent_send_queue_events(room_id).await.unwrap().len(), 1); self.save_dependent_send_queue_event( room_id, @@ -1445,14 +1445,14 @@ impl StateStoreIntegrationTests for DynStateStore { ) .await .unwrap(); - assert_eq!(self.list_dependent_send_queue_events(room_id).await.unwrap().len(), 2); + assert_eq!(self.load_dependent_send_queue_events(room_id).await.unwrap().len(), 2); // Remove event0 / txn0. let removed = self.remove_send_queue_event(room_id, &txn0).await.unwrap(); assert!(removed); // This has removed none of the dependent events. - let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap(); + let dependents = self.load_dependent_send_queue_events(room_id).await.unwrap(); assert_eq!(dependents.len(), 2); } } diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index c5d8c01384a..4f27affe00c 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -949,7 +949,7 @@ impl StateStore for MemoryStore { /// /// This returns absolutely all the dependent send queue events, whether /// they have an event id or not. - async fn list_dependent_send_queue_events( + async fn load_dependent_send_queue_events( &self, room: &RoomId, ) -> Result, Self::Error> { diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 142b7584529..dafb109593d 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -439,7 +439,7 @@ pub trait StateStore: AsyncTraitDeps { /// /// This returns absolutely all the dependent send queue events, whether /// they have an event id or not. They must be returned in insertion order. - async fn list_dependent_send_queue_events( + async fn load_dependent_send_queue_events( &self, room: &RoomId, ) -> Result, Self::Error>; @@ -710,11 +710,11 @@ impl StateStore for EraseStateStoreError { self.0.remove_dependent_send_queue_event(room_id, own_txn_id).await.map_err(Into::into) } - async fn list_dependent_send_queue_events( + async fn load_dependent_send_queue_events( &self, room_id: &RoomId, ) -> Result, Self::Error> { - self.0.list_dependent_send_queue_events(room_id).await.map_err(Into::into) + self.0.load_dependent_send_queue_events(room_id).await.map_err(Into::into) } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 193fd4c7bbf..024a41bc094 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -1618,7 +1618,7 @@ impl_state_store!({ Ok(false) } - async fn list_dependent_send_queue_events( + async fn load_dependent_send_queue_events( &self, room_id: &RoomId, ) -> Result> { diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 53cd1bd7a00..995d6410742 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -1896,7 +1896,7 @@ impl StateStore for SqliteStateStore { Ok(num_deleted > 0) } - async fn list_dependent_send_queue_events( + async fn load_dependent_send_queue_events( &self, room_id: &RoomId, ) -> Result> { diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 9e0b14aeceb..3a3858fbc0c 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -893,7 +893,7 @@ impl QueueStorage { }); let local_reactions = store - .list_dependent_send_queue_events(&self.room_id) + .load_dependent_send_queue_events(&self.room_id) .await? .into_iter() .filter_map(|dep| match dep.kind { @@ -1078,7 +1078,7 @@ impl QueueStorage { let store = client.store(); let dependent_events = store - .list_dependent_send_queue_events(&self.room_id) + .load_dependent_send_queue_events(&self.room_id) .await .map_err(RoomSendQueueStorageError::StorageError)?; From 4cbd18cb372e413f7be0fe8b52b8c511c5151679 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 25 Oct 2024 18:32:35 +0200 Subject: [PATCH 400/979] refactor(base): move all send-queue related types to a new store::send_queue module No changes in functionality, only code motion. --- .../src/store/integration_tests.rs | 5 +- .../matrix-sdk-base/src/store/memory_store.rs | 6 +- crates/matrix-sdk-base/src/store/mod.rs | 11 +- .../matrix-sdk-base/src/store/send_queue.rs | 228 ++++++++++++++++++ crates/matrix-sdk-base/src/store/traits.rs | 223 +---------------- 5 files changed, 248 insertions(+), 225 deletions(-) create mode 100644 crates/matrix-sdk-base/src/store/send_queue.rs diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index f59c81e28e9..454592ffa27 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -36,10 +36,7 @@ use serde_json::{json, value::Value as JsonValue}; use super::{DependentQueuedEventKind, DynStateStore, ServerCapabilities}; use crate::{ deserialized_responses::MemberEvent, - store::{ - traits::ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, - StateStoreExt, - }, + store::{ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, StateStoreExt}, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue, }; diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 4f27affe00c..0dcea4efdab 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -36,10 +36,8 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use super::{ - traits::{ - ChildTransactionId, ComposerDraft, QueuedEvent, SerializableEventContent, - ServerCapabilities, - }, + send_queue::{ChildTransactionId, QueuedEvent, SerializableEventContent}, + traits::{ComposerDraft, ServerCapabilities}, DependentQueuedEvent, DependentQueuedEventKind, Result, RoomInfo, StateChanges, StateStore, StoreError, }; diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 6c79893923c..ae4f9b8baba 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -68,16 +68,19 @@ use crate::{ pub(crate) mod ambiguity_map; mod memory_store; pub mod migration_helpers; +mod send_queue; #[cfg(any(test, feature = "testing"))] pub use self::integration_tests::StateStoreIntegrationTests; pub use self::{ memory_store::MemoryStore, + send_queue::{ + ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, + QueuedEvent, SerializableEventContent, + }, traits::{ - ChildTransactionId, ComposerDraft, ComposerDraftType, DependentQueuedEvent, - DependentQueuedEventKind, DynStateStore, IntoStateStore, QueueWedgeError, QueuedEvent, - SerializableEventContent, ServerCapabilities, StateStore, StateStoreDataKey, - StateStoreDataValue, StateStoreExt, + ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities, + StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt, }, }; diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs new file mode 100644 index 00000000000..58a39e3c571 --- /dev/null +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -0,0 +1,228 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! All data types related to the send queue. + +use std::{collections::BTreeMap, fmt, ops::Deref}; + +use ruma::{ + events::{AnyMessageLikeEventContent, EventContent as _, RawExt as _}, + serde::Raw, + OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, +}; +use serde::{Deserialize, Serialize}; + +/// A thin wrapper to serialize a `AnyMessageLikeEventContent`. +#[derive(Clone, Serialize, Deserialize)] +pub struct SerializableEventContent { + event: Raw, + event_type: String, +} + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for SerializableEventContent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Don't include the event in the debug display. + f.debug_struct("SerializedEventContent") + .field("event_type", &self.event_type) + .finish_non_exhaustive() + } +} + +impl SerializableEventContent { + /// Create a [`SerializableEventContent`] from a raw + /// [`AnyMessageLikeEventContent`] along with its type. + pub fn from_raw(event: Raw, event_type: String) -> Self { + Self { event_type, event } + } + + /// Create a [`SerializableEventContent`] from an + /// [`AnyMessageLikeEventContent`]. + pub fn new(event: &AnyMessageLikeEventContent) -> Result { + Ok(Self::from_raw(Raw::new(event)?, event.event_type().to_string())) + } + + /// Convert a [`SerializableEventContent`] back into a + /// [`AnyMessageLikeEventContent`]. + pub fn deserialize(&self) -> Result { + self.event.deserialize_with_type(self.event_type.clone().into()) + } + + /// Returns the raw event content along with its type. + /// + /// Useful for callers manipulating custom events. + pub fn raw(self) -> (Raw, String) { + (self.event, self.event_type) + } +} + +/// An event to be sent with a send queue. +#[derive(Clone)] +pub struct QueuedEvent { + /// The content of the message-like event we'd like to send. + pub event: SerializableEventContent, + + /// Unique transaction id for the queued event, acting as a key. + pub transaction_id: OwnedTransactionId, + + /// Set when the event couldn't be sent because of an unrecoverable API + /// error. `None` if the event is in queue for being sent. + pub error: Option, +} + +impl QueuedEvent { + /// True if the event couldn't be sent because of an unrecoverable API + /// error. See [`Self::error`] for more details on the reason. + pub fn is_wedged(&self) -> bool { + self.error.is_some() + } +} + +/// Represents a failed to send unrecoverable error of an event sent via the +/// send queue. +/// +/// It is a serializable representation of a client error, see +/// `From` implementation for more details. These errors can not be +/// automatically retried, but yet some manual action can be taken before retry +/// sending. If not the only solution is to delete the local event. +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum QueueWedgeError { + /// This error occurs when there are some insecure devices in the room, and + /// the current encryption setting prohibits sharing with them. + #[error("There are insecure devices in the room")] + InsecureDevices { + /// The insecure devices as a Map of userID to deviceID. + user_device_map: BTreeMap>, + }, + + /// This error occurs when a previously verified user is not anymore, and + /// the current encryption setting prohibits sharing when it happens. + #[error("Some users that were previously verified are not anymore")] + IdentityViolations { + /// The users that are expected to be verified but are not. + users: Vec, + }, + + /// It is required to set up cross-signing and properly verify the current + /// session before sending. + #[error("Own verification is required")] + CrossVerificationRequired, + + /// Other errors. + #[error("Other unrecoverable error: {msg}")] + GenericApiError { + /// Description of the error. + msg: String, + }, +} + +/// The specific user intent that characterizes a [`DependentQueuedEvent`]. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DependentQueuedEventKind { + /// The event should be edited. + Edit { + /// The new event for the content. + new_content: SerializableEventContent, + }, + + /// The event should be redacted/aborted/removed. + Redact, + + /// The event should be reacted to, with the given key. + React { + /// Key used for the reaction. + key: String, + }, +} + +/// A transaction id identifying a [`DependentQueuedEvent`] rather than its +/// parent [`QueuedEvent`]. +/// +/// This thin wrapper adds some safety to some APIs, making it possible to +/// distinguish between the parent's `TransactionId` and the dependent event's +/// own `TransactionId`. +#[repr(transparent)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct ChildTransactionId(OwnedTransactionId); + +impl ChildTransactionId { + /// Returns a new [`ChildTransactionId`]. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(TransactionId::new()) + } +} + +impl Deref for ChildTransactionId { + type Target = TransactionId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for ChildTransactionId { + fn from(val: String) -> Self { + Self(val.into()) + } +} + +impl From for OwnedTransactionId { + fn from(val: ChildTransactionId) -> Self { + val.0 + } +} + +/// An event to be sent, depending on a [`QueuedEvent`] to be sent first. +/// +/// Depending on whether the event has been sent or not, this will either update +/// the local echo in the storage, or send an event equivalent to the user +/// intent to the homeserver. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DependentQueuedEvent { + /// Unique identifier for this dependent queued event. + /// + /// Useful for deletion. + pub own_transaction_id: ChildTransactionId, + + /// The kind of user intent. + pub kind: DependentQueuedEventKind, + + /// Transaction id for the parent's local echo / used in the server request. + /// + /// Note: this is the transaction id used for the depended-on event, i.e. + /// the one that was originally sent and that's being modified with this + /// dependent event. + pub parent_transaction_id: OwnedTransactionId, + + /// If the parent event has been sent, the parent's event identifier + /// returned by the server once the local echo has been sent out. + /// + /// Note: this is the event id used for the depended-on event after it's + /// been sent, not for a possible event that could have been sent + /// because of this [`DependentQueuedEvent`]. + pub event_id: Option, +} + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for QueuedEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Hide the content from the debug log. + f.debug_struct("QueuedEvent") + .field("transaction_id", &self.transaction_id) + .field("is_wedged", &self.is_wedged()) + .finish_non_exhaustive() + } +} diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index dafb109593d..50f2a52fae4 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -16,7 +16,6 @@ use std::{ borrow::Borrow, collections::{BTreeMap, BTreeSet}, fmt, - ops::Deref, sync::Arc, }; @@ -29,20 +28,22 @@ use ruma::{ events::{ presence::PresenceEvent, receipt::{Receipt, ReceiptThread, ReceiptType}, - AnyGlobalAccountDataEvent, AnyMessageLikeEventContent, AnyRoomAccountDataEvent, - EmptyStateKey, EventContent as _, GlobalAccountDataEvent, GlobalAccountDataEventContent, - GlobalAccountDataEventType, RawExt as _, RedactContent, RedactedStateEventContent, - RoomAccountDataEvent, RoomAccountDataEventContent, RoomAccountDataEventType, - StateEventType, StaticEventContent, StaticStateEventContent, + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, EmptyStateKey, GlobalAccountDataEvent, + GlobalAccountDataEventContent, GlobalAccountDataEventType, RedactContent, + RedactedStateEventContent, RoomAccountDataEvent, RoomAccountDataEventContent, + RoomAccountDataEventType, StateEventType, StaticEventContent, StaticStateEventContent, }, serde::Raw, time::SystemTime, - EventId, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, - OwnedUserId, RoomId, TransactionId, UserId, + EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, + TransactionId, UserId, }; use serde::{Deserialize, Serialize}; -use super::{StateChanges, StoreError}; +use super::{ + ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, + QueuedEvent, SerializableEventContent, StateChanges, StoreError, +}; use crate::{ deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState}, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, @@ -1103,210 +1104,6 @@ impl StateStoreDataKey<'_> { pub const COMPOSER_DRAFT: &'static str = "composer_draft"; } -/// A thin wrapper to serialize a `AnyMessageLikeEventContent`. -#[derive(Clone, Serialize, Deserialize)] -pub struct SerializableEventContent { - event: Raw, - event_type: String, -} - -#[cfg(not(tarpaulin_include))] -impl fmt::Debug for SerializableEventContent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Don't include the event in the debug display. - f.debug_struct("SerializedEventContent") - .field("event_type", &self.event_type) - .finish_non_exhaustive() - } -} - -impl SerializableEventContent { - /// Create a [`SerializableEventContent`] from a raw - /// [`AnyMessageLikeEventContent`] along with its type. - pub fn from_raw(event: Raw, event_type: String) -> Self { - Self { event_type, event } - } - - /// Create a [`SerializableEventContent`] from an - /// [`AnyMessageLikeEventContent`]. - pub fn new(event: &AnyMessageLikeEventContent) -> Result { - Ok(Self::from_raw(Raw::new(event)?, event.event_type().to_string())) - } - - /// Convert a [`SerializableEventContent`] back into a - /// [`AnyMessageLikeEventContent`]. - pub fn deserialize(&self) -> Result { - self.event.deserialize_with_type(self.event_type.clone().into()) - } - - /// Returns the raw event content along with its type. - /// - /// Useful for callers manipulating custom events. - pub fn raw(self) -> (Raw, String) { - (self.event, self.event_type) - } -} - -/// An event to be sent with a send queue. -#[derive(Clone)] -pub struct QueuedEvent { - /// The content of the message-like event we'd like to send. - pub event: SerializableEventContent, - - /// Unique transaction id for the queued event, acting as a key. - pub transaction_id: OwnedTransactionId, - - /// Set when the event couldn't be sent because of an unrecoverable API - /// error. `None` if the event is in queue for being sent. - pub error: Option, -} - -impl QueuedEvent { - /// True if the event couldn't be sent because of an unrecoverable API - /// error. See [`Self::error`] for more details on the reason. - pub fn is_wedged(&self) -> bool { - self.error.is_some() - } -} - -/// Represents a failed to send unrecoverable error of an event sent via the -/// send_queue. -/// -/// It is a serializable representation of a client error, see -/// `From` implementation for more details. These errors can not be -/// automatically retried, but yet some manual action can be taken before retry -/// sending. If not the only solution is to delete the local event. -#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] -pub enum QueueWedgeError { - /// This error occurs when there are some insecure devices in the room, and - /// the current encryption setting prohibits sharing with them. - #[error("There are insecure devices in the room")] - InsecureDevices { - /// The insecure devices as a Map of userID to deviceID. - user_device_map: BTreeMap>, - }, - - /// This error occurs when a previously verified user is not anymore, and - /// the current encryption setting prohibits sharing when it happens. - #[error("Some users that were previously verified are not anymore")] - IdentityViolations { - /// The users that are expected to be verified but are not. - users: Vec, - }, - - /// It is required to set up cross-signing and properly verify the current - /// session before sending. - #[error("Own verification is required")] - CrossVerificationRequired, - - /// Other errors. - #[error("Other unrecoverable error: {msg}")] - GenericApiError { - /// Description of the error. - msg: String, - }, -} - -/// The specific user intent that characterizes a [`DependentQueuedEvent`]. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum DependentQueuedEventKind { - /// The event should be edited. - Edit { - /// The new event for the content. - new_content: SerializableEventContent, - }, - - /// The event should be redacted/aborted/removed. - Redact, - - /// The event should be reacted to, with the given key. - React { - /// Key used for the reaction. - key: String, - }, -} - -/// A transaction id identifying a [`DependentQueuedEvent`] rather than its -/// parent [`QueuedEvent`]. -/// -/// This thin wrapper adds some safety to some APIs, making it possible to -/// distinguish between the parent's `TransactionId` and the dependent event's -/// own `TransactionId`. -#[repr(transparent)] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct ChildTransactionId(OwnedTransactionId); - -impl ChildTransactionId { - /// Returns a new [`ChildTransactionId`]. - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self(TransactionId::new()) - } -} - -impl Deref for ChildTransactionId { - type Target = TransactionId; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for ChildTransactionId { - fn from(val: String) -> Self { - Self(val.into()) - } -} - -impl From for OwnedTransactionId { - fn from(val: ChildTransactionId) -> Self { - val.0 - } -} - -/// An event to be sent, depending on a [`QueuedEvent`] to be sent first. -/// -/// Depending on whether the event has been sent or not, this will either update -/// the local echo in the storage, or send an event equivalent to the user -/// intent to the homeserver. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DependentQueuedEvent { - /// Unique identifier for this dependent queued event. - /// - /// Useful for deletion. - pub own_transaction_id: ChildTransactionId, - - /// The kind of user intent. - pub kind: DependentQueuedEventKind, - - /// Transaction id for the parent's local echo / used in the server request. - /// - /// Note: this is the transaction id used for the depended-on event, i.e. - /// the one that was originally sent and that's being modified with this - /// dependent event. - pub parent_transaction_id: OwnedTransactionId, - - /// If the parent event has been sent, the parent's event identifier - /// returned by the server once the local echo has been sent out. - /// - /// Note: this is the event id used for the depended-on event after it's - /// been sent, not for a possible event that could have been sent - /// because of this [`DependentQueuedEvent`]. - pub event_id: Option, -} - -#[cfg(not(tarpaulin_include))] -impl fmt::Debug for QueuedEvent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Hide the content from the debug log. - f.debug_struct("QueuedEvent") - .field("transaction_id", &self.transaction_id) - .field("is_wedged", &self.is_wedged()) - .finish_non_exhaustive() - } -} - #[cfg(test)] mod tests { use super::{now_timestamp_ms, ServerCapabilities}; From 58d46f015bc40a27c5646a6f971cc3c931e358b7 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 25 Oct 2024 19:34:44 +0200 Subject: [PATCH 401/979] refactor(base): add a `QueuedRequestKind` enum In a next commit, the `QueuedEvent` will be renamed to `QueuedRequest`. This specifies which kind of request we want to send with the send queue; for now, it can only be an event. --- .../src/store/integration_tests.rs | 8 +- .../matrix-sdk-base/src/store/memory_store.rs | 17 ++--- crates/matrix-sdk-base/src/store/mod.rs | 2 +- .../matrix-sdk-base/src/store/send_queue.rs | 34 +++++++-- .../src/state_store/mod.rs | 76 ++++++++++++------- crates/matrix-sdk-sqlite/src/state_store.rs | 5 +- crates/matrix-sdk/src/send_queue.rs | 18 +++-- 7 files changed, 99 insertions(+), 61 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 454592ffa27..1d8933dba0c 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1219,7 +1219,7 @@ impl StateStoreIntegrationTests for DynStateStore { { assert_eq!(pending[0].transaction_id, txn0); - let deserialized = pending[0].event.deserialize().unwrap(); + let deserialized = pending[0].as_event().unwrap().deserialize().unwrap(); assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), "msg0"); @@ -1246,7 +1246,7 @@ impl StateStoreIntegrationTests for DynStateStore { assert_eq!(pending[0].transaction_id, txn0); for i in 0..4 { - let deserialized = pending[i].event.deserialize().unwrap(); + let deserialized = pending[i].as_event().unwrap().deserialize().unwrap(); assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), format!("msg{i}")); assert!(!pending[i].is_wedged()); @@ -1293,7 +1293,7 @@ impl StateStoreIntegrationTests for DynStateStore { { assert_eq!(pending[2].transaction_id, *txn2); - let deserialized = pending[2].event.deserialize().unwrap(); + let deserialized = pending[2].as_event().unwrap().deserialize().unwrap(); assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), "wow that's a cool test"); @@ -1301,7 +1301,7 @@ impl StateStoreIntegrationTests for DynStateStore { for i in 0..4 { if i != 2 { - let deserialized = pending[i].event.deserialize().unwrap(); + let deserialized = pending[i].as_event().unwrap().deserialize().unwrap(); assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); assert_eq!(content.body(), format!("msg{i}")); diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 0dcea4efdab..47fa167b193 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -38,8 +38,8 @@ use tracing::{debug, instrument, trace, warn}; use super::{ send_queue::{ChildTransactionId, QueuedEvent, SerializableEventContent}, traits::{ComposerDraft, ServerCapabilities}, - DependentQueuedEvent, DependentQueuedEventKind, Result, RoomInfo, StateChanges, StateStore, - StoreError, + DependentQueuedEvent, DependentQueuedEventKind, QueuedRequestKind, Result, RoomInfo, + StateChanges, StateStore, StoreError, }; use crate::{ deserialized_responses::RawAnySyncOrStrippedState, store::QueueWedgeError, @@ -806,14 +806,11 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, - event: SerializableEventContent, + content: SerializableEventContent, ) -> Result<(), Self::Error> { - self.send_queue_events - .write() - .unwrap() - .entry(room_id.to_owned()) - .or_default() - .push(QueuedEvent { event, transaction_id, error: None }); + self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().push( + QueuedEvent { kind: QueuedRequestKind::Event { content }, transaction_id, error: None }, + ); Ok(()) } @@ -832,7 +829,7 @@ impl StateStore for MemoryStore { .iter_mut() .find(|item| item.transaction_id == transaction_id) { - entry.event = content; + entry.kind = QueuedRequestKind::Event { content }; entry.error = None; Ok(true) } else { diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index ae4f9b8baba..8cc5de5dc4c 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -76,7 +76,7 @@ pub use self::{ memory_store::MemoryStore, send_queue::{ ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, - QueuedEvent, SerializableEventContent, + QueuedEvent, QueuedRequestKind, SerializableEventContent, }, traits::{ ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities, diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 58a39e3c571..f76f6d95e3d 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -16,6 +16,7 @@ use std::{collections::BTreeMap, fmt, ops::Deref}; +use as_variant::as_variant; use ruma::{ events::{AnyMessageLikeEventContent, EventContent as _, RawExt as _}, serde::Raw, @@ -62,26 +63,43 @@ impl SerializableEventContent { /// Returns the raw event content along with its type. /// /// Useful for callers manipulating custom events. - pub fn raw(self) -> (Raw, String) { - (self.event, self.event_type) + pub fn raw(&self) -> (&Raw, &str) { + (&self.event, &self.event_type) } } -/// An event to be sent with a send queue. +/// The kind of a send queue request. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum QueuedRequestKind { + /// An event to be sent via the send queue. + Event { + /// The content of the message-like event we'd like to send. + content: SerializableEventContent, + }, +} + +/// A request to be sent with a send queue. #[derive(Clone)] pub struct QueuedEvent { - /// The content of the message-like event we'd like to send. - pub event: SerializableEventContent, + /// The kind of queued request we're going to send. + pub kind: QueuedRequestKind, - /// Unique transaction id for the queued event, acting as a key. + /// Unique transaction id for the queued request, acting as a key. pub transaction_id: OwnedTransactionId, - /// Set when the event couldn't be sent because of an unrecoverable API - /// error. `None` if the event is in queue for being sent. + /// Error returned when the request couldn't be sent and is stuck in the + /// unrecoverable state. + /// + /// `None` if the request is in the queue, waiting to be sent. pub error: Option, } impl QueuedEvent { + /// Returns `Some` if the queued request is about sending an event. + pub fn as_event(&self) -> Option<&SerializableEventContent> { + as_variant!(&self.kind, QueuedRequestKind::Event { content } => content) + } + /// True if the event couldn't be sent because of an unrecoverable API /// error. See [`Self::error`] for more details on the reason. pub fn is_wedged(&self) -> bool { diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 024a41bc094..0a90b1b8cc4 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -26,8 +26,8 @@ use matrix_sdk_base::{ deserialized_responses::RawAnySyncOrStrippedState, store::{ ChildTransactionId, ComposerDraft, DependentQueuedEvent, DependentQueuedEventKind, - QueuedEvent, SerializableEventContent, ServerCapabilities, StateChanges, StateStore, - StoreError, + QueuedEvent, QueuedRequestKind, SerializableEventContent, ServerCapabilities, StateChanges, + StateStore, StoreError, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, StateStoreDataKey, StateStoreDataValue, }; @@ -431,14 +431,36 @@ struct PersistedQueuedEvent { pub room_id: OwnedRoomId, // All these fields are the same as in [`QueuedEvent`]. - event: SerializableEventContent, + /// Kind. Optional because it might be missing from previous formats. + kind: Option, transaction_id: OwnedTransactionId, - // Deprecated (from old format), now replaced with error field. - // Kept here for migration + pub error: Option, + + // Migrated fields: keep these private, they're not used anymore elsewhere in the code base. + /// Deprecated (from old format), now replaced with error field. is_wedged: Option, - pub error: Option, + event: Option, +} + +impl PersistedQueuedEvent { + fn into_queued_event(self) -> Option { + let kind = + self.kind.or_else(|| self.event.map(|content| QueuedRequestKind::Event { content }))?; + + let error = match self.is_wedged { + Some(true) => { + // Migrate to a generic error. + Some(QueueWedgeError::GenericApiError { + msg: "local echo failed to send in a previous session".into(), + }) + } + _ => self.error, + }; + + Some(QueuedEvent { kind, transaction_id: self.transaction_id, error }) + } } // Small hack to have the following macro invocation act as the appropriate @@ -1329,10 +1351,11 @@ impl_state_store!({ // Push the new event. prev.push(PersistedQueuedEvent { room_id: room_id.to_owned(), - event: content, + kind: Some(QueuedRequestKind::Event { content }), transaction_id, - is_wedged: None, error: None, + is_wedged: None, + event: None, }); // Save the new vector into db. @@ -1369,9 +1392,12 @@ impl_state_store!({ // Modify the one event. if let Some(entry) = prev.iter_mut().find(|entry| entry.transaction_id == transaction_id) { - entry.event = content; - entry.is_wedged = None; + entry.kind = Some(QueuedRequestKind::Event { content }); + // Reset the error state. entry.error = None; + // Remove migrated fields. + entry.is_wedged = None; + entry.event = None; // Save the new vector into db. obj.put_key_val(&encoded_key, &self.serialize_value(&prev)?)?; @@ -1435,22 +1461,7 @@ impl_state_store!({ |val| self.deserialize_value::>(&val), )?; - Ok(prev - .into_iter() - .map(|item| QueuedEvent { - event: item.event, - transaction_id: item.transaction_id, - error: match item.is_wedged { - Some(true) => { - // migrate a generic error - Some(QueueWedgeError::GenericApiError { - msg: "local echo failed to send in a previous session".into(), - }) - } - _ => item.error, - }, - }) - .collect()) + Ok(prev.into_iter().filter_map(PersistedQueuedEvent::into_queued_event).collect()) } async fn update_send_queue_event_status( @@ -1663,7 +1674,8 @@ impl From<&StrippedRoomMemberEvent> for RoomMember { #[cfg(test)] mod migration_tests { - use matrix_sdk_base::store::SerializableEventContent; + use assert_matches2::assert_matches; + use matrix_sdk_base::store::{QueuedRequestKind, SerializableEventContent}; use ruma::{ events::room::message::RoomMessageEventContent, room_id, OwnedRoomId, OwnedTransactionId, TransactionId, @@ -1698,7 +1710,7 @@ mod migration_tests { let old_persisted_queue_event = OldPersistedQueuedEvent { room_id: room_a_id.to_owned(), event: content, - transaction_id, + transaction_id: transaction_id.clone(), is_wedged: true, }; @@ -1710,6 +1722,14 @@ mod migration_tests { assert_eq!(new_persisted.is_wedged, Some(true)); assert!(new_persisted.error.is_none()); + + assert!(new_persisted.event.is_some()); + assert!(new_persisted.kind.is_none()); + + let queued = new_persisted.into_queued_event().unwrap(); + assert_matches!(queued.kind, QueuedRequestKind::Event { .. }); + assert_eq!(queued.transaction_id, transaction_id); + assert!(queued.error.is_some()); } } diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 995d6410742..bc8b9d00327 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -12,7 +12,8 @@ use matrix_sdk_base::{ deserialized_responses::{RawAnySyncOrStrippedState, SyncOrStrippedState}, store::{ migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedEvent, - DependentQueuedEventKind, QueueWedgeError, QueuedEvent, SerializableEventContent, + DependentQueuedEventKind, QueueWedgeError, QueuedEvent, QueuedRequestKind, + SerializableEventContent, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, @@ -1767,7 +1768,7 @@ impl StateStore for SqliteStateStore { for entry in res { queued_events.push(QueuedEvent { transaction_id: entry.0.into(), - event: self.deserialize_json(&entry.1)?, + kind: QueuedRequestKind::Event { content: self.deserialize_json(&entry.1)? }, error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?, }); } diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 3a3858fbc0c..5642b61d42e 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -54,7 +54,7 @@ use std::{ use matrix_sdk_base::{ store::{ ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, - QueuedEvent, SerializableEventContent, + QueuedEvent, QueuedRequestKind, SerializableEventContent, }, RoomState, StoreError, }; @@ -452,7 +452,7 @@ impl RoomSendQueue { continue; }; - let (event, event_type) = queued_event.event.raw(); + let (event, event_type) = queued_event.as_event().unwrap().raw(); match room .send_raw(&event_type.to_string(), event) .with_transaction_id(&queued_event.transaction_id) @@ -881,13 +881,15 @@ impl QueueStorage { store.load_send_queue_events(&self.room_id).await?.into_iter().map(|queued| { LocalEcho { transaction_id: queued.transaction_id.clone(), - content: LocalEchoContent::Event { - serialized_event: queued.event, - send_handle: SendHandle { - room: room.clone(), - transaction_id: queued.transaction_id, + content: match queued.kind { + QueuedRequestKind::Event { content } => LocalEchoContent::Event { + serialized_event: content, + send_handle: SendHandle { + room: room.clone(), + transaction_id: queued.transaction_id, + }, + send_error: queued.error, }, - send_error: queued.error, }, } }); From 9c858c12084f70524fb33c076d35cbb8e0ac4cc3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 28 Oct 2024 15:09:50 +0100 Subject: [PATCH 402/979] refactor(base): rename all send-queue related "events" to "requests" Changelog: Renamed all the send-queue related "events" to "requests", so as to generalize usage of the send queue to not-events (e.g. medias, redactions, etc.). --- bindings/matrix-sdk-ffi/src/client.rs | 2 +- .../src/store/integration_tests.rs | 72 +-- .../matrix-sdk-base/src/store/memory_store.rs | 42 +- crates/matrix-sdk-base/src/store/mod.rs | 4 +- .../matrix-sdk-base/src/store/send_queue.rs | 50 +- crates/matrix-sdk-base/src/store/traits.rs | 120 ++--- .../src/state_store/mod.rs | 126 ++--- .../005_send_queue_dependent_events.sql | 2 +- crates/matrix-sdk-sqlite/src/state_store.rs | 46 +- crates/matrix-sdk/src/send_queue.rs | 430 ++++++++++-------- .../tests/integration/send_queue.rs | 2 +- 11 files changed, 466 insertions(+), 430 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 5e928e571f5..be6176a58df 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -503,7 +503,7 @@ impl Client { Arc::new(TaskHandle::new(RUNTIME.spawn(async move { // Respawn tasks for rooms that had unsent events. At this point we've just // created the subscriber, so it'll be notified about errors. - q.respawn_tasks_for_rooms_with_unsent_events().await; + q.respawn_tasks_for_rooms_with_unsent_requests().await; loop { match subscriber.recv().await { diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 1d8933dba0c..d83eef9793e 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -33,7 +33,7 @@ use ruma::{ }; use serde_json::{json, value::Value as JsonValue}; -use super::{DependentQueuedEventKind, DynStateStore, ServerCapabilities}; +use super::{DependentQueuedRequestKind, DynStateStore, ServerCapabilities}; use crate::{ deserialized_responses::MemberEvent, store::{ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, StateStoreExt}, @@ -1202,7 +1202,7 @@ impl StateStoreIntegrationTests for DynStateStore { let room_id = room_id!("!test_send_queue:localhost"); // No queued event in store at first. - let events = self.load_send_queue_events(room_id).await.unwrap(); + let events = self.load_send_queue_requests(room_id).await.unwrap(); assert!(events.is_empty()); // Saving one thing should work. @@ -1210,10 +1210,10 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into()) .unwrap(); - self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap(); + self.save_send_queue_request(room_id, txn0.clone(), event0).await.unwrap(); // Reading it will work. - let pending = self.load_send_queue_events(room_id).await.unwrap(); + let pending = self.load_send_queue_requests(room_id).await.unwrap(); assert_eq!(pending.len(), 1); { @@ -1234,11 +1234,11 @@ impl StateStoreIntegrationTests for DynStateStore { ) .unwrap(); - self.save_send_queue_event(room_id, txn, event).await.unwrap(); + self.save_send_queue_request(room_id, txn, event).await.unwrap(); } // Reading all the events should work. - let pending = self.load_send_queue_events(room_id).await.unwrap(); + let pending = self.load_send_queue_requests(room_id).await.unwrap(); // All the events should be retrieved, in the same order. assert_eq!(pending.len(), 4); @@ -1254,7 +1254,7 @@ impl StateStoreIntegrationTests for DynStateStore { // Marking an event as wedged works. let txn2 = &pending[2].transaction_id; - self.update_send_queue_event_status( + self.update_send_queue_request_status( room_id, txn2, Some(QueueWedgeError::GenericApiError { msg: "Oops".to_owned() }), @@ -1263,7 +1263,7 @@ impl StateStoreIntegrationTests for DynStateStore { .unwrap(); // And it is reflected. - let pending = self.load_send_queue_events(room_id).await.unwrap(); + let pending = self.load_send_queue_requests(room_id).await.unwrap(); // All the events should be retrieved, in the same order. assert_eq!(pending.len(), 4); @@ -1284,10 +1284,10 @@ impl StateStoreIntegrationTests for DynStateStore { &RoomMessageEventContent::text_plain("wow that's a cool test").into(), ) .unwrap(); - self.update_send_queue_event(room_id, txn2, event0).await.unwrap(); + self.update_send_queue_request(room_id, txn2, event0).await.unwrap(); // And it is reflected. - let pending = self.load_send_queue_events(room_id).await.unwrap(); + let pending = self.load_send_queue_requests(room_id).await.unwrap(); assert_eq!(pending.len(), 4); { @@ -1311,10 +1311,10 @@ impl StateStoreIntegrationTests for DynStateStore { } // Removing an event works. - self.remove_send_queue_event(room_id, &txn0).await.unwrap(); + self.remove_send_queue_request(room_id, &txn0).await.unwrap(); // And it is reflected. - let pending = self.load_send_queue_events(room_id).await.unwrap(); + let pending = self.load_send_queue_requests(room_id).await.unwrap(); assert_eq!(pending.len(), 3); assert_eq!(pending[1].transaction_id, *txn2); @@ -1332,7 +1332,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into()) .unwrap(); - self.save_send_queue_event(room_id2, txn.clone(), event).await.unwrap(); + self.save_send_queue_request(room_id2, txn.clone(), event).await.unwrap(); } // Add and remove one event for room3. @@ -1342,14 +1342,14 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into()) .unwrap(); - self.save_send_queue_event(room_id3, txn.clone(), event).await.unwrap(); + self.save_send_queue_request(room_id3, txn.clone(), event).await.unwrap(); - self.remove_send_queue_event(room_id3, &txn).await.unwrap(); + self.remove_send_queue_request(room_id3, &txn).await.unwrap(); } // Query all the rooms which have unsent events. Per the previous steps, // it should be room1 and room2, not room3. - let outstanding_rooms = self.load_rooms_with_unsent_events().await.unwrap(); + let outstanding_rooms = self.load_rooms_with_unsent_requests().await.unwrap(); assert_eq!(outstanding_rooms.len(), 2); assert!(outstanding_rooms.iter().any(|room| room == room_id)); assert!(outstanding_rooms.iter().any(|room| room == room_id2)); @@ -1363,53 +1363,53 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into()) .unwrap(); - self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap(); + self.save_send_queue_request(room_id, txn0.clone(), event0).await.unwrap(); // No dependents, to start with. - assert!(self.load_dependent_send_queue_events(room_id).await.unwrap().is_empty()); + assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty()); // Save a redaction for that event. let child_txn = ChildTransactionId::new(); - self.save_dependent_send_queue_event( + self.save_dependent_queued_request( room_id, &txn0, child_txn.clone(), - DependentQueuedEventKind::Redact, + DependentQueuedRequestKind::RedactEvent, ) .await .unwrap(); // It worked. - let dependents = self.load_dependent_send_queue_events(room_id).await.unwrap(); + let dependents = self.load_dependent_queued_requests(room_id).await.unwrap(); assert_eq!(dependents.len(), 1); assert_eq!(dependents[0].parent_transaction_id, txn0); assert_eq!(dependents[0].own_transaction_id, child_txn); assert!(dependents[0].event_id.is_none()); - assert_matches!(dependents[0].kind, DependentQueuedEventKind::Redact); + assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent); // Update the event id. let event_id = owned_event_id!("$1"); let num_updated = - self.update_dependent_send_queue_event(room_id, &txn0, event_id.clone()).await.unwrap(); + self.update_dependent_queued_request(room_id, &txn0, event_id.clone()).await.unwrap(); assert_eq!(num_updated, 1); // It worked. - let dependents = self.load_dependent_send_queue_events(room_id).await.unwrap(); + let dependents = self.load_dependent_queued_requests(room_id).await.unwrap(); assert_eq!(dependents.len(), 1); assert_eq!(dependents[0].parent_transaction_id, txn0); assert_eq!(dependents[0].own_transaction_id, child_txn); assert_eq!(dependents[0].event_id.as_ref(), Some(&event_id)); - assert_matches!(dependents[0].kind, DependentQueuedEventKind::Redact); + assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent); // Now remove it. let removed = self - .remove_dependent_send_queue_event(room_id, &dependents[0].own_transaction_id) + .remove_dependent_queued_request(room_id, &dependents[0].own_transaction_id) .await .unwrap(); assert!(removed); // It worked. - assert!(self.load_dependent_send_queue_events(room_id).await.unwrap().is_empty()); + assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty()); // Now, inserting a dependent event and removing the original send queue event // will NOT remove the dependent event. @@ -1417,23 +1417,23 @@ impl StateStoreIntegrationTests for DynStateStore { let event1 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into()) .unwrap(); - self.save_send_queue_event(room_id, txn1.clone(), event1).await.unwrap(); + self.save_send_queue_request(room_id, txn1.clone(), event1).await.unwrap(); - self.save_dependent_send_queue_event( + self.save_dependent_queued_request( room_id, &txn0, ChildTransactionId::new(), - DependentQueuedEventKind::Redact, + DependentQueuedRequestKind::RedactEvent, ) .await .unwrap(); - assert_eq!(self.load_dependent_send_queue_events(room_id).await.unwrap().len(), 1); + assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 1); - self.save_dependent_send_queue_event( + self.save_dependent_queued_request( room_id, &txn1, ChildTransactionId::new(), - DependentQueuedEventKind::Edit { + DependentQueuedRequestKind::EditEvent { new_content: SerializableEventContent::new( &RoomMessageEventContent::text_plain("edit").into(), ) @@ -1442,14 +1442,14 @@ impl StateStoreIntegrationTests for DynStateStore { ) .await .unwrap(); - assert_eq!(self.load_dependent_send_queue_events(room_id).await.unwrap().len(), 2); + assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 2); // Remove event0 / txn0. - let removed = self.remove_send_queue_event(room_id, &txn0).await.unwrap(); + let removed = self.remove_send_queue_request(room_id, &txn0).await.unwrap(); assert!(removed); // This has removed none of the dependent events. - let dependents = self.load_dependent_send_queue_events(room_id).await.unwrap(); + let dependents = self.load_dependent_queued_requests(room_id).await.unwrap(); assert_eq!(dependents.len(), 2); } } diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 47fa167b193..e55ec14caf3 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -36,9 +36,9 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use super::{ - send_queue::{ChildTransactionId, QueuedEvent, SerializableEventContent}, + send_queue::{ChildTransactionId, QueuedRequest, SerializableEventContent}, traits::{ComposerDraft, ServerCapabilities}, - DependentQueuedEvent, DependentQueuedEventKind, QueuedRequestKind, Result, RoomInfo, + DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo, StateChanges, StateStore, StoreError, }; use crate::{ @@ -88,8 +88,8 @@ pub struct MemoryStore { >, >, custom: StdRwLock, Vec>>, - send_queue_events: StdRwLock>>, - dependent_send_queue_events: StdRwLock>>, + send_queue_events: StdRwLock>>, + dependent_send_queue_events: StdRwLock>>, } impl MemoryStore { @@ -802,19 +802,23 @@ impl StateStore for MemoryStore { Ok(()) } - async fn save_send_queue_event( + async fn save_send_queue_request( &self, room_id: &RoomId, transaction_id: OwnedTransactionId, content: SerializableEventContent, ) -> Result<(), Self::Error> { self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().push( - QueuedEvent { kind: QueuedRequestKind::Event { content }, transaction_id, error: None }, + QueuedRequest { + kind: QueuedRequestKind::Event { content }, + transaction_id, + error: None, + }, ); Ok(()) } - async fn update_send_queue_event( + async fn update_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -837,7 +841,7 @@ impl StateStore for MemoryStore { } } - async fn remove_send_queue_event( + async fn remove_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -860,14 +864,14 @@ impl StateStore for MemoryStore { Ok(false) } - async fn load_send_queue_events( + async fn load_send_queue_requests( &self, room_id: &RoomId, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone()) } - async fn update_send_queue_event_status( + async fn update_send_queue_request_status( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -887,19 +891,19 @@ impl StateStore for MemoryStore { Ok(()) } - async fn load_rooms_with_unsent_events(&self) -> Result, Self::Error> { + async fn load_rooms_with_unsent_requests(&self) -> Result, Self::Error> { Ok(self.send_queue_events.read().unwrap().keys().cloned().collect()) } - async fn save_dependent_send_queue_event( + async fn save_dependent_queued_request( &self, room: &RoomId, parent_transaction_id: &TransactionId, own_transaction_id: ChildTransactionId, - content: DependentQueuedEventKind, + content: DependentQueuedRequestKind, ) -> Result<(), Self::Error> { self.dependent_send_queue_events.write().unwrap().entry(room.to_owned()).or_default().push( - DependentQueuedEvent { + DependentQueuedRequest { kind: content, parent_transaction_id: parent_transaction_id.to_owned(), own_transaction_id, @@ -909,7 +913,7 @@ impl StateStore for MemoryStore { Ok(()) } - async fn update_dependent_send_queue_event( + async fn update_dependent_queued_request( &self, room: &RoomId, parent_txn_id: &TransactionId, @@ -925,7 +929,7 @@ impl StateStore for MemoryStore { Ok(num_updated) } - async fn remove_dependent_send_queue_event( + async fn remove_dependent_queued_request( &self, room: &RoomId, txn_id: &ChildTransactionId, @@ -944,10 +948,10 @@ impl StateStore for MemoryStore { /// /// This returns absolutely all the dependent send queue events, whether /// they have an event id or not. - async fn load_dependent_send_queue_events( + async fn load_dependent_queued_requests( &self, room: &RoomId, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(self.dependent_send_queue_events.read().unwrap().get(room).cloned().unwrap_or_default()) } } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 8cc5de5dc4c..392dd6e5bb8 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -75,8 +75,8 @@ pub use self::integration_tests::StateStoreIntegrationTests; pub use self::{ memory_store::MemoryStore, send_queue::{ - ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, - QueuedEvent, QueuedRequestKind, SerializableEventContent, + ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, + QueuedRequest, QueuedRequestKind, SerializableEventContent, }, traits::{ ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities, diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index f76f6d95e3d..9a70c579641 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -80,7 +80,7 @@ pub enum QueuedRequestKind { /// A request to be sent with a send queue. #[derive(Clone)] -pub struct QueuedEvent { +pub struct QueuedRequest { /// The kind of queued request we're going to send. pub kind: QueuedRequestKind, @@ -94,13 +94,13 @@ pub struct QueuedEvent { pub error: Option, } -impl QueuedEvent { +impl QueuedRequest { /// Returns `Some` if the queued request is about sending an event. pub fn as_event(&self) -> Option<&SerializableEventContent> { as_variant!(&self.kind, QueuedRequestKind::Event { content } => content) } - /// True if the event couldn't be sent because of an unrecoverable API + /// True if the request couldn't be sent because of an unrecoverable API /// error. See [`Self::error`] for more details on the reason. pub fn is_wedged(&self) -> bool { self.error.is_some() @@ -145,27 +145,31 @@ pub enum QueueWedgeError { }, } -/// The specific user intent that characterizes a [`DependentQueuedEvent`]. +/// The specific user intent that characterizes a +/// [`DependentQueuedRequestKind`]. #[derive(Clone, Debug, Serialize, Deserialize)] -pub enum DependentQueuedEventKind { +pub enum DependentQueuedRequestKind { /// The event should be edited. - Edit { + #[serde(rename = "Edit")] + EditEvent { /// The new event for the content. new_content: SerializableEventContent, }, /// The event should be redacted/aborted/removed. - Redact, + #[serde(rename = "Redact")] + RedactEvent, /// The event should be reacted to, with the given key. - React { + #[serde(rename = "React")] + ReactEvent { /// Key used for the reaction. key: String, }, } -/// A transaction id identifying a [`DependentQueuedEvent`] rather than its -/// parent [`QueuedEvent`]. +/// A transaction id identifying a [`DependentQueuedRequest`] rather than its +/// parent [`QueuedRequest`]. /// /// This thin wrapper adds some safety to some APIs, making it possible to /// distinguish between the parent's `TransactionId` and the dependent event's @@ -203,42 +207,42 @@ impl From for OwnedTransactionId { } } -/// An event to be sent, depending on a [`QueuedEvent`] to be sent first. +/// A request to be sent, depending on a [`QueuedRequest`] to be sent first. /// -/// Depending on whether the event has been sent or not, this will either update -/// the local echo in the storage, or send an event equivalent to the user -/// intent to the homeserver. +/// Depending on whether the parent request has been sent or not, this will +/// either update the local echo in the storage, or materialize an equivalent +/// request implementing the user intent to the homeserver. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DependentQueuedEvent { - /// Unique identifier for this dependent queued event. +pub struct DependentQueuedRequest { + /// Unique identifier for this dependent queued request. /// /// Useful for deletion. pub own_transaction_id: ChildTransactionId, /// The kind of user intent. - pub kind: DependentQueuedEventKind, + pub kind: DependentQueuedRequestKind, /// Transaction id for the parent's local echo / used in the server request. /// - /// Note: this is the transaction id used for the depended-on event, i.e. + /// Note: this is the transaction id used for the depended-on request, i.e. /// the one that was originally sent and that's being modified with this - /// dependent event. + /// dependent request. pub parent_transaction_id: OwnedTransactionId, - /// If the parent event has been sent, the parent's event identifier + /// If the parent request has been sent, the parent's request identifier /// returned by the server once the local echo has been sent out. /// /// Note: this is the event id used for the depended-on event after it's /// been sent, not for a possible event that could have been sent - /// because of this [`DependentQueuedEvent`]. + /// because of this [`DependentQueuedRequest`]. pub event_id: Option, } #[cfg(not(tarpaulin_include))] -impl fmt::Debug for QueuedEvent { +impl fmt::Debug for QueuedRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Hide the content from the debug log. - f.debug_struct("QueuedEvent") + f.debug_struct("QueuedRequest") .field("transaction_id", &self.transaction_id) .field("is_wedged", &self.is_wedged()) .finish_non_exhaustive() diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 50f2a52fae4..e3ef3c9de59 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -41,8 +41,8 @@ use ruma::{ use serde::{Deserialize, Serialize}; use super::{ - ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, - QueuedEvent, SerializableEventContent, StateChanges, StoreError, + ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, + QueuedRequest, SerializableEventContent, StateChanges, StoreError, }; use crate::{ deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState}, @@ -343,7 +343,7 @@ pub trait StateStore: AsyncTraitDeps { /// * `room_id` - The `RoomId` of the room to delete. async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error>; - /// Save an event to be sent by a send queue later. + /// Save a request to be sent by a send queue later (e.g. sending an event). /// /// # Arguments /// @@ -352,98 +352,100 @@ pub trait StateStore: AsyncTraitDeps { /// (and its transaction). Note: this is expected to be randomly generated /// and thus unique. /// * `content` - Serializable event content to be sent. - async fn save_send_queue_event( + async fn save_send_queue_request( &self, room_id: &RoomId, transaction_id: OwnedTransactionId, content: SerializableEventContent, ) -> Result<(), Self::Error>; - /// Updates a send queue event with the given content, and resets its wedged - /// status to false. + /// Updates a send queue request with the given content, and resets its + /// error status. /// /// # Arguments /// /// * `room_id` - The `RoomId` of the send queue's room. - /// * `transaction_id` - The unique key identifying the event to be sent + /// * `transaction_id` - The unique key identifying the request to be sent /// (and its transaction). /// * `content` - Serializable event content to replace the original one. /// - /// Returns true if an event has been updated, or false otherwise. - async fn update_send_queue_event( + /// Returns true if a request has been updated, or false otherwise. + async fn update_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, content: SerializableEventContent, ) -> Result; - /// Remove an event previously inserted with [`Self::save_send_queue_event`] - /// from the database, based on its transaction id. + /// Remove a request previously inserted with + /// [`Self::save_send_queue_request`] from the database, based on its + /// transaction id. /// - /// Returns true if an event has been removed, or false otherwise. - async fn remove_send_queue_event( + /// Returns true if something has been removed, or false otherwise. + async fn remove_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, ) -> Result; - /// Loads all the send queue events for the given room. - async fn load_send_queue_events( + /// Loads all the send queue requests for the given room. + async fn load_send_queue_requests( &self, room_id: &RoomId, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; /// Updates the send queue error status (wedge) for a given send queue - /// event. - /// Set `error` to None if the problem has been resolved and the event was - /// finally sent. - async fn update_send_queue_event_status( + /// request. + async fn update_send_queue_request_status( &self, room_id: &RoomId, transaction_id: &TransactionId, error: Option, ) -> Result<(), Self::Error>; - /// Loads all the rooms which have any pending events in their send queue. - async fn load_rooms_with_unsent_events(&self) -> Result, Self::Error>; + /// Loads all the rooms which have any pending requests in their send queue. + async fn load_rooms_with_unsent_requests(&self) -> Result, Self::Error>; - /// Add a new entry to the list of dependent send queue event for an event. - async fn save_dependent_send_queue_event( + /// Add a new entry to the list of dependent send queue requests for a + /// parent request. + async fn save_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, - content: DependentQueuedEventKind, + content: DependentQueuedRequestKind, ) -> Result<(), Self::Error>; - /// Update a set of dependent send queue events with an event id, + /// Update a set of dependent send queue requests with an event id, /// effectively marking them as ready. /// - /// Returns the number of updated events. - async fn update_dependent_send_queue_event( + /// Returns the number of updated requests. + async fn update_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, event_id: OwnedEventId, ) -> Result; - /// Remove a specific dependent send queue event by id. + /// Remove a specific dependent send queue request by id. /// - /// Returns true if the dependent send queue event has been indeed removed. - async fn remove_dependent_send_queue_event( + /// Returns true if the dependent send queue request has been indeed + /// removed. + async fn remove_dependent_queued_request( &self, room: &RoomId, own_txn_id: &ChildTransactionId, ) -> Result; - /// List all the dependent send queue events. + /// List all the dependent send queue requests. /// - /// This returns absolutely all the dependent send queue events, whether - /// they have an event id or not. They must be returned in insertion order. - async fn load_dependent_send_queue_events( + /// This returns absolutely all the dependent send queue requests, whether + /// they have a parent event id or not. As a contract for implementors, they + /// must be returned in insertion order. + async fn load_dependent_queued_requests( &self, room: &RoomId, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; } #[repr(transparent)] @@ -629,93 +631,93 @@ impl StateStore for EraseStateStoreError { self.0.remove_room(room_id).await.map_err(Into::into) } - async fn save_send_queue_event( + async fn save_send_queue_request( &self, room_id: &RoomId, transaction_id: OwnedTransactionId, content: SerializableEventContent, ) -> Result<(), Self::Error> { - self.0.save_send_queue_event(room_id, transaction_id, content).await.map_err(Into::into) + self.0.save_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into) } - async fn update_send_queue_event( + async fn update_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, content: SerializableEventContent, ) -> Result { - self.0.update_send_queue_event(room_id, transaction_id, content).await.map_err(Into::into) + self.0.update_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into) } - async fn remove_send_queue_event( + async fn remove_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, ) -> Result { - self.0.remove_send_queue_event(room_id, transaction_id).await.map_err(Into::into) + self.0.remove_send_queue_request(room_id, transaction_id).await.map_err(Into::into) } - async fn load_send_queue_events( + async fn load_send_queue_requests( &self, room_id: &RoomId, - ) -> Result, Self::Error> { - self.0.load_send_queue_events(room_id).await.map_err(Into::into) + ) -> Result, Self::Error> { + self.0.load_send_queue_requests(room_id).await.map_err(Into::into) } - async fn update_send_queue_event_status( + async fn update_send_queue_request_status( &self, room_id: &RoomId, transaction_id: &TransactionId, error: Option, ) -> Result<(), Self::Error> { self.0 - .update_send_queue_event_status(room_id, transaction_id, error) + .update_send_queue_request_status(room_id, transaction_id, error) .await .map_err(Into::into) } - async fn load_rooms_with_unsent_events(&self) -> Result, Self::Error> { - self.0.load_rooms_with_unsent_events().await.map_err(Into::into) + async fn load_rooms_with_unsent_requests(&self) -> Result, Self::Error> { + self.0.load_rooms_with_unsent_requests().await.map_err(Into::into) } - async fn save_dependent_send_queue_event( + async fn save_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, - content: DependentQueuedEventKind, + content: DependentQueuedRequestKind, ) -> Result<(), Self::Error> { self.0 - .save_dependent_send_queue_event(room_id, parent_txn_id, own_txn_id, content) + .save_dependent_queued_request(room_id, parent_txn_id, own_txn_id, content) .await .map_err(Into::into) } - async fn update_dependent_send_queue_event( + async fn update_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, event_id: OwnedEventId, ) -> Result { self.0 - .update_dependent_send_queue_event(room_id, parent_txn_id, event_id) + .update_dependent_queued_request(room_id, parent_txn_id, event_id) .await .map_err(Into::into) } - async fn remove_dependent_send_queue_event( + async fn remove_dependent_queued_request( &self, room_id: &RoomId, own_txn_id: &ChildTransactionId, ) -> Result { - self.0.remove_dependent_send_queue_event(room_id, own_txn_id).await.map_err(Into::into) + self.0.remove_dependent_queued_request(room_id, own_txn_id).await.map_err(Into::into) } - async fn load_dependent_send_queue_events( + async fn load_dependent_queued_requests( &self, room_id: &RoomId, - ) -> Result, Self::Error> { - self.0.load_dependent_send_queue_events(room_id).await.map_err(Into::into) + ) -> Result, Self::Error> { + self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into) } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 0a90b1b8cc4..3dd7fab7a6b 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -25,9 +25,9 @@ use indexed_db_futures::prelude::*; use matrix_sdk_base::{ deserialized_responses::RawAnySyncOrStrippedState, store::{ - ChildTransactionId, ComposerDraft, DependentQueuedEvent, DependentQueuedEventKind, - QueuedEvent, QueuedRequestKind, SerializableEventContent, ServerCapabilities, StateChanges, - StateStore, StoreError, + ChildTransactionId, ComposerDraft, DependentQueuedRequest, DependentQueuedRequestKind, + QueuedRequest, QueuedRequestKind, SerializableEventContent, ServerCapabilities, + StateChanges, StateStore, StoreError, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, StateStoreDataKey, StateStoreDataValue, }; @@ -423,14 +423,14 @@ impl IndexeddbStateStore { } } -/// A superset of [`QueuedEvent`] that also contains the room id, since we want -/// to return them. +/// A superset of [`QueuedRequest`] that also contains the room id, since we +/// want to return them. #[derive(Serialize, Deserialize)] -struct PersistedQueuedEvent { +struct PersistedQueuedRequest { /// In which room is this event going to be sent. pub room_id: OwnedRoomId, - // All these fields are the same as in [`QueuedEvent`]. + // All these fields are the same as in [`QueuedRequest`]. /// Kind. Optional because it might be missing from previous formats. kind: Option, transaction_id: OwnedTransactionId, @@ -444,8 +444,8 @@ struct PersistedQueuedEvent { event: Option, } -impl PersistedQueuedEvent { - fn into_queued_event(self) -> Option { +impl PersistedQueuedRequest { + fn into_queued_request(self) -> Option { let kind = self.kind.or_else(|| self.event.map(|content| QueuedRequestKind::Event { content }))?; @@ -459,7 +459,7 @@ impl PersistedQueuedEvent { _ => self.error, }; - Some(QueuedEvent { kind, transaction_id: self.transaction_id, error }) + Some(QueuedRequest { kind, transaction_id: self.transaction_id, error }) } } @@ -1324,7 +1324,7 @@ impl_state_store!({ self.get_user_ids_inner(room_id, memberships, false).await } - async fn save_send_queue_event( + async fn save_send_queue_request( &self, room_id: &RoomId, transaction_id: OwnedTransactionId, @@ -1338,18 +1338,19 @@ impl_state_store!({ let obj = tx.object_store(keys::ROOM_SEND_QUEUE)?; - // We store an encoded vector of the queued events, with their transaction ids. + // We store an encoded vector of the queued requests, with their transaction + // ids. // Reload the previous vector for this room, or create an empty one. let prev = obj.get(&encoded_key)?.await?; let mut prev = prev.map_or_else( || Ok(Vec::new()), - |val| self.deserialize_value::>(&val), + |val| self.deserialize_value::>(&val), )?; - // Push the new event. - prev.push(PersistedQueuedEvent { + // Push the new request. + prev.push(PersistedQueuedRequest { room_id: room_id.to_owned(), kind: Some(QueuedRequestKind::Event { content }), transaction_id, @@ -1366,7 +1367,7 @@ impl_state_store!({ Ok(()) } - async fn update_send_queue_event( + async fn update_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -1380,17 +1381,18 @@ impl_state_store!({ let obj = tx.object_store(keys::ROOM_SEND_QUEUE)?; - // We store an encoded vector of the queued events, with their transaction ids. + // We store an encoded vector of the queued requests, with their transaction + // ids. // Reload the previous vector for this room, or create an empty one. let prev = obj.get(&encoded_key)?.await?; let mut prev = prev.map_or_else( || Ok(Vec::new()), - |val| self.deserialize_value::>(&val), + |val| self.deserialize_value::>(&val), )?; - // Modify the one event. + // Modify the one request. if let Some(entry) = prev.iter_mut().find(|entry| entry.transaction_id == transaction_id) { entry.kind = Some(QueuedRequestKind::Event { content }); // Reset the error state. @@ -1409,7 +1411,7 @@ impl_state_store!({ } } - async fn remove_send_queue_event( + async fn remove_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -1423,11 +1425,12 @@ impl_state_store!({ let obj = tx.object_store(keys::ROOM_SEND_QUEUE)?; - // We store an encoded vector of the queued events, with their transaction ids. + // We store an encoded vector of the queued requests, with their transaction + // ids. // Reload the previous vector for this room. if let Some(val) = obj.get(&encoded_key)?.await? { - let mut prev = self.deserialize_value::>(&val)?; + let mut prev = self.deserialize_value::>(&val)?; if let Some(pos) = prev.iter().position(|item| item.transaction_id == transaction_id) { prev.remove(pos); @@ -1445,10 +1448,11 @@ impl_state_store!({ Ok(false) } - async fn load_send_queue_events(&self, room_id: &RoomId) -> Result> { + async fn load_send_queue_requests(&self, room_id: &RoomId) -> Result> { let encoded_key = self.encode_key(keys::ROOM_SEND_QUEUE, room_id); - // We store an encoded vector of the queued events, with their transaction ids. + // We store an encoded vector of the queued requests, with their transaction + // ids. let prev = self .inner .transaction_on_one_with_mode(keys::ROOM_SEND_QUEUE, IdbTransactionMode::Readwrite)? @@ -1458,13 +1462,13 @@ impl_state_store!({ let prev = prev.map_or_else( || Ok(Vec::new()), - |val| self.deserialize_value::>(&val), + |val| self.deserialize_value::>(&val), )?; - Ok(prev.into_iter().filter_map(PersistedQueuedEvent::into_queued_event).collect()) + Ok(prev.into_iter().filter_map(PersistedQueuedRequest::into_queued_request).collect()) } - async fn update_send_queue_event_status( + async fn update_send_queue_request_status( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -1479,12 +1483,12 @@ impl_state_store!({ let obj = tx.object_store(keys::ROOM_SEND_QUEUE)?; if let Some(val) = obj.get(&encoded_key)?.await? { - let mut prev = self.deserialize_value::>(&val)?; - if let Some(queued_event) = + let mut prev = self.deserialize_value::>(&val)?; + if let Some(request) = prev.iter_mut().find(|item| item.transaction_id == transaction_id) { - queued_event.is_wedged = None; - queued_event.error = error; + request.is_wedged = None; + request.error = error; obj.put_key_val(&encoded_key, &self.serialize_value(&prev)?)?; } } @@ -1494,7 +1498,7 @@ impl_state_store!({ Ok(()) } - async fn load_rooms_with_unsent_events(&self) -> Result> { + async fn load_rooms_with_unsent_requests(&self) -> Result> { let tx = self .inner .transaction_on_one_with_mode(keys::ROOM_SEND_QUEUE, IdbTransactionMode::Readwrite)?; @@ -1505,8 +1509,8 @@ impl_state_store!({ .get_all()? .await? .into_iter() - .map(|item| self.deserialize_value::>(&item)) - .collect::>, _>>()? + .map(|item| self.deserialize_value::>(&item)) + .collect::>, _>>()? .into_iter() .flat_map(|vec| vec.into_iter().map(|item| item.room_id)) .collect::>(); @@ -1514,12 +1518,12 @@ impl_state_store!({ Ok(all_entries.into_iter().collect()) } - async fn save_dependent_send_queue_event( + async fn save_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, - content: DependentQueuedEventKind, + content: DependentQueuedRequestKind, ) -> Result<()> { let encoded_key = self.encode_key(keys::DEPENDENT_SEND_QUEUE, room_id); @@ -1530,17 +1534,17 @@ impl_state_store!({ let obj = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?; - // We store an encoded vector of the dependent events. + // We store an encoded vector of the dependent requests. // Reload the previous vector for this room, or create an empty one. let prev = obj.get(&encoded_key)?.await?; let mut prev = prev.map_or_else( || Ok(Vec::new()), - |val| self.deserialize_value::>(&val), + |val| self.deserialize_value::>(&val), )?; - // Push the new event. - prev.push(DependentQueuedEvent { + // Push the new request. + prev.push(DependentQueuedRequest { kind: content, parent_transaction_id: parent_txn_id.to_owned(), own_transaction_id: own_txn_id, @@ -1555,7 +1559,7 @@ impl_state_store!({ Ok(()) } - async fn update_dependent_send_queue_event( + async fn update_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, @@ -1570,16 +1574,16 @@ impl_state_store!({ let obj = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?; - // We store an encoded vector of the dependent events. + // We store an encoded vector of the dependent requests. // Reload the previous vector for this room, or create an empty one. let prev = obj.get(&encoded_key)?.await?; let mut prev = prev.map_or_else( || Ok(Vec::new()), - |val| self.deserialize_value::>(&val), + |val| self.deserialize_value::>(&val), )?; - // Modify all events that match. + // Modify all requests that match. let mut num_updated = 0; for entry in prev.iter_mut().filter(|entry| entry.parent_transaction_id == parent_txn_id) { entry.event_id = Some(event_id.clone()); @@ -1594,7 +1598,7 @@ impl_state_store!({ Ok(num_updated) } - async fn remove_dependent_send_queue_event( + async fn remove_dependent_queued_request( &self, room_id: &RoomId, txn_id: &ChildTransactionId, @@ -1608,10 +1612,10 @@ impl_state_store!({ let obj = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?; - // We store an encoded vector of the dependent events. + // We store an encoded vector of the dependent requests. // Reload the previous vector for this room. if let Some(val) = obj.get(&encoded_key)?.await? { - let mut prev = self.deserialize_value::>(&val)?; + let mut prev = self.deserialize_value::>(&val)?; if let Some(pos) = prev.iter().position(|item| item.own_transaction_id == *txn_id) { prev.remove(pos); @@ -1629,13 +1633,13 @@ impl_state_store!({ Ok(false) } - async fn load_dependent_send_queue_events( + async fn load_dependent_queued_requests( &self, room_id: &RoomId, - ) -> Result> { + ) -> Result> { let encoded_key = self.encode_key(keys::DEPENDENT_SEND_QUEUE, room_id); - // We store an encoded vector of the dependent events. + // We store an encoded vector of the dependent requests. let prev = self .inner .transaction_on_one_with_mode( @@ -1648,7 +1652,7 @@ impl_state_store!({ prev.map_or_else( || Ok(Vec::new()), - |val| self.deserialize_value::>(&val), + |val| self.deserialize_value::>(&val), ) } }); @@ -1682,17 +1686,13 @@ mod migration_tests { }; use serde::{Deserialize, Serialize}; - use crate::state_store::PersistedQueuedEvent; + use crate::state_store::PersistedQueuedRequest; #[derive(Serialize, Deserialize)] - struct OldPersistedQueuedEvent { - /// In which room is this event going to be sent. - pub room_id: OwnedRoomId, - - // All these fields are the same as in [`QueuedEvent`]. + struct OldPersistedQueuedRequest { + room_id: OwnedRoomId, event: SerializableEventContent, transaction_id: OwnedTransactionId, - is_wedged: bool, } @@ -1707,18 +1707,18 @@ mod migration_tests { SerializableEventContent::new(&RoomMessageEventContent::text_plain("Hello").into()) .unwrap(); - let old_persisted_queue_event = OldPersistedQueuedEvent { + let old_persisted_queue_event = OldPersistedQueuedRequest { room_id: room_a_id.to_owned(), event: content, transaction_id: transaction_id.clone(), is_wedged: true, }; - let serialized_persisted_event = serde_json::to_vec(&old_persisted_queue_event).unwrap(); + let serialized_persisted = serde_json::to_vec(&old_persisted_queue_event).unwrap(); // Load it with the new version. - let new_persisted: PersistedQueuedEvent = - serde_json::from_slice(&serialized_persisted_event).unwrap(); + let new_persisted: PersistedQueuedRequest = + serde_json::from_slice(&serialized_persisted).unwrap(); assert_eq!(new_persisted.is_wedged, Some(true)); assert!(new_persisted.error.is_none()); @@ -1726,7 +1726,7 @@ mod migration_tests { assert!(new_persisted.event.is_some()); assert!(new_persisted.kind.is_none()); - let queued = new_persisted.into_queued_event().unwrap(); + let queued = new_persisted.into_queued_request().unwrap(); assert_matches!(queued.kind, QueuedRequestKind::Event { .. }); assert_eq!(queued.transaction_id, transaction_id); assert!(queued.error.is_some()); diff --git a/crates/matrix-sdk-sqlite/migrations/state_store/005_send_queue_dependent_events.sql b/crates/matrix-sdk-sqlite/migrations/state_store/005_send_queue_dependent_events.sql index bb522d2d008..03e65c05748 100644 --- a/crates/matrix-sdk-sqlite/migrations/state_store/005_send_queue_dependent_events.sql +++ b/crates/matrix-sdk-sqlite/migrations/state_store/005_send_queue_dependent_events.sql @@ -14,6 +14,6 @@ CREATE TABLE "dependent_send_queue_events" ( -- Used as a value (thus encrypted/decrypted), can be null. "event_id" BLOB NULL, - -- Serialized `DependentQueuedEventKind`, used as a value (thus encrypted/decrypted). + -- Serialized `DependentQueuedRequestKind`, used as a value (thus encrypted/decrypted). "content" BLOB NOT NULL ); diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index bc8b9d00327..86f2b95d525 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -11,8 +11,8 @@ use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ deserialized_responses::{RawAnySyncOrStrippedState, SyncOrStrippedState}, store::{ - migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedEvent, - DependentQueuedEventKind, QueueWedgeError, QueuedEvent, QueuedRequestKind, + migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedRequest, + DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, SerializableEventContent, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStore, @@ -1670,7 +1670,7 @@ impl StateStore for SqliteStateStore { .await } - async fn save_send_queue_event( + async fn save_send_queue_request( &self, room_id: &RoomId, transaction_id: OwnedTransactionId, @@ -1695,7 +1695,7 @@ impl StateStore for SqliteStateStore { .await } - async fn update_send_queue_event( + async fn update_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -1718,7 +1718,7 @@ impl StateStore for SqliteStateStore { Ok(num_updated > 0) } - async fn remove_send_queue_event( + async fn remove_send_queue_request( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -1742,10 +1742,10 @@ impl StateStore for SqliteStateStore { Ok(num_deleted > 0) } - async fn load_send_queue_events( + async fn load_send_queue_requests( &self, room_id: &RoomId, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { let room_id = self.encode_key(keys::SEND_QUEUE, room_id); // Note: ROWID is always present and is an auto-incremented integer counter. We @@ -1764,19 +1764,19 @@ impl StateStore for SqliteStateStore { ) .await?; - let mut queued_events = Vec::with_capacity(res.len()); + let mut requests = Vec::with_capacity(res.len()); for entry in res { - queued_events.push(QueuedEvent { + requests.push(QueuedRequest { transaction_id: entry.0.into(), kind: QueuedRequestKind::Event { content: self.deserialize_json(&entry.1)? }, error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?, }); } - Ok(queued_events) + Ok(requests) } - async fn update_send_queue_event_status( + async fn update_send_queue_request_status( &self, room_id: &RoomId, transaction_id: &TransactionId, @@ -1799,7 +1799,7 @@ impl StateStore for SqliteStateStore { .await } - async fn load_rooms_with_unsent_events(&self) -> Result, Self::Error> { + async fn load_rooms_with_unsent_requests(&self) -> Result, Self::Error> { // If the values were not encrypted, we could use `SELECT DISTINCT` here, but we // have to manually do the deduplication: indeed, for all X, encrypt(X) // != encrypted(X), since we use a nonce in the encryption process. @@ -1822,12 +1822,12 @@ impl StateStore for SqliteStateStore { .collect()) } - async fn save_dependent_send_queue_event( + async fn save_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, - content: DependentQueuedEventKind, + content: DependentQueuedRequestKind, ) -> Result<()> { let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id); let content = self.serialize_json(&content)?; @@ -1850,7 +1850,7 @@ impl StateStore for SqliteStateStore { .await } - async fn update_dependent_send_queue_event( + async fn update_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, @@ -1873,7 +1873,7 @@ impl StateStore for SqliteStateStore { .await } - async fn remove_dependent_send_queue_event( + async fn remove_dependent_queued_request( &self, room_id: &RoomId, txn_id: &ChildTransactionId, @@ -1897,10 +1897,10 @@ impl StateStore for SqliteStateStore { Ok(num_deleted > 0) } - async fn load_dependent_send_queue_events( + async fn load_dependent_queued_requests( &self, room_id: &RoomId, - ) -> Result> { + ) -> Result> { let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id); // Note: transaction_id is not encoded, see why in `save_send_queue_event`. @@ -1919,7 +1919,7 @@ impl StateStore for SqliteStateStore { let mut dependent_events = Vec::with_capacity(res.len()); for entry in res { - dependent_events.push(DependentQueuedEvent { + dependent_events.push(DependentQueuedRequest { own_transaction_id: entry.0.into(), parent_transaction_id: entry.1.into(), event_id: entry.2.map(|bytes| self.deserialize_value(&bytes)).transpose()?, @@ -2290,12 +2290,12 @@ mod migration_tests { // This transparently migrates to the latest version. let store = SqliteStateStore::open(path, Some(SECRET)).await.unwrap(); - let queued_event = store.load_send_queue_events(room_a_id).await.unwrap(); + let requests = store.load_send_queue_requests(room_a_id).await.unwrap(); - assert_eq!(queued_event.len(), 2); + assert_eq!(requests.len(), 2); let migrated_wedged = - queued_event.iter().find(|e| e.transaction_id == wedged_event_transaction_id).unwrap(); + requests.iter().find(|e| e.transaction_id == wedged_event_transaction_id).unwrap(); assert!(migrated_wedged.is_wedged()); assert_matches!( @@ -2303,7 +2303,7 @@ mod migration_tests { Some(QueueWedgeError::GenericApiError { .. }) ); - let migrated_ok = queued_event + let migrated_ok = requests .iter() .find(|e| e.transaction_id == local_event_transaction_id.clone()) .unwrap(); diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 5642b61d42e..fcee608eed2 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -38,7 +38,7 @@ //! - enable/disable them all at once with [`SendQueue::set_enabled()`]. //! - get notifications about send errors with [`SendQueue::subscribe_errors`]. //! - reload all unsent events that had been persisted in storage using -//! [`SendQueue::respawn_tasks_for_rooms_with_unsent_events()`]. It is +//! [`SendQueue::respawn_tasks_for_rooms_with_unsent_requests()`]. It is //! recommended to call this method during initialization of a client, //! otherwise persisted unsent events will only be re-sent after the send //! queue for the given room has been reopened for the first time. @@ -53,8 +53,8 @@ use std::{ use matrix_sdk_base::{ store::{ - ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, QueueWedgeError, - QueuedEvent, QueuedRequestKind, SerializableEventContent, + ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, + QueuedRequest, QueuedRequestKind, SerializableEventContent, }, RoomState, StoreError, }; @@ -97,16 +97,16 @@ impl SendQueue { Self { client } } - /// Reload all the rooms which had unsent events, and respawn tasks for + /// Reload all the rooms which had unsent requests, and respawn tasks for /// those rooms. - pub async fn respawn_tasks_for_rooms_with_unsent_events(&self) { + pub async fn respawn_tasks_for_rooms_with_unsent_requests(&self) { if !self.is_enabled() { return; } let room_ids = - self.client.store().load_rooms_with_unsent_events().await.unwrap_or_else(|err| { - warn!("error when loading rooms with unsent events: {err}"); + self.client.store().load_rooms_with_unsent_requests().await.unwrap_or_else(|err| { + warn!("error when loading rooms with unsent requests: {err}"); Vec::new() }); @@ -118,11 +118,14 @@ impl SendQueue { } } + /// Tiny helper to get the send queue's global context from the [`Client`]. #[inline(always)] fn data(&self) -> &SendQueueData { &self.client.inner.send_queue_data } + /// Get or create a new send queue for a given room, and insert it into our + /// memoized rooms mapping. fn for_room(&self, room: Room) -> RoomSendQueue { let data = self.data(); @@ -141,7 +144,9 @@ impl SendQueue { &self.client, owned_room_id.clone(), ); + map.insert(owned_room_id, room_q.clone()); + room_q } @@ -150,9 +155,9 @@ impl SendQueue { /// If we're disabling the queue, and requests were being sent, they're not /// aborted, and will continue until a status resolves (error responses /// will keep the events in the buffer of events to send later). The - /// disablement will happen before the next event is sent. + /// disablement will happen before the next request is sent. /// - /// This may wake up background tasks and resume sending of events in the + /// This may wake up background tasks and resume sending of requests in the /// background. pub async fn set_enabled(&self, enabled: bool) { debug!(?enabled, "setting global send queue enablement"); @@ -165,8 +170,8 @@ impl SendQueue { } // Reload some extra rooms that might not have been awaken yet, but could have - // events from previous sessions. - self.respawn_tasks_for_rooms_with_unsent_events().await; + // requests from previous sessions. + self.respawn_tasks_for_rooms_with_unsent_requests().await; } /// Returns whether the send queue is enabled, at a client-wide @@ -176,32 +181,32 @@ impl SendQueue { } /// A subscriber to the enablement status (enabled or disabled) of the - /// send queue. + /// send queue, along with useful errors. pub fn subscribe_errors(&self) -> broadcast::Receiver { self.data().error_reporter.subscribe() } } -/// A specific room ran into an error, and has disabled itself. +/// A specific room's send queue ran into an error, and it has disabled itself. #[derive(Clone, Debug)] pub struct SendQueueRoomError { - /// Which room is failing? + /// For which room is the send queue failing? pub room_id: OwnedRoomId, - /// The error the room has ran into, when trying to send an event. + /// The error the room has ran into, when trying to send a request. pub error: Arc, /// Whether the error is considered recoverable or not. /// /// An error that's recoverable will disable the room's send queue, while an - /// unrecoverable error will be parked, until the user decides to cancel - /// sending it. + /// unrecoverable error will be parked, until the user decides to do + /// something about it. pub is_recoverable: bool, } impl Client { /// Returns a [`SendQueue`] that handles sending, retrying and not - /// forgetting about messages that are to be sent. + /// forgetting about requests that are to be sent. pub fn send_queue(&self) -> SendQueue { SendQueue::new(self.clone()) } @@ -321,10 +326,11 @@ impl RoomSendQueue { /// the [`Self::subscribe()`] method to get updates about the sending of /// that event. /// - /// By default, if sending the event fails on the first attempt, it will be - /// retried a few times. If sending failed, the entire client's sending - /// queue will be disabled, and it will need to be manually re-enabled - /// by the caller. + /// By default, if sending failed on the first attempt, it will be retried a + /// few times. If sending failed after those retries, the entire + /// client's sending queue will be disabled, and it will need to be + /// manually re-enabled by the caller (e.g. after network is back, or when + /// something has been done about the faulty requests). pub async fn send_raw( &self, content: Raw, @@ -368,10 +374,11 @@ impl RoomSendQueue { /// the [`Self::subscribe()`] method to get updates about the sending of /// that event. /// - /// By default, if sending the event fails on the first attempt, it will be - /// retried a few times. If sending failed, the entire client's sending - /// queue will be disabled, and it will need to be manually re-enabled - /// by the caller. + /// By default, if sending failed on the first attempt, it will be retried a + /// few times. If sending failed after those retries, the entire + /// client's sending queue will be disabled, and it will need to be + /// manually re-enabled by the caller (e.g. after network is back, or when + /// something has been done about the faulty requests). pub async fn send( &self, content: AnyMessageLikeEventContent, @@ -383,8 +390,8 @@ impl RoomSendQueue { .await } - /// Returns the current local events as well as a receiver to listen to the - /// send queue updates, as defined in [`RoomSendQueueUpdate`]. + /// Returns the current local requests as well as a receiver to listen to + /// the send queue updates, as defined in [`RoomSendQueueUpdate`]. pub async fn subscribe( &self, ) -> Result<(Vec, broadcast::Receiver), RoomSendQueueError> @@ -394,6 +401,11 @@ impl RoomSendQueue { Ok((local_echoes, self.inner.updates.subscribe())) } + /// A task that must be spawned in the async runtime, running in the + /// background for each room that has a send queue. + /// + /// It only progresses forward: nothing can be cancelled at any point, which + /// makes the implementation not overly complicated to follow. #[instrument(skip_all, fields(room_id = %room.room_id()))] async fn sending_task( room: WeakRoom, @@ -413,10 +425,10 @@ impl RoomSendQueue { break; } - // Try to apply dependent events now; those applying to previously failed + // Try to apply dependent requests now; those applying to previously failed // attempts (local echoes) would succeed now. - if let Err(err) = queue.apply_dependent_events().await { - warn!("errors when applying dependent events: {err}"); + if let Err(err) = queue.apply_dependent_requests().await { + warn!("errors when applying dependent requests: {err}"); } if !locally_enabled.load(Ordering::SeqCst) { @@ -426,8 +438,8 @@ impl RoomSendQueue { continue; } - let queued_event = match queue.peek_next_to_send().await { - Ok(Some(event)) => event, + let queued_request = match queue.peek_next_to_send().await { + Ok(Some(request)) => request, Ok(None) => { trace!("queue is empty, sleeping"); @@ -437,12 +449,12 @@ impl RoomSendQueue { } Err(err) => { - warn!("error when loading next event to send: {err}"); + warn!("error when loading next request to send: {err}"); continue; } }; - trace!(txn_id = %queued_event.transaction_id, "received an event to send!"); + trace!(txn_id = %queued_request.transaction_id, "received a request to send!"); let Some(room) = room.get() else { if is_dropping.load(Ordering::SeqCst) { @@ -452,20 +464,23 @@ impl RoomSendQueue { continue; }; - let (event, event_type) = queued_event.as_event().unwrap().raw(); + let (event, event_type) = match &queued_request.kind { + QueuedRequestKind::Event { content } => content.raw(), + }; + match room - .send_raw(&event_type.to_string(), event) - .with_transaction_id(&queued_event.transaction_id) + .send_raw(event_type, event) + .with_transaction_id(&queued_request.transaction_id) .with_request_config(RequestConfig::short_retry()) .await { Ok(res) => { - trace!(txn_id = %queued_event.transaction_id, event_id = %res.event_id, "successfully sent"); + trace!(txn_id = %queued_request.transaction_id, event_id = %res.event_id, "successfully sent"); - match queue.mark_as_sent(&queued_event.transaction_id, &res.event_id).await { + match queue.mark_as_sent(&queued_request.transaction_id, &res.event_id).await { Ok(()) => { let _ = updates.send(RoomSendQueueUpdate::SentEvent { - transaction_id: queued_event.transaction_id, + transaction_id: queued_request.transaction_id, event_id: res.event_id, }); } @@ -497,35 +512,34 @@ impl RoomSendQueue { }; if is_recoverable { - warn!(txn_id = %queued_event.transaction_id, error = ?err, "Recoverable error when sending event: {err}, disabling send queue"); + warn!(txn_id = %queued_request.transaction_id, error = ?err, "Recoverable error when sending request: {err}, disabling send queue"); - // In this case, we intentionally keep the event in the queue, but mark it + // In this case, we intentionally keep the request in the queue, but mark it // as not being sent anymore. - queue.mark_as_not_being_sent(&queued_event.transaction_id).await; + queue.mark_as_not_being_sent(&queued_request.transaction_id).await; // Let observers know about a failure *after* we've marked the item as not // being sent anymore. Otherwise, there's a possible race where a caller - // might try to remove an item, while it's still - // marked as being sent, resulting in a cancellation - // failure. + // might try to remove an item, while it's still marked as being sent, + // resulting in a cancellation failure. // Disable the queue for this room after a recoverable error happened. This // should be the sign that this error is temporary (maybe network // disconnected, maybe the server had a hiccup). locally_enabled.store(false, Ordering::SeqCst); } else { - warn!(txn_id = %queued_event.transaction_id, error = ?err, "Unrecoverable error when sending event: {err}"); + warn!(txn_id = %queued_request.transaction_id, error = ?err, "Unrecoverable error when sending request: {err}"); - // Mark the event as wedged, so it's not picked at any future point. + // Mark the request as wedged, so it's not picked at any future point. if let Err(storage_error) = queue .mark_as_wedged( - &queued_event.transaction_id, + &queued_request.transaction_id, QueueWedgeError::from(&err), ) .await { - warn!("unable to mark event as wedged: {storage_error}"); + warn!("unable to mark request as wedged: {storage_error}"); } } @@ -538,7 +552,7 @@ impl RoomSendQueue { }); let _ = updates.send(RoomSendQueueUpdate::SendError { - transaction_id: queued_event.transaction_id, + transaction_id: queued_request.transaction_id, error, is_recoverable, }); @@ -574,7 +588,7 @@ impl RoomSendQueue { .await .map_err(RoomSendQueueError::StorageError)?; - // Wake up the queue, in case the room was asleep before unwedging the event. + // Wake up the queue, in case the room was asleep before unwedging the request. self.inner.notifier.notify_one(); let _ = self @@ -595,14 +609,17 @@ impl From<&crate::Error> for QueueWedgeError { SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice(user_map) => { QueueWedgeError::InsecureDevices { user_device_map: user_map.clone() } } + SessionRecipientCollectionError::VerifiedUserChangedIdentity(users) => { QueueWedgeError::IdentityViolations { users: users.clone() } } + SessionRecipientCollectionError::CrossSigningNotSetup | SessionRecipientCollectionError::SendingFromUnverifiedDevice => { QueueWedgeError::CrossVerificationRequired } }, + _ => QueueWedgeError::GenericApiError { msg: value.to_string() }, } } @@ -612,23 +629,24 @@ struct RoomSendQueueInner { /// The room which this send queue relates to. room: WeakRoom, - /// Broadcaster for notifications about the statuses of events to be sent. + /// Broadcaster for notifications about the statuses of requests to be sent. /// /// Can be subscribed to from the outside. updates: broadcast::Sender, - /// Queue of events that are either to be sent, or being sent. + /// Queue of requests that are either to be sent, or being sent. /// - /// When an event has been sent to the server, it is removed from that queue - /// *after* being sent. That way, we will retry sending upon failure, in - /// the same order events have been inserted in the first place. + /// When a request has been sent to the server, it is removed from that + /// queue *after* being sent. That way, we will retry sending upon + /// failure, in the same order requests have been inserted in the first + /// place. queue: QueueStorage, /// A notifier that's updated any time common data is touched (stopped or /// enabled statuses), or the associated room [`QueueStorage`]. notifier: Arc, - /// Should the room process new events or not (because e.g. it might be + /// Should the room process new requests or not (because e.g. it might be /// running off the network)? locally_enabled: Arc, @@ -645,14 +663,14 @@ struct QueueStorage { /// To which room is this storage related. room_id: OwnedRoomId, - /// All the queued events that are being sent at the moment. + /// All the queued requests that are being sent at the moment. /// /// It also serves as an internal lock on the storage backend. being_sent: Arc>>, } impl QueueStorage { - /// Create a new synchronized queue for queuing events to be sent later. + /// Create a new queue for queuing requests to be sent later. fn new(client: WeakClient, room: OwnedRoomId) -> Self { Self { room_id: room, being_sent: Default::default(), client } } @@ -673,39 +691,40 @@ impl QueueStorage { self.client()? .store() - .save_send_queue_event(&self.room_id, transaction_id.clone(), serializable) + .save_send_queue_request(&self.room_id, transaction_id.clone(), serializable) .await?; Ok(transaction_id) } - /// Peeks the next event to be sent, marking it as being sent. + /// Peeks the next request to be sent, marking it as being sent. /// /// It is required to call [`Self::mark_as_sent`] after it's been /// effectively sent. - async fn peek_next_to_send(&self) -> Result, RoomSendQueueStorageError> { + async fn peek_next_to_send(&self) -> Result, RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; - let queued_events = self.client()?.store().load_send_queue_events(&self.room_id).await?; + let queued_requests = + self.client()?.store().load_send_queue_requests(&self.room_id).await?; - if let Some(event) = queued_events.iter().find(|queued| !queued.is_wedged()) { - being_sent.insert(event.transaction_id.clone()); + if let Some(request) = queued_requests.iter().find(|queued| !queued.is_wedged()) { + being_sent.insert(request.transaction_id.clone()); - Ok(Some(event.clone())) + Ok(Some(request.clone())) } else { Ok(None) } } - /// Marks an event popped with [`Self::peek_next_to_send`] and identified + /// Marks a request popped with [`Self::peek_next_to_send`] and identified /// with the given transaction id as not being sent anymore, so it can /// be removed from the queue later. async fn mark_as_not_being_sent(&self, transaction_id: &TransactionId) { self.being_sent.write().await.remove(transaction_id); } - /// Marks an event popped with [`Self::peek_next_to_send`] and identified + /// Marks a request popped with [`Self::peek_next_to_send`] and identified /// with the given transaction id as being wedged (and not being sent /// anymore), so it can be removed from the queue later. async fn mark_as_wedged( @@ -720,11 +739,11 @@ impl QueueStorage { Ok(self .client()? .store() - .update_send_queue_event_status(&self.room_id, transaction_id, Some(reason)) + .update_send_queue_request_status(&self.room_id, transaction_id, Some(reason)) .await?) } - /// Marks an event identified with the given transaction id as being now + /// Marks a request identified with the given transaction id as being now /// unwedged and adds it back to the queue. async fn mark_as_unwedged( &self, @@ -733,12 +752,12 @@ impl QueueStorage { Ok(self .client()? .store() - .update_send_queue_event_status(&self.room_id, transaction_id, None) + .update_send_queue_request_status(&self.room_id, transaction_id, None) .await?) } - /// Marks an event pushed with [`Self::push`] and identified with the given - /// transaction id as sent by removing it from the local queue. + /// Marks a request pushed with [`Self::push`] and identified with the given + /// transaction id as sent, by removing it from the local queue. async fn mark_as_sent( &self, transaction_id: &TransactionId, @@ -751,15 +770,15 @@ impl QueueStorage { let client = self.client()?; let store = client.store(); - // Update all dependent events. + // Update all dependent requests. store - .update_dependent_send_queue_event(&self.room_id, transaction_id, event_id.to_owned()) + .update_dependent_queued_request(&self.room_id, transaction_id, event_id.to_owned()) .await?; - let removed = store.remove_send_queue_event(&self.room_id, transaction_id).await?; + let removed = store.remove_send_queue_request(&self.room_id, transaction_id).await?; if !removed { - warn!(txn_id = %transaction_id, "event marked as sent was missing from storage"); + warn!(txn_id = %transaction_id, "request marked as sent was missing from storage"); } Ok(()) @@ -770,8 +789,8 @@ impl QueueStorage { /// /// Returns whether the given transaction has been effectively removed. If /// false, this either means that the transaction id was unrelated to - /// this queue, or that the event was sent before we cancelled it. - async fn cancel( + /// this queue, or that the request was sent before we cancelled it. + async fn cancel_event( &self, transaction_id: &TransactionId, ) -> Result { @@ -782,11 +801,11 @@ impl QueueStorage { // Save the intent to redact the event. self.client()? .store() - .save_dependent_send_queue_event( + .save_dependent_queued_request( &self.room_id, transaction_id, ChildTransactionId::new(), - DependentQueuedEventKind::Redact, + DependentQueuedRequestKind::RedactEvent, ) .await?; @@ -794,19 +813,18 @@ impl QueueStorage { } let removed = - self.client()?.store().remove_send_queue_event(&self.room_id, transaction_id).await?; + self.client()?.store().remove_send_queue_request(&self.room_id, transaction_id).await?; Ok(removed) } - /// Replace an event that has been sent with - /// [`Self::push`] with the given transaction id, before it's been actually - /// sent. + /// Replace an event that has been sent with [`Self::push`] with the given + /// transaction id, before it's been actually sent. /// /// Returns whether the given transaction has been effectively edited. If /// false, this either means that the transaction id was unrelated to - /// this queue, or that the event was sent before we edited it. - async fn replace( + /// this queue, or that the request was sent before we edited it. + async fn replace_event( &self, transaction_id: &TransactionId, serializable: SerializableEventContent, @@ -815,14 +833,14 @@ impl QueueStorage { let being_sent = self.being_sent.read().await; if being_sent.contains(transaction_id) { - // Save the intent to redact the event. + // Save the intent to edit the associated event. self.client()? .store() - .save_dependent_send_queue_event( + .save_dependent_queued_request( &self.room_id, transaction_id, ChildTransactionId::new(), - DependentQueuedEventKind::Edit { new_content: serializable }, + DependentQueuedRequestKind::EditEvent { new_content: serializable }, ) .await?; @@ -832,12 +850,13 @@ impl QueueStorage { let edited = self .client()? .store() - .update_send_queue_event(&self.room_id, transaction_id, serializable) + .update_send_queue_request(&self.room_id, transaction_id, serializable) .await?; Ok(edited) } + /// Reacts to the given local echo of an event. #[instrument(skip(self))] async fn react( &self, @@ -847,28 +866,28 @@ impl QueueStorage { let client = self.client()?; let store = client.store(); - let queued_events = store.load_send_queue_events(&self.room_id).await?; + let requests = store.load_send_queue_requests(&self.room_id).await?; - // If the event has been already sent, abort immediately. - if !queued_events.iter().any(|item| item.transaction_id == transaction_id) { + // If the target event has been already sent, abort immediately. + if !requests.iter().any(|item| item.transaction_id == transaction_id) { return Ok(None); } - // Record the dependent event. + // Record the dependent request. let reaction_txn_id = ChildTransactionId::new(); store - .save_dependent_send_queue_event( + .save_dependent_queued_request( &self.room_id, transaction_id, reaction_txn_id.clone(), - DependentQueuedEventKind::React { key }, + DependentQueuedRequestKind::ReactEvent { key }, ) .await?; Ok(Some(reaction_txn_id)) } - /// Returns a list of the local echoes, that is, all the events that we're + /// Returns a list of the local echoes, that is, all the requests that we're /// about to send but that haven't been sent yet (or are being sent). async fn local_echoes( &self, @@ -877,8 +896,8 @@ impl QueueStorage { let client = self.client()?; let store = client.store(); - let local_events = - store.load_send_queue_events(&self.room_id).await?.into_iter().map(|queued| { + let local_requests = + store.load_send_queue_requests(&self.room_id).await?.into_iter().map(|queued| { LocalEcho { transaction_id: queued.transaction_id.clone(), content: match queued.kind { @@ -894,45 +913,48 @@ impl QueueStorage { } }); - let local_reactions = store - .load_dependent_send_queue_events(&self.room_id) - .await? - .into_iter() - .filter_map(|dep| match dep.kind { - DependentQueuedEventKind::Edit { .. } | DependentQueuedEventKind::Redact => None, - DependentQueuedEventKind::React { key } => Some(LocalEcho { - transaction_id: dep.own_transaction_id.clone().into(), - content: LocalEchoContent::React { - key, - send_handle: SendReactionHandle { - room: room.clone(), - transaction_id: dep.own_transaction_id, + let local_reactions = + store.load_dependent_queued_requests(&self.room_id).await?.into_iter().filter_map( + |dep| match dep.kind { + DependentQueuedRequestKind::EditEvent { .. } + | DependentQueuedRequestKind::RedactEvent => { + // TODO: reflect local edits/redacts too? + None + } + DependentQueuedRequestKind::ReactEvent { key } => Some(LocalEcho { + transaction_id: dep.own_transaction_id.clone().into(), + content: LocalEchoContent::React { + key, + send_handle: SendReactionHandle { + room: room.clone(), + transaction_id: dep.own_transaction_id, + }, + applies_to: dep.parent_transaction_id, }, - applies_to: dep.parent_transaction_id, - }, - }), - }); + }), + }, + ); - Ok(local_events.chain(local_reactions).collect()) + Ok(local_requests.chain(local_reactions).collect()) } - /// Try to apply a single dependent event, whether it's local or remote. + /// Try to apply a single dependent request, whether it's local or remote. /// /// This swallows errors that would retrigger every time if we retried - /// applying the dependent event: invalid edit content, etc. + /// applying the dependent request: invalid edit content, etc. /// - /// Returns true if the dependent event has been sent (or should not be + /// Returns true if the dependent request has been sent (or should not be /// retried later). #[instrument(skip_all)] - async fn try_apply_single_dependent_event( + async fn try_apply_single_dependent_request( &self, client: &Client, - de: DependentQueuedEvent, + de: DependentQueuedRequest, ) -> Result { let store = client.store(); match de.kind { - DependentQueuedEventKind::Edit { new_content } => { + DependentQueuedRequestKind::EditEvent { new_content } => { if let Some(event_id) = de.event_id { // The parent event has been sent, so send an edit event. let room = client @@ -983,7 +1005,7 @@ impl QueueStorage { ); store - .save_send_queue_event( + .save_send_queue_request( &self.room_id, de.own_transaction_id.into(), serializable, @@ -991,10 +1013,9 @@ impl QueueStorage { .await .map_err(RoomSendQueueStorageError::StorageError)?; } else { - // The parent event is still local (sending must have failed); update the local - // echo. + // The parent event is still local; update the local echo. let edited = store - .update_send_queue_event( + .update_send_queue_request( &self.room_id, &de.parent_transaction_id, new_content, @@ -1008,7 +1029,7 @@ impl QueueStorage { } } - DependentQueuedEventKind::Redact => { + DependentQueuedRequestKind::RedactEvent => { if let Some(event_id) = de.event_id { // The parent event has been sent; send a redaction. let room = client @@ -1032,7 +1053,7 @@ impl QueueStorage { // The parent event is still local (sending must have failed); redact the local // echo. let removed = store - .remove_send_queue_event(&self.room_id, &de.parent_transaction_id) + .remove_send_queue_request(&self.room_id, &de.parent_transaction_id) .await .map_err(RoomSendQueueStorageError::StorageError)?; @@ -1042,7 +1063,7 @@ impl QueueStorage { } } - DependentQueuedEventKind::React { key } => { + DependentQueuedRequestKind::ReactEvent { key } => { if let Some(event_id) = de.event_id { // Queue the reaction event in the send queue 🧠. let react_event = @@ -1054,7 +1075,7 @@ impl QueueStorage { ); store - .save_send_queue_event( + .save_send_queue_request( &self.room_id, de.own_transaction_id.into(), serializable, @@ -1072,78 +1093,78 @@ impl QueueStorage { } #[instrument(skip(self))] - async fn apply_dependent_events(&self) -> Result<(), RoomSendQueueError> { + async fn apply_dependent_requests(&self) -> Result<(), RoomSendQueueError> { // Keep the lock until we're done touching the storage. let _being_sent = self.being_sent.read().await; let client = self.client()?; let store = client.store(); - let dependent_events = store - .load_dependent_send_queue_events(&self.room_id) + let dependent_requests = store + .load_dependent_queued_requests(&self.room_id) .await .map_err(RoomSendQueueStorageError::StorageError)?; - let num_initial_dependent_events = dependent_events.len(); - if num_initial_dependent_events == 0 { + let num_initial_dependent_requests = dependent_requests.len(); + if num_initial_dependent_requests == 0 { // Returning early here avoids a bit of useless logging. return Ok(()); } - let canonicalized_dependent_events = canonicalize_dependent_events(&dependent_events); + let canonicalized_dependent_requests = canonicalize_dependent_requests(&dependent_requests); // Get rid of the all non-canonical dependent events. - for original in &dependent_events { - if !canonicalized_dependent_events + for original in &dependent_requests { + if !canonicalized_dependent_requests .iter() .any(|canonical| canonical.own_transaction_id == original.own_transaction_id) { store - .remove_dependent_send_queue_event(&self.room_id, &original.own_transaction_id) + .remove_dependent_queued_request(&self.room_id, &original.own_transaction_id) .await .map_err(RoomSendQueueStorageError::StorageError)?; } } - let mut num_dependent_events = canonicalized_dependent_events.len(); + let mut num_dependent_requests = canonicalized_dependent_requests.len(); debug!( - num_dependent_events, - num_initial_dependent_events, "starting handling of dependent events" + num_dependent_requests, + num_initial_dependent_requests, "starting handling of dependent requests" ); - for dependent in canonicalized_dependent_events { + for dependent in canonicalized_dependent_requests { let dependent_id = dependent.own_transaction_id.clone(); - match self.try_apply_single_dependent_event(&client, dependent).await { + match self.try_apply_single_dependent_request(&client, dependent).await { Ok(should_remove) => { if should_remove { - // The dependent event has been successfully applied, forget about it. + // The dependent request has been successfully applied, forget about it. store - .remove_dependent_send_queue_event(&self.room_id, &dependent_id) + .remove_dependent_queued_request(&self.room_id, &dependent_id) .await .map_err(RoomSendQueueStorageError::StorageError)?; - num_dependent_events -= 1; + num_dependent_requests -= 1; } } Err(err) => { - warn!("error when applying single dependent event: {err}"); + warn!("error when applying single dependent request: {err}"); } } } debug!( - leftover_dependent_events = num_dependent_events, - "stopped handling dependent events" + leftover_dependent_requests = num_dependent_requests, + "stopped handling dependent request" ); Ok(()) } - /// Remove a single dependent event from storage. - async fn remove_dependent_send_queue_event( + /// Remove a single dependent request from storage. + async fn remove_dependent_send_queue_request( &self, dependent_event_id: &ChildTransactionId, ) -> Result { @@ -1153,7 +1174,7 @@ impl QueueStorage { Ok(self .client()? .store() - .remove_dependent_send_queue_event(&self.room_id, dependent_event_id) + .remove_dependent_queued_request(&self.room_id, dependent_event_id) .await?) } } @@ -1184,10 +1205,11 @@ pub enum LocalEchoContent { }, } -/// An event that has been locally queued for sending, but hasn't been sent yet. +/// A local representation for a request that hasn't been sent yet to the user's +/// homeserver. #[derive(Clone, Debug)] pub struct LocalEcho { - /// Transaction id used to identify this event. + /// Transaction id used to identify the associated request. pub transaction_id: OwnedTransactionId, /// The content for the local echo. pub content: LocalEchoContent, @@ -1259,8 +1281,9 @@ pub enum RoomSendQueueError { #[error("the room isn't in the joined state")] RoomNotJoined, - /// The room is missing from the client. This could happen if the client is - /// shutting down. + /// The room is missing from the client. + /// + /// This happens only whenever the client is shutting down. #[error("the room is now missing from the client")] RoomDisappeared, @@ -1286,6 +1309,7 @@ pub enum RoomSendQueueStorageError { } /// A handle to manipulate an event that was scheduled to be sent to a room. +// TODO (bnjbvr): consider renaming `SendEventHandle`, unless we can reuse it for medias too. #[derive(Clone, Debug)] pub struct SendHandle { room: RoomSendQueue, @@ -1301,7 +1325,7 @@ impl SendHandle { pub async fn abort(&self) -> Result { trace!("received an abort request"); - if self.room.inner.queue.cancel(&self.transaction_id).await? { + if self.room.inner.queue.cancel_event(&self.transaction_id).await? { trace!("successful abort"); // Propagate a cancelled update too. @@ -1330,7 +1354,7 @@ impl SendHandle { let serializable = SerializableEventContent::from_raw(new_content, event_type); - if self.room.inner.queue.replace(&self.transaction_id, serializable.clone()).await? { + if self.room.inner.queue.replace_event(&self.transaction_id, serializable.clone()).await? { trace!("successful edit"); // Wake up the queue, in case the room was asleep before the edit. @@ -1423,7 +1447,7 @@ impl SendReactionHandle { /// Will return true if the reaction could be aborted, false if it's been /// sent (and there's no matching local echo anymore). pub async fn abort(&self) -> Result { - if self.room.inner.queue.remove_dependent_send_queue_event(&self.transaction_id).await? { + if self.room.inner.queue.remove_dependent_send_queue_request(&self.transaction_id).await? { // Simple case: the reaction was found in the dependent event list. // Propagate a cancelled update too. @@ -1450,27 +1474,29 @@ impl SendReactionHandle { } } -/// From a given source of [`DependentQueuedEvent`], return only the most +/// From a given source of [`DependentQueuedRequest`], return only the most /// meaningful, i.e. the ones that wouldn't be overridden after applying the /// others. -fn canonicalize_dependent_events(dependent: &[DependentQueuedEvent]) -> Vec { - let mut by_event_id = HashMap::>::new(); +fn canonicalize_dependent_requests( + dependent: &[DependentQueuedRequest], +) -> Vec { + let mut by_event_id = HashMap::>::new(); for d in dependent { let prevs = by_event_id.entry(d.parent_transaction_id.clone()).or_default(); - if prevs.iter().any(|prev| matches!(prev.kind, DependentQueuedEventKind::Redact)) { - // The event has already been flagged for redaction, don't consider the other - // dependent events. + if prevs.iter().any(|prev| matches!(prev.kind, DependentQueuedRequestKind::RedactEvent)) { + // The parent event has already been flagged for redaction, don't consider the + // other dependent events. continue; } match &d.kind { - DependentQueuedEventKind::Edit { .. } => { + DependentQueuedRequestKind::EditEvent { .. } => { // Replace any previous edit with this one. if let Some(prev_edit) = prevs .iter_mut() - .find(|prev| matches!(prev.kind, DependentQueuedEventKind::Edit { .. })) + .find(|prev| matches!(prev.kind, DependentQueuedRequestKind::EditEvent { .. })) { *prev_edit = d; } else { @@ -1478,11 +1504,11 @@ fn canonicalize_dependent_events(dependent: &[DependentQueuedEvent]) -> Vec { + DependentQueuedRequestKind::ReactEvent { .. } => { prevs.push(d); } - DependentQueuedEventKind::Redact => { + DependentQueuedRequestKind::RedactEvent => { // Remove every other dependent action. prevs.clear(); prevs.push(d); @@ -1502,7 +1528,7 @@ mod tests { use assert_matches2::{assert_let, assert_matches}; use matrix_sdk_base::store::{ - ChildTransactionId, DependentQueuedEvent, DependentQueuedEventKind, + ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, SerializableEventContent, }; use matrix_sdk_test::{async_test, JoinedRoomBuilder, SyncResponseBuilder}; @@ -1511,7 +1537,7 @@ mod tests { room_id, TransactionId, }; - use super::canonicalize_dependent_events; + use super::canonicalize_dependent_requests; use crate::{client::WeakClient, test_utils::logged_in_client}; #[async_test] @@ -1564,10 +1590,10 @@ mod tests { // Smoke test: canonicalizing a single dependent event returns it. let txn = TransactionId::new(); - let edit = DependentQueuedEvent { + let edit = DependentQueuedRequest { own_transaction_id: ChildTransactionId::new(), parent_transaction_id: txn.clone(), - kind: DependentQueuedEventKind::Edit { + kind: DependentQueuedRequestKind::EditEvent { new_content: SerializableEventContent::new( &RoomMessageEventContent::text_plain("edit").into(), ) @@ -1575,10 +1601,10 @@ mod tests { }, event_id: None, }; - let res = canonicalize_dependent_events(&[edit]); + let res = canonicalize_dependent_requests(&[edit]); assert_eq!(res.len(), 1); - assert_matches!(&res[0].kind, DependentQueuedEventKind::Edit { .. }); + assert_matches!(&res[0].kind, DependentQueuedRequestKind::EditEvent { .. }); assert_eq!(res[0].parent_transaction_id, txn); assert!(res[0].event_id.is_none()); } @@ -1589,17 +1615,17 @@ mod tests { let txn = TransactionId::new(); let mut inputs = Vec::with_capacity(100); - let redact = DependentQueuedEvent { + let redact = DependentQueuedRequest { own_transaction_id: ChildTransactionId::new(), parent_transaction_id: txn.clone(), - kind: DependentQueuedEventKind::Redact, + kind: DependentQueuedRequestKind::RedactEvent, event_id: None, }; - let edit = DependentQueuedEvent { + let edit = DependentQueuedRequest { own_transaction_id: ChildTransactionId::new(), parent_transaction_id: txn.clone(), - kind: DependentQueuedEventKind::Edit { + kind: DependentQueuedRequestKind::EditEvent { new_content: SerializableEventContent::new( &RoomMessageEventContent::text_plain("edit").into(), ) @@ -1622,10 +1648,10 @@ mod tests { inputs.push(edit); } - let res = canonicalize_dependent_events(&inputs); + let res = canonicalize_dependent_requests(&inputs); assert_eq!(res.len(), 1); - assert_matches!(&res[0].kind, DependentQueuedEventKind::Redact); + assert_matches!(&res[0].kind, DependentQueuedRequestKind::RedactEvent); assert_eq!(res[0].parent_transaction_id, txn); } @@ -1635,10 +1661,10 @@ mod tests { // The latest edit of a list is always preferred. let inputs = (0..10) - .map(|i| DependentQueuedEvent { + .map(|i| DependentQueuedRequest { own_transaction_id: ChildTransactionId::new(), parent_transaction_id: parent_txn.clone(), - kind: DependentQueuedEventKind::Edit { + kind: DependentQueuedRequestKind::EditEvent { new_content: SerializableEventContent::new( &RoomMessageEventContent::text_plain(format!("edit{i}")).into(), ) @@ -1650,10 +1676,10 @@ mod tests { let txn = inputs[9].parent_transaction_id.clone(); - let res = canonicalize_dependent_events(&inputs); + let res = canonicalize_dependent_requests(&inputs); assert_eq!(res.len(), 1); - assert_let!(DependentQueuedEventKind::Edit { new_content } = &res[0].kind); + assert_let!(DependentQueuedRequestKind::EditEvent { new_content } = &res[0].kind); assert_let!( AnyMessageLikeEventContent::RoomMessage(msg) = new_content.deserialize().unwrap() ); @@ -1671,16 +1697,16 @@ mod tests { let inputs = vec![ // This one pertains to txn1. - DependentQueuedEvent { + DependentQueuedRequest { own_transaction_id: child1.clone(), - kind: DependentQueuedEventKind::Redact, + kind: DependentQueuedRequestKind::RedactEvent, parent_transaction_id: txn1.clone(), event_id: None, }, // This one pertains to txn2. - DependentQueuedEvent { + DependentQueuedRequest { own_transaction_id: child2, - kind: DependentQueuedEventKind::Edit { + kind: DependentQueuedRequestKind::EditEvent { new_content: SerializableEventContent::new( &RoomMessageEventContent::text_plain("edit").into(), ) @@ -1691,7 +1717,7 @@ mod tests { }, ]; - let res = canonicalize_dependent_events(&inputs); + let res = canonicalize_dependent_requests(&inputs); // The canonicalization shouldn't depend per event id. assert_eq!(res.len(), 2); @@ -1699,10 +1725,10 @@ mod tests { for dependent in res { if dependent.own_transaction_id == child1 { assert_eq!(dependent.parent_transaction_id, txn1); - assert_matches!(dependent.kind, DependentQueuedEventKind::Redact); + assert_matches!(dependent.kind, DependentQueuedRequestKind::RedactEvent); } else { assert_eq!(dependent.parent_transaction_id, txn2); - assert_matches!(dependent.kind, DependentQueuedEventKind::Edit { .. }); + assert_matches!(dependent.kind, DependentQueuedRequestKind::EditEvent { .. }); } } } @@ -1713,17 +1739,17 @@ mod tests { let txn = TransactionId::new(); let react_id = ChildTransactionId::new(); - let react = DependentQueuedEvent { + let react = DependentQueuedRequest { own_transaction_id: react_id.clone(), - kind: DependentQueuedEventKind::React { key: "🧠".to_owned() }, + kind: DependentQueuedRequestKind::ReactEvent { key: "🧠".to_owned() }, parent_transaction_id: txn.clone(), event_id: None, }; let edit_id = ChildTransactionId::new(); - let edit = DependentQueuedEvent { + let edit = DependentQueuedRequest { own_transaction_id: edit_id.clone(), - kind: DependentQueuedEventKind::Edit { + kind: DependentQueuedRequestKind::EditEvent { new_content: SerializableEventContent::new( &RoomMessageEventContent::text_plain("edit").into(), ) @@ -1733,7 +1759,7 @@ mod tests { event_id: None, }; - let res = canonicalize_dependent_events(&[react, edit]); + let res = canonicalize_dependent_requests(&[react, edit]); assert_eq!(res.len(), 2); assert_eq!(res[0].own_transaction_id, edit_id); diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index c88ea5c6934..55455dd0ff9 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1799,7 +1799,7 @@ async fn test_reloading_rooms_with_unsent_events() { .unwrap(); set_client_session(&client).await; - client.send_queue().respawn_tasks_for_rooms_with_unsent_events().await; + client.send_queue().respawn_tasks_for_rooms_with_unsent_requests().await; // Let the sending queues process events. sleep(Duration::from_secs(1)).await; From 5d828d234efefb2dd42787148476ff6f04d2eb9c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 28 Oct 2024 16:09:41 +0100 Subject: [PATCH 403/979] feat(ring buffer): implement `RingBuffer::iter_mut()` --- crates/matrix-sdk-common/src/ring_buffer.rs | 27 ++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-common/src/ring_buffer.rs b/crates/matrix-sdk-common/src/ring_buffer.rs index 8b3bbcff15a..e92e7f5bc38 100644 --- a/crates/matrix-sdk-common/src/ring_buffer.rs +++ b/crates/matrix-sdk-common/src/ring_buffer.rs @@ -14,7 +14,7 @@ use std::{ collections::{ - vec_deque::{Drain, Iter}, + vec_deque::{Drain, Iter, IterMut}, VecDeque, }, num::NonZeroUsize, @@ -88,6 +88,13 @@ impl RingBuffer { self.inner.iter() } + /// Returns a mutable iterator that provides elements in front-to-back + /// order, i.e. the same order you would get if you repeatedly called + /// pop(). + pub fn iter_mut(&mut self) -> IterMut<'_, T> { + self.inner.iter_mut() + } + /// Returns an iterator that drains its items. pub fn drain(&mut self, range: R) -> Drain<'_, T> where @@ -221,6 +228,24 @@ mod tests { assert_eq!(ring_buffer.remove(10), None); } + #[test] + fn test_iter() { + let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap()); + + ring_buffer.push(1); + ring_buffer.push(2); + ring_buffer.push(3); + + let as_vec = ring_buffer.iter().copied().collect::>(); + assert_eq!(as_vec, [1, 2, 3]); + + let first_entry = ring_buffer.iter_mut().next().unwrap(); + *first_entry = 42; + + let as_vec = ring_buffer.iter().copied().collect::>(); + assert_eq!(as_vec, [42, 2, 3]); + } + #[test] fn test_drain() { let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap()); From 50473ba1a811529546972081e4b6d1ec1042c9e7 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 28 Oct 2024 16:10:28 +0100 Subject: [PATCH 404/979] chore(ring buffer): prefix all tests with `test_` in this file --- crates/matrix-sdk-common/src/ring_buffer.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-common/src/ring_buffer.rs b/crates/matrix-sdk-common/src/ring_buffer.rs index e92e7f5bc38..89aed5bec62 100644 --- a/crates/matrix-sdk-common/src/ring_buffer.rs +++ b/crates/matrix-sdk-common/src/ring_buffer.rs @@ -268,14 +268,14 @@ mod tests { } #[test] - fn clear_on_empty_buffer_is_a_noop() { + fn test_clear_on_empty_buffer_is_a_noop() { let mut ring_buffer: RingBuffer = RingBuffer::new(NonZeroUsize::new(3).unwrap()); ring_buffer.clear(); assert_eq!(ring_buffer.len(), 0); } #[test] - fn clear_removes_all_items() { + fn test_clear_removes_all_items() { // Given a RingBuffer that has been used let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap()); ring_buffer.push(4); @@ -295,7 +295,7 @@ mod tests { } #[test] - fn clear_does_not_affect_capacity() { + fn test_clear_does_not_affect_capacity() { // Given a RingBuffer that has been used let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap()); ring_buffer.push(4); @@ -313,7 +313,7 @@ mod tests { } #[test] - fn capacity_is_what_we_passed_to_new() { + fn test_capacity_is_what_we_passed_to_new() { // Given a RingBuffer let ring_buffer = RingBuffer::::new(NonZeroUsize::new(13).unwrap()); // When I ask for its capacity I get what I provided at the start @@ -321,7 +321,7 @@ mod tests { } #[test] - fn capacity_is_not_affected_by_overflowing() { + fn test_capacity_is_not_affected_by_overflowing() { // Given a RingBuffer that has been used let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap()); ring_buffer.push(4); @@ -343,7 +343,7 @@ mod tests { } #[test] - fn roundtrip_serialization() { + fn test_roundtrip_serialization() { // Given a RingBuffer let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap()); ring_buffer.push("1".to_owned()); @@ -363,7 +363,7 @@ mod tests { } #[test] - fn extending_an_empty_ringbuffer_adds_the_items() { + fn test_extending_an_empty_ringbuffer_adds_the_items() { // Given a RingBuffer let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap()); @@ -375,7 +375,7 @@ mod tests { } #[test] - fn extend_adds_items_to_the_end() { + fn test_extend_adds_items_to_the_end() { // Given a RingBuffer with something in it let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap()); ring_buffer.push("1".to_owned()); @@ -392,7 +392,7 @@ mod tests { } #[test] - fn extend_does_not_overflow_max_length() { + fn test_extend_does_not_overflow_max_length() { // Given a RingBuffer with something in it let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap()); ring_buffer.push("1".to_owned()); @@ -415,7 +415,7 @@ mod tests { } #[test] - fn extending_a_full_ringbuffer_preserves_max_length() { + fn test_extending_a_full_ringbuffer_preserves_max_length() { // Given a full RingBuffer with something in it let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(2).unwrap()); ring_buffer.push("1".to_owned()); From be88e0ad69d53d3d8327f2c32f04ff0a59f805c2 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 28 Oct 2024 16:38:00 +0100 Subject: [PATCH 405/979] feat(event cache store): Implement renaming media keys --- .../event_cache_store/integration_tests.rs | 46 +++++++++++++++++++ .../src/event_cache_store/memory_store.rs | 21 ++++++++- .../src/event_cache_store/traits.rs | 30 ++++++++++++ .../src/event_cache_store.rs | 22 +++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs index 2a0cc30faf0..ccb92f05005 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs @@ -31,6 +31,9 @@ use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSettings}; pub trait EventCacheStoreIntegrationTests { /// Test media content storage. async fn test_media_content(&self); + + /// Test replacing a MXID. + async fn test_replace_media_key(&self); } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -139,6 +142,42 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { "other media was removed" ); } + + async fn test_replace_media_key(&self) { + let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d"); + let req = + MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File }; + + let content = "hello".as_bytes().to_owned(); + + // Media isn't present in the cache. + assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found"); + + // Add the media. + self.add_media_content(&req, content.clone()).await.expect("adding media failed"); + + // Sanity-check: media is found after adding it. + assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello"); + + // Replacing a media request works. + let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d"); + let new_req = MediaRequest { + source: MediaSource::Plain(new_uri.to_owned()), + format: MediaFormat::File, + }; + self.replace_media_key(&req, &new_req) + .await + .expect("replacing the media request key failed"); + + // Finding with the previous request doesn't work anymore. + assert!( + self.get_media_content(&req).await.unwrap().is_none(), + "unexpected media found with the old key" + ); + + // Finding with the new request does work. + assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello"); + } } /// Macro building to allow your `EventCacheStore` implementation to run the @@ -184,6 +223,13 @@ macro_rules! event_cache_store_integration_tests { get_event_cache_store().await.unwrap().into_event_cache_store(); event_cache_store.test_media_content().await; } + + #[async_test] + async fn test_replace_media_key() { + let event_cache_store = + get_event_cache_store().await.unwrap().into_event_cache_store(); + event_cache_store.test_replace_media_key().await; + } } }; } diff --git a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs index 381c52cbc19..15fdb23708e 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs @@ -60,18 +60,35 @@ impl EventCacheStore for MemoryStore { Ok(()) } + async fn replace_media_key( + &self, + from: &MediaRequest, + to: &MediaRequest, + ) -> Result<(), Self::Error> { + let expected_key = from.unique_key(); + + let mut medias = self.media.write().unwrap(); + if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) { + *mxc = to.uri().to_owned(); + *key = to.unique_key(); + } + + Ok(()) + } + async fn get_media_content(&self, request: &MediaRequest) -> Result>> { - let media = self.media.read().unwrap(); let expected_key = request.unique_key(); + let media = self.media.read().unwrap(); Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| { (media_key == &expected_key).then(|| media_content.to_owned()) })) } async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { - let mut media = self.media.write().unwrap(); let expected_key = request.unique_key(); + + let mut media = self.media.write().unwrap(); let Some(index) = media .iter() .position(|(_media_uri, media_key, _media_content)| media_key == &expected_key) diff --git a/crates/matrix-sdk-base/src/event_cache_store/traits.rs b/crates/matrix-sdk-base/src/event_cache_store/traits.rs index 6c56166177b..08691b06729 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/traits.rs @@ -42,6 +42,28 @@ pub trait EventCacheStore: AsyncTraitDeps { content: Vec, ) -> Result<(), Self::Error>; + /// Replaces the given media's content key with another one. + /// + /// This should be used whenever a temporary (local) MXID has been used, and + /// it must now be replaced with its actual remote counterpart (after + /// uploading some content, or creating an empty MXC URI). + /// + /// ⚠ No check is performed to ensure that the media formats are consistent, + /// i.e. it's possible to update with a thumbnail key a media that was + /// keyed as a file before. The caller is responsible of ensuring that + /// the replacement makes sense, according to their use case. + /// + /// # Arguments + /// + /// * `from` - The previous `MediaRequest` of the file. + /// + /// * `to` - The new `MediaRequest` of the file. + async fn replace_media_key( + &self, + from: &MediaRequest, + to: &MediaRequest, + ) -> Result<(), Self::Error>; + /// Get a media file's content out of the media store. /// /// # Arguments @@ -91,6 +113,14 @@ impl EventCacheStore for EraseEventCacheStoreError { self.0.add_media_content(request, content).await.map_err(Into::into) } + async fn replace_media_key( + &self, + from: &MediaRequest, + to: &MediaRequest, + ) -> Result<(), Self::Error> { + self.0.replace_media_key(from, to).await.map_err(Into::into) + } + async fn get_media_content( &self, request: &MediaRequest, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 51afb9e5deb..5a2bf2462e8 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -155,6 +155,28 @@ impl EventCacheStore for SqliteEventCacheStore { Ok(()) } + async fn replace_media_key( + &self, + from: &MediaRequest, + to: &MediaRequest, + ) -> Result<(), Self::Error> { + let prev_uri = self.encode_key(keys::MEDIA, from.source.unique_key()); + let prev_format = self.encode_key(keys::MEDIA, from.format.unique_key()); + + let new_uri = self.encode_key(keys::MEDIA, to.source.unique_key()); + let new_format = self.encode_key(keys::MEDIA, to.format.unique_key()); + + let conn = self.acquire().await?; + conn.execute( + r#"UPDATE media SET uri = ?, format = ?, last_access = CAST(strftime('%s') as INT) + WHERE uri = ? AND format = ?"#, + (new_uri, new_format, prev_uri, prev_format), + ) + .await?; + + Ok(()) + } + async fn get_media_content(&self, request: &MediaRequest) -> Result>> { let uri = self.encode_key(keys::MEDIA, request.source.unique_key()); let format = self.encode_key(keys::MEDIA, request.format.unique_key()); From 5158b392777efa5fa8c83c045ee82cfb6737099a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 27 Oct 2024 14:23:40 +0100 Subject: [PATCH 406/979] refactor!: Upgrade Ruma to 0.11.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- Cargo.lock | 45 +++++++++++------- Cargo.toml | 4 +- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 11 +++-- crates/matrix-sdk-base/src/client.rs | 47 ++++++++++++++----- crates/matrix-sdk-crypto/src/backups/mod.rs | 6 +-- .../src/gossiping/machine.rs | 4 +- crates/matrix-sdk-crypto/src/machine/mod.rs | 6 +-- .../src/machine/test_helpers.rs | 10 ++-- .../src/machine/tests/mod.rs | 8 ++-- crates/matrix-sdk-crypto/src/olm/account.rs | 47 +++++++++---------- .../src/session_manager/group_sessions/mod.rs | 4 +- .../src/session_manager/sessions.rs | 8 ++-- .../src/types/one_time_keys.rs | 26 +++++----- .../src/notification_settings/command.rs | 19 ++++---- .../src/notification_settings/mod.rs | 26 ++++------ .../notification_settings/rule_commands.rs | 45 +++++------------- .../src/notification_settings/rules.rs | 6 +-- crates/matrix-sdk/src/room/mod.rs | 4 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 6 ++- .../integration/encryption/verification.rs | 8 ++-- 20 files changed, 178 insertions(+), 162 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be9c9a99775..f3c3d638016 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4574,8 +4574,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d719b9e1ce5b34a1e0b6e2ba4707f7923ce7fb3474881d771466456d68f3e485" dependencies = [ "assign", "js_int", @@ -4591,8 +4592,9 @@ dependencies = [ [[package]] name = "ruma-client-api" -version = "0.18.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325e054db8d5545c00767d9868356d61e63f2c6cb8b54768346d66696ea4ad48" dependencies = [ "as_variant", "assign", @@ -4614,8 +4616,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4717eb215175df5087fdd79da2c9a4198c9a50fe747db0afbc23c8ac18a25da8" dependencies = [ "as_variant", "base64 0.22.1", @@ -4646,8 +4649,9 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.28.1" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969cfed397d22f0338d99457409aa9c9dd4def4a5ce8d6567e914a320bad30da" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4671,8 +4675,9 @@ dependencies = [ [[package]] name = "ruma-federation-api" -version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5a09ac22b3352bf7a350514dc9a87e1b56aba04c326ac9ce142740f7218afa" dependencies = [ "http", "js_int", @@ -4685,8 +4690,9 @@ dependencies = [ [[package]] name = "ruma-html" -version = "0.2.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7571886b6df90a4ed72e7481a5a39cc2a5b3a4e956e9366ad798e4e2e9fe8005" dependencies = [ "as_variant", "html5ever", @@ -4697,8 +4703,9 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" -version = "0.9.5" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7f9b534a65698d7db3c08d94bf91de0046fe6c7893a7b360502f65e7011ac4" dependencies = [ "js_int", "thiserror", @@ -4706,8 +4713,9 @@ dependencies = [ [[package]] name = "ruma-macros" -version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d57d3cb20e8e758e8f7c5e408ce831d46758003b615100099852e468631934" dependencies = [ "cfg-if", "once_cell", @@ -4722,8 +4730,9 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" -version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfced466fbed6277f74ac3887eeb96c185a09f4323dc3c39bcea04870430fe9a" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index d648226e1fc..8ad1eacd0f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ once_cell = "1.16.0" pin-project-lite = "0.2.9" rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } -ruma = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a", features = [ +ruma = { version = "0.11.0", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -59,7 +59,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21d "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" } +ruma-common = "0.14.0" serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index da12075bfe4..6403379ac6c 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -42,7 +42,8 @@ use ruma::{ }, serde::Raw, to_device::DeviceIdOrAllDevices, - DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId, + DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId, + UserId, }; use serde::{Deserialize, Serialize}; use serde_json::{value::RawValue, Value}; @@ -528,11 +529,11 @@ impl OlmMachine { ) -> Result { let to_device: ToDevice = serde_json::from_str(&events)?; let device_changes: RumaDeviceLists = device_changes.into(); - let key_counts: BTreeMap = key_counts + let key_counts: BTreeMap = key_counts .into_iter() .map(|(k, v)| { ( - DeviceKeyAlgorithm::from(k), + OneTimeKeyAlgorithm::from(k), v.clamp(0, i32::MAX) .try_into() .expect("Couldn't convert key counts into an UInt"), @@ -540,8 +541,8 @@ impl OlmMachine { }) .collect(); - let unused_fallback_keys: Option> = - unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect()); + let unused_fallback_keys: Option> = + unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect()); let (to_device_events, room_key_infos) = self.runtime.block_on( self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges { diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index d7ff49ea7f4..36485c13e26 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -43,6 +43,7 @@ use ruma::{ api::client as api, events::{ ignored_user_list::IgnoredUserListEvent, + marked_unread::MarkedUnreadEventContent, push_rules::{PushRulesEvent, PushRulesEventContent}, room::{ member::{MembershipState, RoomMemberEventContent, SyncRoomMemberEvent}, @@ -687,6 +688,25 @@ impl BaseClient { } } + // Helper to update the unread marker for stable and unstable prefixes. + fn on_unread_marker( + room_id: &RoomId, + content: &MarkedUnreadEventContent, + room_info: &mut RoomInfo, + room_info_notable_updates: &mut BTreeMap, + ) { + if room_info.base_info.is_marked_unread != content.unread { + // Notify the room list about a manual read marker change if the + // value's changed. + room_info_notable_updates + .entry(room_id.to_owned()) + .or_default() + .insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER); + } + + room_info.base_info.is_marked_unread = content.unread; + } + // Handle new events. for raw_event in events { match raw_event.deserialize() { @@ -696,19 +716,24 @@ impl BaseClient { match event { AnyRoomAccountDataEvent::MarkedUnread(event) => { on_room_info(room_id, changes, self, |room_info| { - if room_info.base_info.is_marked_unread != event.content.unread { - // Notify the room list about a manual read marker change if the - // value's changed. - room_info_notable_updates - .entry(room_id.to_owned()) - .or_default() - .insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER); - } - - room_info.base_info.is_marked_unread = event.content.unread; + on_unread_marker( + room_id, + &event.content, + room_info, + room_info_notable_updates, + ); + }); + } + AnyRoomAccountDataEvent::UnstableMarkedUnread(event) => { + on_room_info(room_id, changes, self, |room_info| { + on_unread_marker( + room_id, + &event.content.0, + room_info, + room_info_notable_updates, + ); }); } - AnyRoomAccountDataEvent::Tag(event) => { on_room_info(room_id, changes, self, |room_info| { room_info.base_info.handle_notable_tags(&event.content.tags); diff --git a/crates/matrix-sdk-crypto/src/backups/mod.rs b/crates/matrix-sdk-crypto/src/backups/mod.rs index a3fe22fc1d2..fc98dfa03d0 100644 --- a/crates/matrix-sdk-crypto/src/backups/mod.rs +++ b/crates/matrix-sdk-crypto/src/backups/mod.rs @@ -224,19 +224,19 @@ impl BackupMachine { if device_key_id.algorithm() == DeviceKeyAlgorithm::Ed25519 { // No need to check our own device here, we're doing that using // the check_own_device_signature(). - if device_key_id.device_id() == self.store.static_account().device_id { + if device_key_id.key_name() == self.store.static_account().device_id { continue; } let state = self .test_ed25519_device_signature( - device_key_id.device_id(), + device_key_id.key_name(), signatures, auth_data, ) .await?; - result.insert(device_key_id.device_id().to_owned(), state); + result.insert(device_key_id.key_name().to_owned(), state); // Abort the loop if we found a trusted and valid signature, // unless we should check all of them. diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 22a4e4e9cdd..9fa0bd9fdaf 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -34,7 +34,7 @@ use ruma::{ events::secret::request::{ RequestAction, SecretName, ToDeviceSecretRequestEvent as SecretRequestEvent, }, - DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomId, + DeviceId, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, }; use tracing::{debug, field::debug, info, instrument, trace, warn, Span}; @@ -178,7 +178,7 @@ impl GossipMachine { .map(|(key, value)| { let device_map = value .iter() - .map(|d| (d.to_owned(), DeviceKeyAlgorithm::SignedCurve25519)) + .map(|d| (d.to_owned(), OneTimeKeyAlgorithm::SignedCurve25519)) .collect(); (key.to_owned(), device_map) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 3624f4aa2cb..9439a83dea4 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -44,7 +44,7 @@ use ruma::{ AnyToDeviceEvent, MessageLikeEventContent, }, serde::{JsonObject, Raw}, - DeviceId, DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedDeviceKeyId, + DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId, }; use serde_json::{value::to_raw_value, Value}; @@ -2557,9 +2557,9 @@ pub struct EncryptionSyncChanges<'a> { /// sync response. pub changed_devices: &'a DeviceLists, /// The number of one time keys, as returned in the sync response. - pub one_time_keys_counts: &'a BTreeMap, + pub one_time_keys_counts: &'a BTreeMap, /// An optional list of fallback keys. - pub unused_fallback_keys: Option<&'a [DeviceKeyAlgorithm]>, + pub unused_fallback_keys: Option<&'a [OneTimeKeyAlgorithm]>, /// A next-batch token obtained from a to-device sync query. pub next_batch_token: Option, } diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index c1ace327dea..684b509436a 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -21,13 +21,15 @@ use as_variant::as_variant; use matrix_sdk_test::{ruma_response_from_json, test_json}; use ruma::{ api::client::keys::{ - claim_keys, get_keys, get_keys::v3::Response as KeysQueryResponse, upload_keys, + claim_keys, + get_keys::{self, v3::Response as KeysQueryResponse}, + upload_keys, }, device_id, encryption::OneTimeKey, events::dummy::ToDeviceDummyEventContent, serde::Raw, - user_id, DeviceId, OwnedDeviceKeyId, TransactionId, UserId, + user_id, DeviceId, OwnedOneTimeKeyId, TransactionId, UserId, }; use serde_json::json; @@ -37,7 +39,7 @@ use crate::{ }; /// These keys need to be periodically uploaded to the server. -type OneTimeKeys = BTreeMap>; +type OneTimeKeys = BTreeMap>; fn alice_device_id() -> &'static DeviceId { device_id!("JLAFKJWSCS") @@ -178,7 +180,7 @@ pub async fn create_session( machine: &OlmMachine, user_id: &UserId, device_id: &DeviceId, - key_id: OwnedDeviceKeyId, + key_id: OwnedOneTimeKeyId, one_time_key: Raw, ) { let one_time_keys = BTreeMap::from([( diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 21dabf2023a..0de4cbcc8ad 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -37,7 +37,7 @@ use ruma::{ room_id, serde::Raw, uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, - TransactionId, UserId, + OneTimeKeyAlgorithm, TransactionId, UserId, }; use serde_json::json; use vodozemac::{ @@ -174,7 +174,7 @@ async fn test_generate_one_time_keys() { .await .unwrap(); - response.one_time_key_counts.insert(DeviceKeyAlgorithm::SignedCurve25519, uint!(50)); + response.one_time_key_counts.insert(OneTimeKeyAlgorithm::SignedCurve25519, uint!(50)); machine.receive_keys_upload_response(&response).await.unwrap(); @@ -275,7 +275,7 @@ fn test_one_time_key_signing() { async fn test_keys_for_upload() { let machine = OlmMachine::new(user_id(), alice_device_id()).await; - let key_counts = BTreeMap::from([(DeviceKeyAlgorithm::SignedCurve25519, 49u8.into())]); + let key_counts = BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, 49u8.into())]); machine .receive_sync_changes(EncryptionSyncChanges { to_device_events: Vec::new(), @@ -327,7 +327,7 @@ async fn test_keys_for_upload() { let mut response = keys_upload_response(); response.one_time_key_counts.insert( - DeviceKeyAlgorithm::SignedCurve25519, + OneTimeKeyAlgorithm::SignedCurve25519, account.max_one_time_keys().try_into().unwrap(), ); diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 27fc53ab73d..ac01502566c 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -31,8 +31,9 @@ use ruma::{ }, events::AnyToDeviceEvent, serde::Raw, - DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, - OwnedDeviceKeyId, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UInt, UserId, + DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, + OneTimeKeyId, OwnedDeviceId, OwnedDeviceKeyId, OwnedOneTimeKeyId, OwnedUserId, RoomId, + SecondsSinceUnixEpoch, UInt, UserId, }; use serde::{de::Error, Deserialize, Serialize}; use serde_json::{ @@ -403,7 +404,7 @@ impl fmt::Debug for Account { } } -pub type OneTimeKeys = BTreeMap>; +pub type OneTimeKeys = BTreeMap>; pub type FallbackKeys = OneTimeKeys; impl Account { @@ -521,10 +522,10 @@ impl Account { pub(crate) fn update_key_counts( &mut self, - one_time_key_counts: &BTreeMap, - unused_fallback_keys: Option<&[DeviceKeyAlgorithm]>, + one_time_key_counts: &BTreeMap, + unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>, ) { - if let Some(count) = one_time_key_counts.get(&DeviceKeyAlgorithm::SignedCurve25519) { + if let Some(count) = one_time_key_counts.get(&OneTimeKeyAlgorithm::SignedCurve25519) { let count: u64 = (*count).into(); let old_count = self.uploaded_key_count(); @@ -827,9 +828,7 @@ impl Account { /// Sign and prepare one-time keys to be uploaded. /// /// If no one-time keys need to be uploaded, returns an empty `BTreeMap`. - pub fn signed_one_time_keys( - &self, - ) -> BTreeMap> { + pub fn signed_one_time_keys(&self) -> OneTimeKeys { let one_time_keys = self.one_time_keys(); if one_time_keys.is_empty() { @@ -842,9 +841,7 @@ impl Account { /// Sign and prepare fallback keys to be uploaded. /// /// If no fallback keys need to be uploaded returns an empty BTreeMap. - pub fn signed_fallback_keys( - &self, - ) -> BTreeMap> { + pub fn signed_fallback_keys(&self) -> FallbackKeys { let fallback_key = self.fallback_key(); if fallback_key.is_empty() { @@ -858,15 +855,15 @@ impl Account { &self, keys: HashMap, fallback: bool, - ) -> BTreeMap> { + ) -> OneTimeKeys { let mut keys_map = BTreeMap::new(); for (key_id, key) in keys { let signed_key = self.sign_key(key, fallback); keys_map.insert( - DeviceKeyId::from_parts( - DeviceKeyAlgorithm::SignedCurve25519, + OneTimeKeyId::from_parts( + OneTimeKeyAlgorithm::SignedCurve25519, key_id.to_base64().as_str().into(), ), signed_key.into_raw(), @@ -949,7 +946,7 @@ impl Account { )] fn find_pre_key_bundle( device: &DeviceData, - key_map: &BTreeMap>, + key_map: &OneTimeKeys, ) -> Result { let mut keys = key_map.iter(); @@ -994,7 +991,7 @@ impl Account { pub fn create_outbound_session( &self, device: &DeviceData, - key_map: &BTreeMap>, + key_map: &OneTimeKeys, our_device_keys: DeviceKeys, ) -> Result { let pre_key_bundle = Self::find_pre_key_bundle(device, key_map)?; @@ -1502,8 +1499,8 @@ mod tests { use anyhow::Result; use matrix_sdk_test::async_test; use ruma::{ - device_id, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, - UserId, + device_id, user_id, DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, + OneTimeKeyId, UserId, }; use serde_json::json; @@ -1532,12 +1529,12 @@ mod tests { let (_, second_one_time_keys, _) = account.keys_for_upload(); assert!(!second_one_time_keys.is_empty()); - let device_key_ids: BTreeSet<&DeviceKeyId> = + let one_time_key_ids: BTreeSet<&OneTimeKeyId> = one_time_keys.keys().map(Deref::deref).collect(); - let second_device_key_ids: BTreeSet<&DeviceKeyId> = + let second_one_time_key_ids: BTreeSet<&OneTimeKeyId> = second_one_time_keys.keys().map(Deref::deref).collect(); - assert_eq!(device_key_ids, second_device_key_ids); + assert_eq!(one_time_key_ids, second_one_time_key_ids); account.mark_keys_as_published(); account.update_uploaded_key_count(50); @@ -1552,10 +1549,10 @@ mod tests { let (_, fourth_one_time_keys, _) = account.keys_for_upload(); assert!(!fourth_one_time_keys.is_empty()); - let fourth_device_key_ids: BTreeSet<&DeviceKeyId> = + let fourth_one_time_key_ids: BTreeSet<&OneTimeKeyId> = fourth_one_time_keys.keys().map(Deref::deref).collect(); - assert_ne!(device_key_ids, fourth_device_key_ids); + assert_ne!(one_time_key_ids, fourth_one_time_key_ids); Ok(()) } @@ -1573,7 +1570,7 @@ mod tests { "We should not upload fallback keys until we know if the server supports them." ); - let one_time_keys = BTreeMap::from([(DeviceKeyAlgorithm::SignedCurve25519, 50u8.into())]); + let one_time_keys = BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, 50u8.into())]); // A `None` here means that the server doesn't support fallback keys, no // fallback key gets uploaded. diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs index 06c4bef261f..1fc60ffd9c9 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs @@ -789,7 +789,7 @@ mod tests { events::room::history_visibility::HistoryVisibility, room_id, to_device::DeviceIdOrAllDevices, - user_id, DeviceId, DeviceKeyAlgorithm, TransactionId, UInt, UserId, + user_id, DeviceId, OneTimeKeyAlgorithm, TransactionId, UInt, UserId, }; use serde_json::{json, Value}; @@ -1347,7 +1347,7 @@ mod tests { let machine = OlmMachine::new(alice_id(), alice_device_id()).await; assert_let!(Ok(Some((txn_id, device_keys_request))) = machine.upload_device_keys().await); let device_keys_response = upload_keys::v3::Response::new(BTreeMap::from([( - DeviceKeyAlgorithm::SignedCurve25519, + OneTimeKeyAlgorithm::SignedCurve25519, UInt::new(device_keys_request.one_time_keys.len() as u64).unwrap(), )])); machine.mark_request_as_sent(&txn_id, &device_keys_response).await.unwrap(); diff --git a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs index b6e7d850cb9..2ae2306c330 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs @@ -25,7 +25,7 @@ use ruma::{ }, assign, events::dummy::ToDeviceDummyEventContent, - DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedServerName, + DeviceId, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedOneTimeKeyId, OwnedServerName, OwnedTransactionId, OwnedUserId, SecondsSinceUnixEpoch, ServerName, TransactionId, UserId, }; use tracing::{debug, error, info, instrument, warn}; @@ -261,7 +261,7 @@ impl SessionManager { missing_session_devices_by_user .entry(user_id.to_owned()) .or_default() - .insert(device_id, DeviceKeyAlgorithm::SignedCurve25519); + .insert(device_id, OneTimeKeyAlgorithm::SignedCurve25519); } } else { failed_devices_by_user @@ -279,7 +279,7 @@ impl SessionManager { missing_session_devices_by_user.entry(user.to_owned()).or_default().extend( device_ids .iter() - .map(|device_id| (device_id.clone(), DeviceKeyAlgorithm::SignedCurve25519)), + .map(|device_id| (device_id.clone(), OneTimeKeyAlgorithm::SignedCurve25519)), ); } @@ -346,7 +346,7 @@ impl SessionManager { failed_servers: &BTreeSet, one_time_keys: &BTreeMap< &OwnedUserId, - BTreeMap<&OwnedDeviceId, BTreeSet<&OwnedDeviceKeyId>>, + BTreeMap<&OwnedDeviceId, BTreeSet<&OwnedOneTimeKeyId>>, >, ) { // First check that the response is for the request we were expecting. diff --git a/crates/matrix-sdk-crypto/src/types/one_time_keys.rs b/crates/matrix-sdk-crypto/src/types/one_time_keys.rs index bb6457b6577..33943787286 100644 --- a/crates/matrix-sdk-crypto/src/types/one_time_keys.rs +++ b/crates/matrix-sdk-crypto/src/types/one_time_keys.rs @@ -20,7 +20,7 @@ use std::collections::BTreeMap; -use ruma::{serde::Raw, DeviceKeyAlgorithm}; +use ruma::{serde::Raw, OneTimeKeyAlgorithm}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{value::to_raw_value, Value}; use vodozemac::Curve25519PublicKey; @@ -110,24 +110,28 @@ pub enum OneTimeKey { } impl OneTimeKey { - /// Deserialize the [`OneTimeKey`] from a [`DeviceKeyAlgorithm`] and a Raw + /// Deserialize the [`OneTimeKey`] from a [`OneTimeKeyAlgorithm`] and a Raw /// JSON value. pub fn deserialize( - algorithm: DeviceKeyAlgorithm, + algorithm: OneTimeKeyAlgorithm, key: &Raw, ) -> Result { match algorithm { - DeviceKeyAlgorithm::Curve25519 => { - let key: String = key.deserialize_as()?; - Ok(OneTimeKey::Key( - Curve25519PublicKey::from_base64(&key).map_err(serde::de::Error::custom)?, - )) - } - DeviceKeyAlgorithm::SignedCurve25519 => { + OneTimeKeyAlgorithm::SignedCurve25519 => { let key: SignedKey = key.deserialize_as()?; Ok(OneTimeKey::SignedKey(key)) } - _ => Err(serde::de::Error::custom(format!("Unsupported key algorithm {algorithm}"))), + _ => match algorithm.as_str() { + "curve25519" => { + let key: String = key.deserialize_as()?; + Ok(OneTimeKey::Key( + Curve25519PublicKey::from_base64(&key).map_err(serde::de::Error::custom)?, + )) + } + _ => { + Err(serde::de::Error::custom(format!("Unsupported key algorithm {algorithm}"))) + } + }, } } } diff --git a/crates/matrix-sdk/src/notification_settings/command.rs b/crates/matrix-sdk/src/notification_settings/command.rs index ed87c9bb5b2..f73ce30feb4 100644 --- a/crates/matrix-sdk/src/notification_settings/command.rs +++ b/crates/matrix-sdk/src/notification_settings/command.rs @@ -1,7 +1,6 @@ use std::fmt::Debug; use ruma::{ - api::client::push::RuleScope, push::{ Action, NewConditionalPushRule, NewPatternedPushRule, NewPushRule, NewSimplePushRule, PushCondition, RuleKind, Tweak, @@ -15,17 +14,17 @@ use crate::NotificationSettingsError; #[derive(Clone, Debug)] pub(crate) enum Command { /// Set a new `Room` push rule - SetRoomPushRule { scope: RuleScope, room_id: OwnedRoomId, notify: bool }, + SetRoomPushRule { room_id: OwnedRoomId, notify: bool }, /// Set a new `Override` push rule matching a `RoomId` - SetOverridePushRule { scope: RuleScope, rule_id: String, room_id: OwnedRoomId, notify: bool }, + SetOverridePushRule { rule_id: String, room_id: OwnedRoomId, notify: bool }, /// Set a new push rule for a keyword. - SetKeywordPushRule { scope: RuleScope, keyword: String }, + SetKeywordPushRule { keyword: String }, /// Set whether a push rule is enabled - SetPushRuleEnabled { scope: RuleScope, kind: RuleKind, rule_id: String, enabled: bool }, + SetPushRuleEnabled { kind: RuleKind, rule_id: String, enabled: bool }, /// Delete a push rule - DeletePushRule { scope: RuleScope, kind: RuleKind, rule_id: String }, + DeletePushRule { kind: RuleKind, rule_id: String }, /// Set a list of actions - SetPushRuleActions { scope: RuleScope, kind: RuleKind, rule_id: String, actions: Vec }, + SetPushRuleActions { kind: RuleKind, rule_id: String, actions: Vec }, } fn get_notify_actions(notify: bool) -> Vec { @@ -40,13 +39,13 @@ impl Command { /// Tries to create a push rule corresponding to this command pub(crate) fn to_push_rule(&self) -> Result { match self { - Self::SetRoomPushRule { scope: _, room_id, notify } => { + Self::SetRoomPushRule { room_id, notify } => { // `Room` push rule for this `room_id` let new_rule = NewSimplePushRule::new(room_id.clone(), get_notify_actions(*notify)); Ok(NewPushRule::Room(new_rule)) } - Self::SetOverridePushRule { scope: _, rule_id, room_id, notify } => { + Self::SetOverridePushRule { rule_id, room_id, notify } => { // `Override` push rule matching this `room_id` let new_rule = NewConditionalPushRule::new( rule_id.clone(), @@ -59,7 +58,7 @@ impl Command { Ok(NewPushRule::Override(new_rule)) } - Self::SetKeywordPushRule { scope: _, keyword } => { + Self::SetKeywordPushRule { keyword } => { // `Content` push rule matching this keyword let new_rule = NewPatternedPushRule::new( keyword.clone(), diff --git a/crates/matrix-sdk/src/notification_settings/mod.rs b/crates/matrix-sdk/src/notification_settings/mod.rs index a5c153400e1..58a89f2c03e 100644 --- a/crates/matrix-sdk/src/notification_settings/mod.rs +++ b/crates/matrix-sdk/src/notification_settings/mod.rs @@ -451,44 +451,39 @@ impl NotificationSettings { let request_config = Some(RequestConfig::short_retry()); for command in &rule_commands.commands { match command { - Command::DeletePushRule { scope, kind, rule_id } => { - let request = delete_pushrule::v3::Request::new( - scope.clone(), - kind.clone(), - rule_id.clone(), - ); + Command::DeletePushRule { kind, rule_id } => { + let request = delete_pushrule::v3::Request::new(kind.clone(), rule_id.clone()); self.client.send(request, request_config).await.map_err(|error| { error!("Unable to delete {kind} push rule `{rule_id}`: {error}"); NotificationSettingsError::UnableToRemovePushRule })?; } - Command::SetRoomPushRule { scope, room_id, notify: _ } => { + Command::SetRoomPushRule { room_id, notify: _ } => { let push_rule = command.to_push_rule()?; - let request = set_pushrule::v3::Request::new(scope.clone(), push_rule); + let request = set_pushrule::v3::Request::new(push_rule); self.client.send(request, request_config).await.map_err(|error| { error!("Unable to set room push rule `{room_id}`: {error}"); NotificationSettingsError::UnableToAddPushRule })?; } - Command::SetOverridePushRule { scope, rule_id, room_id: _, notify: _ } => { + Command::SetOverridePushRule { rule_id, room_id: _, notify: _ } => { let push_rule = command.to_push_rule()?; - let request = set_pushrule::v3::Request::new(scope.clone(), push_rule); + let request = set_pushrule::v3::Request::new(push_rule); self.client.send(request, request_config).await.map_err(|error| { error!("Unable to set override push rule `{rule_id}`: {error}"); NotificationSettingsError::UnableToAddPushRule })?; } - Command::SetKeywordPushRule { scope, keyword: _ } => { + Command::SetKeywordPushRule { keyword: _ } => { let push_rule = command.to_push_rule()?; - let request = set_pushrule::v3::Request::new(scope.clone(), push_rule); + let request = set_pushrule::v3::Request::new(push_rule); self.client .send(request, request_config) .await .map_err(|_| NotificationSettingsError::UnableToAddPushRule)?; } - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { let request = set_pushrule_enabled::v3::Request::new( - scope.clone(), kind.clone(), rule_id.clone(), *enabled, @@ -498,9 +493,8 @@ impl NotificationSettings { NotificationSettingsError::UnableToUpdatePushRule })?; } - Command::SetPushRuleActions { scope, kind, rule_id, actions } => { + Command::SetPushRuleActions { kind, rule_id, actions } => { let request = set_pushrule_actions::v3::Request::new( - scope.clone(), kind.clone(), rule_id.clone(), actions.clone(), diff --git a/crates/matrix-sdk/src/notification_settings/rule_commands.rs b/crates/matrix-sdk/src/notification_settings/rule_commands.rs index 3ac2cda73d9..47956c412b1 100644 --- a/crates/matrix-sdk/src/notification_settings/rule_commands.rs +++ b/crates/matrix-sdk/src/notification_settings/rule_commands.rs @@ -1,5 +1,4 @@ use ruma::{ - api::client::push::RuleScope, push::{ Action, PredefinedContentRuleId, PredefinedOverrideRuleId, RemovePushRuleError, RuleKind, Ruleset, @@ -31,13 +30,8 @@ impl RuleCommands { notify: bool, ) -> Result<(), NotificationSettingsError> { let command = match kind { - RuleKind::Room => Command::SetRoomPushRule { - scope: RuleScope::Global, - room_id: room_id.to_owned(), - notify, - }, + RuleKind::Room => Command::SetRoomPushRule { room_id: room_id.to_owned(), notify }, RuleKind::Override => Command::SetOverridePushRule { - scope: RuleScope::Global, rule_id: room_id.to_string(), room_id: room_id.to_owned(), notify, @@ -60,7 +54,7 @@ impl RuleCommands { &mut self, keyword: String, ) -> Result<(), NotificationSettingsError> { - let command = Command::SetKeywordPushRule { scope: RuleScope::Global, keyword }; + let command = Command::SetKeywordPushRule { keyword }; self.rules.insert(command.to_push_rule()?, None, None)?; self.commands.push(command); @@ -75,7 +69,7 @@ impl RuleCommands { rule_id: String, ) -> Result<(), RemovePushRuleError> { self.rules.remove(kind.clone(), &rule_id)?; - self.commands.push(Command::DeletePushRule { scope: RuleScope::Global, kind, rule_id }); + self.commands.push(Command::DeletePushRule { kind, rule_id }); Ok(()) } @@ -90,7 +84,6 @@ impl RuleCommands { .set_enabled(kind.clone(), rule_id, enabled) .map_err(|_| NotificationSettingsError::RuleNotFound(rule_id.to_owned()))?; self.commands.push(Command::SetPushRuleEnabled { - scope: RuleScope::Global, kind, rule_id: rule_id.to_owned(), enabled, @@ -182,7 +175,6 @@ impl RuleCommands { .set_actions(kind.clone(), rule_id, actions.clone()) .map_err(|_| NotificationSettingsError::RuleNotFound(rule_id.to_owned()))?; self.commands.push(Command::SetPushRuleActions { - scope: RuleScope::Global, kind, rule_id: rule_id.to_owned(), actions, @@ -196,7 +188,6 @@ mod tests { use assert_matches::assert_matches; use matrix_sdk_test::async_test; use ruma::{ - api::client::push::RuleScope, push::{ Action, NewPushRule, NewSimplePushRule, PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RemovePushRuleError, RuleKind, @@ -229,8 +220,7 @@ mod tests { // Exactly one command must have been created. assert_eq!(rule_commands.commands.len(), 1); assert_matches!(&rule_commands.commands[0], - Command::SetRoomPushRule { scope, room_id: command_room_id, notify } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetRoomPushRule { room_id: command_room_id, notify } => { assert_eq!(command_room_id, &room_id); assert!(notify); } @@ -249,8 +239,7 @@ mod tests { // Exactly one command must have been created. assert_eq!(rule_commands.commands.len(), 1); assert_matches!(&rule_commands.commands[0], - Command::SetOverridePushRule { scope, room_id: command_room_id, rule_id, notify } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetOverridePushRule {room_id: command_room_id, rule_id, notify } => { assert_eq!(command_room_id, &room_id); assert_eq!(rule_id, room_id.as_str()); assert!(notify); @@ -298,8 +287,7 @@ mod tests { // Exactly one command must have been created. assert_eq!(rule_commands.commands.len(), 1); assert_matches!(&rule_commands.commands[0], - Command::DeletePushRule { scope, kind, rule_id } => { - assert_eq!(scope, &RuleScope::Global); + Command::DeletePushRule { kind, rule_id } => { assert_eq!(kind, &RuleKind::Room); assert_eq!(rule_id, room_id.as_str()); } @@ -351,8 +339,7 @@ mod tests { // Exactly one command must have been created. assert_eq!(rule_commands.commands.len(), 1); assert_matches!(&rule_commands.commands[0], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { assert_eq!(kind, &RuleKind::Override); assert_eq!(rule_id, PredefinedOverrideRuleId::Reaction.as_str()); assert!(enabled); @@ -426,8 +413,7 @@ mod tests { assert_eq!(rule_commands.commands.len(), 3); assert_matches!(&rule_commands.commands[0], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { assert_eq!(kind, &RuleKind::Override); assert_eq!(rule_id, PredefinedOverrideRuleId::IsUserMention.as_str()); assert!(enabled); @@ -437,8 +423,7 @@ mod tests { #[allow(deprecated)] { assert_matches!(&rule_commands.commands[1], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { assert_eq!(kind, &RuleKind::Content); assert_eq!(rule_id, PredefinedContentRuleId::ContainsUserName.as_str()); assert!(enabled); @@ -446,8 +431,7 @@ mod tests { ); assert_matches!(&rule_commands.commands[2], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { assert_eq!(kind, &RuleKind::Override); assert_eq!(rule_id, PredefinedOverrideRuleId::ContainsDisplayName.as_str()); assert!(enabled); @@ -499,8 +483,7 @@ mod tests { assert_eq!(rule_commands.commands.len(), 2); assert_matches!(&rule_commands.commands[0], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { assert_eq!(kind, &RuleKind::Override); assert_eq!(rule_id, PredefinedOverrideRuleId::IsRoomMention.as_str()); assert!(enabled); @@ -510,8 +493,7 @@ mod tests { #[allow(deprecated)] { assert_matches!(&rule_commands.commands[1], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { assert_eq!(kind, &RuleKind::Override); assert_eq!(rule_id, PredefinedOverrideRuleId::RoomNotif.as_str()); assert!(enabled); @@ -550,8 +532,7 @@ mod tests { // and a `SetPushRuleActions` command must have been added assert_eq!(rule_commands.commands.len(), 1); assert_matches!(&rule_commands.commands[0], - Command::SetPushRuleActions { scope, kind, rule_id, actions } => { - assert_eq!(scope, &RuleScope::Global); + Command::SetPushRuleActions { kind, rule_id, actions } => { assert_eq!(kind, &RuleKind::Underride); assert_eq!(rule_id, PredefinedUnderrideRuleId::Message.as_str()); assert_eq!(actions.len(), 2); diff --git a/crates/matrix-sdk/src/notification_settings/rules.rs b/crates/matrix-sdk/src/notification_settings/rules.rs index 9414852585b..353af7b837d 100644 --- a/crates/matrix-sdk/src/notification_settings/rules.rs +++ b/crates/matrix-sdk/src/notification_settings/rules.rs @@ -253,7 +253,7 @@ impl Rules { pub(crate) fn apply(&mut self, commands: RuleCommands) { for command in commands.commands { match command { - Command::DeletePushRule { scope: _, kind, rule_id } => { + Command::DeletePushRule { kind, rule_id } => { _ = self.ruleset.remove(kind, rule_id); } Command::SetRoomPushRule { .. } @@ -263,10 +263,10 @@ impl Rules { _ = self.ruleset.insert(push_rule, None, None); } } - Command::SetPushRuleEnabled { scope: _, kind, rule_id, enabled } => { + Command::SetPushRuleEnabled { kind, rule_id, enabled } => { _ = self.ruleset.set_enabled(kind, rule_id, enabled); } - Command::SetPushRuleActions { scope: _, kind, rule_id, actions } => { + Command::SetPushRuleActions { kind, rule_id, actions } => { _ = self.ruleset.set_actions(kind, rule_id, actions); } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 420aaf8eb5d..0be442f9167 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -78,7 +78,7 @@ use ruma::{ beacon_info::BeaconInfoEventContent, call::notify::{ApplicationType, CallNotifyEventContent, NotifyType}, direct::DirectEventContent, - marked_unread::MarkedUnreadEventContent, + marked_unread::{MarkedUnreadEventContent, UnstableMarkedUnreadEventContent}, receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ avatar::{self, RoomAvatarEventContent}, @@ -2931,7 +2931,7 @@ impl Room { pub async fn set_unread_flag(&self, unread: bool) -> Result<()> { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; - let content = MarkedUnreadEventContent::new(unread); + let content = UnstableMarkedUnreadEventContent::from(MarkedUnreadEventContent::new(unread)); let request = set_room_account_data::v3::Request::new( user_id.to_owned(), diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 43258f4f634..322a7563f00 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1106,7 +1106,7 @@ mod tests { use matrix_sdk_test::async_test; use ruma::{ api::client::error::ErrorKind, assign, owned_room_id, room_id, serde::Raw, uint, - DeviceKeyAlgorithm, OwnedRoomId, TransactionId, + OwnedRoomId, TransactionId, }; use serde::Deserialize; use serde_json::json; @@ -2689,6 +2689,8 @@ mod tests { #[async_test] #[cfg(feature = "e2e-encryption")] async fn test_process_only_encryption_events() -> Result<()> { + use ruma::OneTimeKeyAlgorithm; + let room = owned_room_id!("!croissant:example.org"); let server = MockServer::start().await; @@ -2705,7 +2707,7 @@ mod tests { extensions: assign!(http::response::Extensions::default(), { e2ee: assign!(http::response::E2EE::default(), { - device_one_time_keys_count: BTreeMap::from([(DeviceKeyAlgorithm::SignedCurve25519, uint!(42))]) + device_one_time_keys_count: BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, uint!(42))]) }), to_device: Some(assign!(http::response::ToDevice::default(), { next_batch: "to-device-token".to_owned(), diff --git a/crates/matrix-sdk/tests/integration/encryption/verification.rs b/crates/matrix-sdk/tests/integration/encryption/verification.rs index ab7eb336fec..69bc38514f6 100644 --- a/crates/matrix-sdk/tests/integration/encryption/verification.rs +++ b/crates/matrix-sdk/tests/integration/encryption/verification.rs @@ -20,7 +20,7 @@ use ruma::{ encryption::{CrossSigningKey, DeviceKeys}, owned_device_id, owned_user_id, serde::Raw, - user_id, DeviceId, DeviceKeyId, OwnedDeviceId, OwnedUserId, + user_id, CrossSigningKeyId, DeviceId, OwnedDeviceId, OwnedUserId, }; use serde_json::json; use wiremock::{ @@ -239,8 +239,10 @@ fn mock_keys_signature_upload(keys: Arc>) -> impl Fn(&Request) -> Re if let Some(existing_master_key) = keys.master.get_mut(&user) { let mut existing = existing_master_key.deserialize().unwrap(); - let target = - DeviceKeyId::from_parts(ruma::DeviceKeyAlgorithm::Ed25519, key_id.into()); + let target = CrossSigningKeyId::from_parts( + ruma::SigningKeyAlgorithm::Ed25519, + key_id.try_into().unwrap(), + ); if existing.keys.contains_key(&target) { let param: CrossSigningKey = serde_json::from_str(raw_key.get()).unwrap(); From 7d64ea1bbc5441434dd21ca395d761510f7c0a43 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 28 Oct 2024 12:02:31 +0100 Subject: [PATCH 407/979] feat(sdk): Introduce `event_cache::Deduplicator`. This patch introduces `Deduplicator`, an efficient type to detect duplicated events in the event cache. It uses a bloom filter, and decorates a collection of events with `Decoration`, which an enum that marks whether an event is unique, duplicated or invalid. --- Cargo.lock | 1 + crates/matrix-sdk/Cargo.toml | 1 + .../src/event_cache/deduplicator.rs | 270 ++++++++++++++++++ crates/matrix-sdk/src/event_cache/mod.rs | 1 + 4 files changed, 273 insertions(+) create mode 100644 crates/matrix-sdk/src/event_cache/deduplicator.rs diff --git a/Cargo.lock b/Cargo.lock index f3c3d638016..12c22b9df21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2898,6 +2898,7 @@ dependencies = [ "futures-executor", "futures-util", "gloo-timers", + "growable-bloom-filter", "http", "imbl", "indexmap 2.6.0", diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index cf356eb7114..27f91ed7ef4 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -82,6 +82,7 @@ eyeball-im = { workspace = true } eyre = { version = "0.6.8", optional = true } futures-core = { workspace = true } futures-util = { workspace = true } +growable-bloom-filter = { workspace = true } http = { workspace = true } imbl = { workspace = true, features = ["serde"] } indexmap = "2.0.2" diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs new file mode 100644 index 00000000000..dae5a274d74 --- /dev/null +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -0,0 +1,270 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Simple but efficient types to find duplicated events. See [`Deduplicator`] +//! to learn more. + +use std::{collections::BTreeSet, sync::Mutex}; + +use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; + +use super::store::{Event, RoomEvents}; + +/// `Deduplicator` is an efficient type to find duplicated events. +/// +/// It uses a [bloom filter] to provide a memory efficient probabilistic answer +/// to: “has event E been seen already?”. False positives are possible, while +/// false negatives are impossible. In the case of a positive reply, we fallback +/// to a linear (backward) search on all events to check whether it's a false +/// positive or not +/// +/// [bloom filter]: https://en.wikipedia.org/wiki/Bloom_filter +pub struct Deduplicator { + bloom_filter: Mutex, +} + +impl Deduplicator { + const APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS: usize = 800_000; + const DESIRED_FALSE_POSITIVE_RATE: f64 = 0.001; + + /// Create a new `Deduplicator`. + pub fn new() -> Self { + Self { + bloom_filter: Mutex::new( + GrowableBloomBuilder::new() + .estimated_insertions(Self::APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS) + .desired_error_ratio(Self::DESIRED_FALSE_POSITIVE_RATE) + .build(), + ), + } + } + + /// Scan a collection of events and detect duplications. + /// + /// This method takes a collection of events `new_events_to_scan` and + /// returns a new collection of events, where each event is decorated by + /// a [`Decoration`], so that the caller can decide what to do with + /// these events. + /// + /// Each scanned event will update `Self`'s internal state. + /// + /// `existing_events` represents all events of a room that already exist. + pub fn scan_and_learn<'a, I>( + &'a self, + new_events_to_scan: I, + existing_events: &'a RoomEvents, + ) -> impl Iterator> + 'a + where + I: Iterator + 'a, + { + // `new_scanned_events` is not a field of `Self` because it is used only detect + // duplicates in `new_events_to_scan`. + let mut new_scanned_events = BTreeSet::new(); + + new_events_to_scan.map(move |event| { + let Some(event_id) = event.event_id() else { + // The event has no `event_id`. + return Decoration::Invalid(event); + }; + + if self.bloom_filter.lock().unwrap().check_and_set(&event_id) { + // Oh oh, it looks like we have found a duplicate! + // + // However, bloom filters have false positives. We are NOT sure the event is NOT + // present. Even if the false positive rate is low, we need to + // iterate over all events to ensure it isn't present. + + // First, let's ensure `event` is not a duplicate from `new_events_to_scan`, + // i.e. if the iterator itself contains duplicated events! We use a `BTreeSet`, + // otherwise using a bloom filter again may generate false positives. + if new_scanned_events.contains(&event_id) { + // The iterator contains a duplicated `event`. + return Decoration::Duplicated(event); + } + + // Second, we can iterate over all events to ensure `event` is not present in + // `existing_events`. + let duplicated = existing_events.revents().any(|(_position, other_event)| { + other_event.event_id().as_ref() == Some(&event_id) + }); + + new_scanned_events.insert(event_id); + + if duplicated { + Decoration::Duplicated(event) + } else { + Decoration::Unique(event) + } + } else { + new_scanned_events.insert(event_id); + + // Bloom filter has no false negatives. We are sure the event is NOT present: we + // can keep it in the iterator. + Decoration::Unique(event) + } + }) + } +} + +/// Information about the scanned collection of events. +#[derive(Debug)] +pub enum Decoration { + /// This event is not duplicated. + Unique(I), + + /// This event is duplicated. + Duplicated(I), + + /// This event is invalid (i.e. not well formed). + Invalid(I), +} + +#[cfg(test)] +mod tests { + use assert_matches2::assert_let; + use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_test::{EventBuilder, ALICE}; + use ruma::{events::room::message::RoomMessageEventContent, owned_event_id, EventId}; + + use super::*; + + fn sync_timeline_event(event_builder: &EventBuilder, event_id: &EventId) -> SyncTimelineEvent { + SyncTimelineEvent::new(event_builder.make_sync_message_event_with_id( + *ALICE, + event_id, + RoomMessageEventContent::text_plain("foo"), + )) + } + + #[test] + fn test_filter_no_duplicate() { + let event_builder = EventBuilder::new(); + + let event_id_0 = owned_event_id!("$ev0"); + let event_id_1 = owned_event_id!("$ev1"); + let event_id_2 = owned_event_id!("$ev2"); + + let event_0 = sync_timeline_event(&event_builder, &event_id_0); + let event_1 = sync_timeline_event(&event_builder, &event_id_1); + let event_2 = sync_timeline_event(&event_builder, &event_id_2); + + let deduplicator = Deduplicator::new(); + let existing_events = RoomEvents::new(); + + let mut events = + deduplicator.scan_and_learn([event_0, event_1, event_2].into_iter(), &existing_events); + + assert_let!(Some(Decoration::Unique(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_0)); + + assert_let!(Some(Decoration::Unique(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_1)); + + assert_let!(Some(Decoration::Unique(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_2)); + + assert!(events.next().is_none()); + } + + #[test] + fn test_filter_duplicates_in_new_events() { + let event_builder = EventBuilder::new(); + + let event_id_0 = owned_event_id!("$ev0"); + let event_id_1 = owned_event_id!("$ev1"); + + let event_0 = sync_timeline_event(&event_builder, &event_id_0); + let event_1 = sync_timeline_event(&event_builder, &event_id_1); + + let deduplicator = Deduplicator::new(); + let existing_events = RoomEvents::new(); + + let mut events = deduplicator.scan_and_learn( + [ + event_0.clone(), // OK + event_0, // Not OK + event_1, // OK + ] + .into_iter(), + &existing_events, + ); + + assert_let!(Some(Decoration::Unique(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_0.clone())); + + assert_let!(Some(Decoration::Duplicated(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_0)); + + assert_let!(Some(Decoration::Unique(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_1)); + + assert!(events.next().is_none()); + } + + #[test] + fn test_filter_duplicates_with_existing_events() { + let event_builder = EventBuilder::new(); + + let event_id_0 = owned_event_id!("$ev0"); + let event_id_1 = owned_event_id!("$ev1"); + let event_id_2 = owned_event_id!("$ev2"); + + let event_0 = sync_timeline_event(&event_builder, &event_id_0); + let event_1 = sync_timeline_event(&event_builder, &event_id_1); + let event_2 = sync_timeline_event(&event_builder, &event_id_2); + + let deduplicator = Deduplicator::new(); + let mut existing_events = RoomEvents::new(); + + // Simulate `event_1` is inserted inside `existing_events`. + { + let mut events = + deduplicator.scan_and_learn([event_1.clone()].into_iter(), &existing_events); + + assert_let!(Some(Decoration::Unique(event_1)) = events.next()); + assert_eq!(event_1.event_id(), Some(event_id_1.clone())); + + assert!(events.next().is_none()); + + drop(events); // make the borrow checker happy. + + // Now we can push `event_1` inside `existing_events`. + existing_events.push_events([event_1]); + } + + // `event_1` will be duplicated. + { + let mut events = deduplicator.scan_and_learn( + [ + event_0, // OK + event_1, // Not OK + event_2, // Ok + ] + .into_iter(), + &existing_events, + ); + + assert_let!(Some(Decoration::Unique(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_0)); + + assert_let!(Some(Decoration::Duplicated(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_1)); + + assert_let!(Some(Decoration::Unique(event)) = events.next()); + assert_eq!(event.event_id(), Some(event_id_2)); + + assert!(events.next().is_none()); + } + } +} diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index ff23b2c4e81..7aa31485386 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -53,6 +53,7 @@ use tracing::{error, info_span, instrument, trace, warn, Instrument as _, Span}; use self::paginator::PaginatorError; use crate::{client::WeakClient, Client}; +mod deduplicator; mod linked_chunk; mod pagination; mod room; From fe79826c7a0329f0966aefbcc16e8b820ed83fbd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 28 Oct 2024 12:08:43 +0100 Subject: [PATCH 408/979] feat(sdk): Find and remove duplicated events in `RoomEvents`. This patch uses the new `Deduplicator` type, along with `LinkeChunk::remove_item_at` to remove duplicated events. When a new event is received, the older one is removed. --- .../src/event_cache/deduplicator.rs | 10 +- .../src/event_cache/linked_chunk/mod.rs | 9 + .../matrix-sdk/src/event_cache/room/events.rs | 690 +++++++++++++----- 3 files changed, 544 insertions(+), 165 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs index dae5a274d74..1ac40894bac 100644 --- a/crates/matrix-sdk/src/event_cache/deduplicator.rs +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -15,11 +15,11 @@ //! Simple but efficient types to find duplicated events. See [`Deduplicator`] //! to learn more. -use std::{collections::BTreeSet, sync::Mutex}; +use std::{collections::BTreeSet, fmt, sync::Mutex}; use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; -use super::store::{Event, RoomEvents}; +use super::room::events::{Event, RoomEvents}; /// `Deduplicator` is an efficient type to find duplicated events. /// @@ -34,6 +34,12 @@ pub struct Deduplicator { bloom_filter: Mutex, } +impl fmt::Debug for Deduplicator { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.debug_struct("Deduplicator").finish_non_exhaustive() + } +} + impl Deduplicator { const APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS: usize = 800_000; const DESIRED_FALSE_POSITIVE_RATE: f64 = 0.001; diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs index 1bfda5f9df7..99fd02fe5bc 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs @@ -948,6 +948,15 @@ impl Position { pub fn index(&self) -> usize { self.1 } + + /// Decrement the index part (see [`Self::index`]), i.e. subtract 1. + /// + /// # Panic + /// + /// This method will panic if it will underflow, i.e. if the index is 0. + pub(super) fn decrement_index(&mut self) { + self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0"); + } } /// An iterator over a [`LinkedChunk`] that traverses the chunk in backward diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index f1b4eba7a0a..322c5c193dd 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -12,9 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::cmp::Ordering; + use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +use ruma::OwnedEventId; +use tracing::{debug, error, warn}; -use super::super::linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}; +use super::super::{ + deduplicator::{Decoration, Deduplicator}, + linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}, +}; /// An alias for the real event type. pub(crate) type Event = SyncTimelineEvent; @@ -33,6 +40,9 @@ const DEFAULT_CHUNK_CAPACITY: usize = 128; pub struct RoomEvents { /// The real in-memory storage for all the events. chunks: LinkedChunk, + + /// The events deduplicator instance to help finding duplicates. + deduplicator: Deduplicator, } impl Default for RoomEvents { @@ -44,7 +54,7 @@ impl Default for RoomEvents { impl RoomEvents { /// Build a new [`RoomEvents`] struct with zero events. pub fn new() -> Self { - Self { chunks: LinkedChunk::new() } + Self { chunks: LinkedChunk::new(), deduplicator: Deduplicator::new() } } /// Clear all events. @@ -58,9 +68,18 @@ impl RoomEvents { pub fn push_events(&mut self, events: I) where I: IntoIterator, - I::IntoIter: ExactSizeIterator, { - self.chunks.push_items_back(events) + let (unique_events, duplicated_event_ids) = + self.filter_duplicated_events(events.into_iter()); + + // Remove the _old_ duplicated events! + // + // We don't have to worry the removals can change the position of the existing + // events, because we are pushing all _new_ `events` at the back. + self.remove_events(duplicated_event_ids); + + // Push new `events`. + self.chunks.push_items_back(unique_events); } /// Push a gap after all events or gaps. @@ -69,12 +88,21 @@ impl RoomEvents { } /// Insert events at a specified position. - pub fn insert_events_at(&mut self, events: I, position: Position) -> Result<(), Error> + pub fn insert_events_at(&mut self, events: I, mut position: Position) -> Result<(), Error> where I: IntoIterator, - I::IntoIter: ExactSizeIterator, { - self.chunks.insert_items_at(events, position) + let (unique_events, duplicated_event_ids) = + self.filter_duplicated_events(events.into_iter()); + + // Remove the _old_ duplicated events! + // + // We **have to worry* the removals can change the position of the + // existing events. We **have** to update the `position` + // argument value for each removal. + self.remove_events_and_update_insert_position(duplicated_event_ids, &mut position); + + self.chunks.insert_items_at(unique_events, position) } /// Insert a gap at a specified position. @@ -96,9 +124,19 @@ impl RoomEvents { ) -> Result<&Chunk, Error> where I: IntoIterator, - I::IntoIter: ExactSizeIterator, { - self.chunks.replace_gap_at(events, gap_identifier) + let (unique_events, duplicated_event_ids) = + self.filter_duplicated_events(events.into_iter()); + + // Remove the _old_ duplicated events! + // + // We don't have to worry the removals can change the position of the existing + // events, because we are replacing a gap: its identifier will not change + // because of the removals. + self.remove_events(duplicated_event_ids); + + // Replace the gap by new events. + self.chunks.replace_gap_at(unique_events, gap_identifier) } /// Search for a chunk, and return its identifier. @@ -129,6 +167,148 @@ impl RoomEvents { pub fn events(&self) -> impl Iterator { self.chunks.items() } + + /// Deduplicate `events` considering all events in `Self::chunks`. + /// + /// The returned tuple contains (i) the unique events, and (ii) the + /// duplicated events (by ID). + fn filter_duplicated_events<'a, I>(&'a mut self, events: I) -> (Vec, Vec) + where + I: Iterator + 'a, + { + let mut duplicated_event_ids = Vec::new(); + + let deduplicated_events = self + .deduplicator + .scan_and_learn(events, self) + .filter_map(|decorated_event| match decorated_event { + Decoration::Unique(event) => Some(event), + Decoration::Duplicated(event) => { + debug!(event_id = ?event.event_id(), "Found a duplicated event"); + + duplicated_event_ids.push( + event + .event_id() + // SAFETY: An event with no ID is decorated as `Decoration::Invalid`. + // Thus, it's safe to unwrap the `Option` here. + .expect("The event has no ID"), + ); + + // Keep the new event! + Some(event) + } + Decoration::Invalid(event) => { + warn!(?event, "Found an event with no ID"); + + None + } + }) + .collect(); + + (deduplicated_events, duplicated_event_ids) + } +} + +// Private implementations, implementation specific. +impl RoomEvents { + /// Remove some events from `Self::chunks`. + /// + /// This method iterates over all event IDs in `event_ids` and removes the + /// associated event (if it exists) from `Self::chunks`. + /// + /// This is used to remove duplicated events, see + /// [`Self::filter_duplicated_events`]. + fn remove_events(&mut self, event_ids: Vec) { + for event_id in event_ids { + let Some(event_position) = self.revents().find_map(|(position, event)| { + (event.event_id().as_ref() == Some(&event_id)).then_some(position) + }) else { + error!(?event_id, "Cannot find the event to remove"); + + continue; + }; + + self.chunks + .remove_item_at(event_position) + .expect("Failed to remove an event we have just found"); + } + } + + /// Remove all events from `Self::chunks` and update a fix [`Position`]. + /// + /// This method iterates over all event IDs in `event_ids` and removes the + /// associated event (if it exists) from `Self::chunks`, exactly like + /// [`Self::remove_events`]. The difference is that it will maintain a + /// [`Position`] according to the removals. This is useful for example if + /// one needs to insert events at a particular position, but it first + /// collects events that must be removed before the insertions (e.g. + /// duplicated events). One has to remove events, but also to maintain the + /// `Position` to its correct initial _target_. Let's see a practical + /// example: + /// + /// ```text + /// // Pseudo-code. + /// + /// let room_events = room_events(['a', 'b', 'c']); + /// let position = position_of('b' in room_events); + /// room_events.remove_events(['a']) + /// + /// // `position` no longer targets 'b', it now targets 'c', because all + /// // items have shifted to the left once. Instead, let's do: + /// + /// let room_events = room_events(['a', 'b', 'c']); + /// let position = position_of('b' in room_events); + /// room_events.remove_events_and_update_insert_position(['a'], &mut position) + /// + /// // `position` has been updated to still target 'b'. + /// ``` + /// + /// This is used to remove duplicated events, see + /// [`Self::filter_duplicated_events`]. + fn remove_events_and_update_insert_position( + &mut self, + event_ids: Vec, + position: &mut Position, + ) { + for event_id in event_ids { + let Some(event_position) = self.revents().find_map(|(position, event)| { + (event.event_id().as_ref() == Some(&event_id)).then_some(position) + }) else { + error!(?event_id, "Cannot find the event to remove"); + + continue; + }; + + self.chunks + .remove_item_at(event_position) + .expect("Failed to remove an event we have just found"); + + // A `Position` is composed of a `ChunkIdentifier` and an index. + // The `ChunkIdentifier` is stable, i.e. it won't change if an + // event is removed in another chunk. It means we only need to + // update `position` if the removal happened in **the same + // chunk**. + if event_position.chunk_identifier() == position.chunk_identifier() { + // Now we can compare the position indices. + match event_position.index().cmp(&position.index()) { + // `event_position`'s index < `position`'s index + Ordering::Less => { + // An event has been removed _before_ the new + // events: `position` needs to be shifted to the + // left by 1. + position.decrement_index(); + } + + // `event_position`'s index >= `position`'s index + Ordering::Equal | Ordering::Greater => { + // An event has been removed at the _same_ position of + // or _after_ the new events: `position` does _NOT_ need + // to be modified. + } + } + } + } + } } #[cfg(test)] @@ -139,6 +319,23 @@ mod tests { use super::*; + macro_rules! assert_events_eq { + ( $events_iterator:expr, [ $( ( $event_id:ident at ( $chunk_identifier:literal, $index:literal ) ) ),* $(,)? ] ) => { + { + let mut events = $events_iterator; + + $( + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), $chunk_identifier ); + assert_eq!(position.index(), $index ); + assert_eq!(event.event_id().unwrap(), $event_id ); + )* + + assert!(events.next().is_none(), "No more events are expected"); + } + }; + } + fn new_event(event_builder: &EventBuilder, event_id: &str) -> (OwnedEventId, Event) { let event_id = EventId::parse(event_id).unwrap(); @@ -171,26 +368,14 @@ mod tests { room_events.push_events([event_0, event_1]); room_events.push_events([event_2]); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 2); - assert_eq!(event.event_id().unwrap(), event_id_2); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + (event_id_2 at (0, 2)), + ] + ); } #[test] @@ -198,27 +383,31 @@ mod tests { let event_builder = EventBuilder::new(); let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); let mut room_events = RoomEvents::new(); - room_events.push_events([event_0.clone()]); - room_events.push_events([event_0]); - - { - let mut events = room_events.events(); + room_events.push_events([event_0.clone(), event_1]); - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + ] + ); - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_0); + // Everything is alright. Now let's push a duplicated event. + room_events.push_events([event_0]); - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + // The first `event_id_0` has been removed. + (event_id_1 at (0, 0)), + (event_id_0 at (0, 1)), + ] + ); } #[test] @@ -234,21 +423,13 @@ mod tests { room_events.push_gap(Gap { prev_token: "hello".to_owned() }); room_events.push_events([event_1]); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (2, 0)), + ] + ); { let mut chunks = room_events.chunks(); @@ -287,69 +468,60 @@ mod tests { room_events.insert_events_at([event_2], position_of_event_1).unwrap(); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_2); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 2); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_2 at (0, 1)), + (event_id_1 at (0, 2)), + ] + ); } #[test] - fn test_insert_events_at_with_dupicates() { + fn test_insert_events_at_with_duplicates() { let event_builder = EventBuilder::new(); let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); let mut room_events = RoomEvents::new(); - room_events.push_events([event_0, event_1.clone()]); + room_events.push_events([event_0.clone(), event_1, event_2]); - let position_of_event_1 = room_events + let position_of_event_2 = room_events .events() .find_map(|(position, event)| { - (event.event_id().unwrap() == event_id_1).then_some(position) + (event.event_id().unwrap() == event_id_2).then_some(position) }) .unwrap(); - room_events.insert_events_at([event_1], position_of_event_1).unwrap(); - - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 2); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + (event_id_2 at (0, 2)), + ] + ); + + // Everything is alright. Now let's insert a duplicated events! + room_events.insert_events_at([event_0, event_3], position_of_event_2).unwrap(); + + assert_events_eq!( + room_events.events(), + [ + // The first `event_id_0` has been removed. + (event_id_1 at (0, 0)), + (event_id_0 at (0, 1)), + (event_id_3 at (0, 2)), + (event_id_2 at (0, 3)), + ] + ); } + #[test] fn test_insert_gap_at() { let event_builder = EventBuilder::new(); @@ -372,21 +544,13 @@ mod tests { .insert_gap_at(Gap { prev_token: "hello".to_owned() }, position_of_event_1) .unwrap(); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (2, 0)), + ] + ); { let mut chunks = room_events.chunks(); @@ -425,26 +589,14 @@ mod tests { room_events.replace_gap_at([event_1, event_2], chunk_identifier_of_gap).unwrap(); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_2); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (2, 0)), + (event_id_2 at (2, 1)), + ] + ); { let mut chunks = room_events.chunks(); @@ -465,10 +617,11 @@ mod tests { let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); let mut room_events = RoomEvents::new(); - room_events.push_events([event_0.clone()]); + room_events.push_events([event_0.clone(), event_1]); room_events.push_gap(Gap { prev_token: "hello".to_owned() }); let chunk_identifier_of_gap = room_events @@ -477,28 +630,26 @@ mod tests { .unwrap() .chunk_identifier(); - room_events.replace_gap_at([event_0, event_1], chunk_identifier_of_gap).unwrap(); - - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + ] + ); + + // Everything is alright. Now let's replace a gap with a duplicated event. + room_events.replace_gap_at([event_0, event_2], chunk_identifier_of_gap).unwrap(); + + assert_events_eq!( + room_events.events(), + [ + // The first `event_id_0` has been removed. + (event_id_1 at (0, 0)), + (event_id_0 at (2, 0)), + (event_id_2 at (2, 1)), + ] + ); { let mut chunks = room_events.chunks(); @@ -512,4 +663,217 @@ mod tests { assert!(chunks.next().is_none()); } } + + #[test] + fn test_remove_events() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); + + // Push some events. + let mut room_events = RoomEvents::new(); + room_events.push_events([event_0, event_1, event_2, event_3]); + + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + (event_id_2 at (0, 2)), + (event_id_3 at (0, 3)), + ] + ); + + // Remove some events. + room_events.remove_events(vec![event_id_1, event_id_3]); + + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_2 at (0, 1)), + ] + ); + } + + #[test] + fn test_remove_events_unknown_event() { + let event_builder = EventBuilder::new(); + + let (event_id_0, _event_0) = new_event(&event_builder, "$ev0"); + + // Push ZERO event. + let mut room_events = RoomEvents::new(); + + assert_events_eq!(room_events.events(), []); + + // Remove one undefined event. + // No error is expected. + room_events.remove_events(vec![event_id_0]); + + assert_events_eq!(room_events.events(), []); + + let mut events = room_events.events(); + assert!(events.next().is_none()); + } + + #[test] + fn test_remove_events_and_update_insert_position() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); + let (event_id_4, event_4) = new_event(&event_builder, "$ev4"); + let (event_id_5, event_5) = new_event(&event_builder, "$ev5"); + let (event_id_6, event_6) = new_event(&event_builder, "$ev6"); + let (event_id_7, event_7) = new_event(&event_builder, "$ev7"); + let (event_id_8, event_8) = new_event(&event_builder, "$ev8"); + + // Push some events. + let mut room_events = RoomEvents::new(); + room_events.push_events([event_0, event_1, event_2, event_3, event_4, event_5, event_6]); + room_events.push_gap(Gap { prev_token: "raclette".to_owned() }); + room_events.push_events([event_7, event_8]); + + fn position_of(room_events: &RoomEvents, event_id: &EventId) -> Position { + room_events + .events() + .find_map(|(position, event)| { + (event.event_id().unwrap() == event_id).then_some(position) + }) + .unwrap() + } + + // In the same chunk… + { + // Get the position of `event_4`. + let mut position = position_of(&room_events, &event_id_4); + + // Remove one event BEFORE `event_4`. + // + // The position must move to the left by 1. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_0], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index() - 1, position.index()); + + // It still represents the position of `event_4`. + assert_eq!(position, position_of(&room_events, &event_id_4)); + } + + // Remove one event AFTER `event_4`. + // + // The position must not move. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_5], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index(), position.index()); + + // It still represents the position of `event_4`. + assert_eq!(position, position_of(&room_events, &event_id_4)); + } + + // Remove one event: `event_4`. + // + // The position must not move. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_4], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index(), position.index()); + } + + // Check the events. + assert_events_eq!( + room_events.events(), + [ + (event_id_1 at (0, 0)), + (event_id_2 at (0, 1)), + (event_id_3 at (0, 2)), + (event_id_6 at (0, 3)), + (event_id_7 at (2, 0)), + (event_id_8 at (2, 1)), + ] + ); + } + + // In another chunk… + { + // Get the position of `event_7`. + let mut position = position_of(&room_events, &event_id_7); + + // Remove one event BEFORE `event_7`. + // + // The position must not move because it happens in another chunk. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_1], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index(), position.index()); + + // It still represents the position of `event_7`. + assert_eq!(position, position_of(&room_events, &event_id_7)); + } + + // Check the events. + assert_events_eq!( + room_events.events(), + [ + (event_id_2 at (0, 0)), + (event_id_3 at (0, 1)), + (event_id_6 at (0, 2)), + (event_id_7 at (2, 0)), + (event_id_8 at (2, 1)), + ] + ); + } + + // In the same chunk, but remove multiple events, just for the fun and to ensure + // the loop works correctly. + { + // Get the position of `event_6`. + let mut position = position_of(&room_events, &event_id_6); + + // Remove three events BEFORE `event_6`. + // + // The position must move. + { + let previous_position = position; + room_events.remove_events_and_update_insert_position( + vec![event_id_2, event_id_3, event_id_7], + &mut position, + ); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index() - 2, position.index()); + + // It still represents the position of `event_6`. + assert_eq!(position, position_of(&room_events, &event_id_6)); + } + + // Check the events. + assert_events_eq!( + room_events.events(), + [ + (event_id_6 at (0, 0)), + (event_id_8 at (2, 0)), + ] + ); + } + } } From 71abbeb1f12625ef3d147fb09a759bd1b09432f4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 29 Oct 2024 13:43:22 +0100 Subject: [PATCH 409/979] test(sdk): Use `EventFactory` to simplify the test cases. --- .../src/event_cache/deduplicator.rs | 40 +++---- .../matrix-sdk/src/event_cache/room/events.rs | 111 +++++++----------- 2 files changed, 61 insertions(+), 90 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs index 1ac40894bac..b87dbd01d42 100644 --- a/crates/matrix-sdk/src/event_cache/deduplicator.rs +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -140,30 +140,28 @@ pub enum Decoration { mod tests { use assert_matches2::assert_let; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; - use matrix_sdk_test::{EventBuilder, ALICE}; - use ruma::{events::room::message::RoomMessageEventContent, owned_event_id, EventId}; + use ruma::{owned_event_id, user_id, EventId}; use super::*; - - fn sync_timeline_event(event_builder: &EventBuilder, event_id: &EventId) -> SyncTimelineEvent { - SyncTimelineEvent::new(event_builder.make_sync_message_event_with_id( - *ALICE, - event_id, - RoomMessageEventContent::text_plain("foo"), - )) + use crate::test_utils::events::EventFactory; + + fn sync_timeline_event(event_id: &EventId) -> SyncTimelineEvent { + EventFactory::new() + .text_msg("") + .sender(user_id!("@mnt_io:matrix.org")) + .event_id(event_id) + .into_sync() } #[test] fn test_filter_no_duplicate() { - let event_builder = EventBuilder::new(); - let event_id_0 = owned_event_id!("$ev0"); let event_id_1 = owned_event_id!("$ev1"); let event_id_2 = owned_event_id!("$ev2"); - let event_0 = sync_timeline_event(&event_builder, &event_id_0); - let event_1 = sync_timeline_event(&event_builder, &event_id_1); - let event_2 = sync_timeline_event(&event_builder, &event_id_2); + let event_0 = sync_timeline_event(&event_id_0); + let event_1 = sync_timeline_event(&event_id_1); + let event_2 = sync_timeline_event(&event_id_2); let deduplicator = Deduplicator::new(); let existing_events = RoomEvents::new(); @@ -185,13 +183,11 @@ mod tests { #[test] fn test_filter_duplicates_in_new_events() { - let event_builder = EventBuilder::new(); - let event_id_0 = owned_event_id!("$ev0"); let event_id_1 = owned_event_id!("$ev1"); - let event_0 = sync_timeline_event(&event_builder, &event_id_0); - let event_1 = sync_timeline_event(&event_builder, &event_id_1); + let event_0 = sync_timeline_event(&event_id_0); + let event_1 = sync_timeline_event(&event_id_1); let deduplicator = Deduplicator::new(); let existing_events = RoomEvents::new(); @@ -220,15 +216,13 @@ mod tests { #[test] fn test_filter_duplicates_with_existing_events() { - let event_builder = EventBuilder::new(); - let event_id_0 = owned_event_id!("$ev0"); let event_id_1 = owned_event_id!("$ev1"); let event_id_2 = owned_event_id!("$ev2"); - let event_0 = sync_timeline_event(&event_builder, &event_id_0); - let event_1 = sync_timeline_event(&event_builder, &event_id_1); - let event_2 = sync_timeline_event(&event_builder, &event_id_2); + let event_0 = sync_timeline_event(&event_id_0); + let event_1 = sync_timeline_event(&event_id_1); + let event_2 = sync_timeline_event(&event_id_2); let deduplicator = Deduplicator::new(); let mut existing_events = RoomEvents::new(); diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 322c5c193dd..19d27b2e80d 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -314,10 +314,10 @@ impl RoomEvents { #[cfg(test)] mod tests { use assert_matches2::assert_let; - use matrix_sdk_test::{EventBuilder, ALICE}; - use ruma::{events::room::message::RoomMessageEventContent, EventId, OwnedEventId}; + use ruma::{user_id, EventId, OwnedEventId}; use super::*; + use crate::test_utils::events::EventFactory; macro_rules! assert_events_eq { ( $events_iterator:expr, [ $( ( $event_id:ident at ( $chunk_identifier:literal, $index:literal ) ) ),* $(,)? ] ) => { @@ -336,14 +336,13 @@ mod tests { }; } - fn new_event(event_builder: &EventBuilder, event_id: &str) -> (OwnedEventId, Event) { + fn new_event(event_id: &str) -> (OwnedEventId, Event) { let event_id = EventId::parse(event_id).unwrap(); - - let event = SyncTimelineEvent::new(event_builder.make_sync_message_event_with_id( - *ALICE, - &event_id, - RoomMessageEventContent::text_plain("foo"), - )); + let event = EventFactory::new() + .text_msg("") + .sender(user_id!("@mnt_io:matrix.org")) + .event_id(&event_id) + .into_sync(); (event_id, event) } @@ -357,11 +356,9 @@ mod tests { #[test] fn test_push_events() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); - let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); let mut room_events = RoomEvents::new(); @@ -380,10 +377,8 @@ mod tests { #[test] fn test_push_events_with_duplicates() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); let mut room_events = RoomEvents::new(); @@ -412,10 +407,8 @@ mod tests { #[test] fn test_push_gap() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); let mut room_events = RoomEvents::new(); @@ -449,11 +442,9 @@ mod tests { #[test] fn test_insert_events_at() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); - let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); let mut room_events = RoomEvents::new(); @@ -480,12 +471,10 @@ mod tests { #[test] fn test_insert_events_at_with_duplicates() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); - let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); - let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); + let (event_id_3, event_3) = new_event("$ev3"); let mut room_events = RoomEvents::new(); @@ -524,10 +513,8 @@ mod tests { #[test] fn test_insert_gap_at() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); let mut room_events = RoomEvents::new(); @@ -570,11 +557,9 @@ mod tests { #[test] fn test_replace_gap_at() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); - let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); let mut room_events = RoomEvents::new(); @@ -613,11 +598,9 @@ mod tests { #[test] fn test_replace_gap_at_with_duplicates() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); - let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); let mut room_events = RoomEvents::new(); @@ -666,12 +649,10 @@ mod tests { #[test] fn test_remove_events() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); - let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); - let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); + let (event_id_3, event_3) = new_event("$ev3"); // Push some events. let mut room_events = RoomEvents::new(); @@ -701,9 +682,7 @@ mod tests { #[test] fn test_remove_events_unknown_event() { - let event_builder = EventBuilder::new(); - - let (event_id_0, _event_0) = new_event(&event_builder, "$ev0"); + let (event_id_0, _event_0) = new_event("$ev0"); // Push ZERO event. let mut room_events = RoomEvents::new(); @@ -722,17 +701,15 @@ mod tests { #[test] fn test_remove_events_and_update_insert_position() { - let event_builder = EventBuilder::new(); - - let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); - let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); - let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); - let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); - let (event_id_4, event_4) = new_event(&event_builder, "$ev4"); - let (event_id_5, event_5) = new_event(&event_builder, "$ev5"); - let (event_id_6, event_6) = new_event(&event_builder, "$ev6"); - let (event_id_7, event_7) = new_event(&event_builder, "$ev7"); - let (event_id_8, event_8) = new_event(&event_builder, "$ev8"); + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); + let (event_id_3, event_3) = new_event("$ev3"); + let (event_id_4, event_4) = new_event("$ev4"); + let (event_id_5, event_5) = new_event("$ev5"); + let (event_id_6, event_6) = new_event("$ev6"); + let (event_id_7, event_7) = new_event("$ev7"); + let (event_id_8, event_8) = new_event("$ev8"); // Push some events. let mut room_events = RoomEvents::new(); From 75683d268fc2e0fc1c21e038ac9c59706a9b9180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 30 Oct 2024 16:10:38 +0100 Subject: [PATCH 410/979] refactor(crypto)!: Remove unused `OneTimeKey::Key` and `SessionCreationError::OneTimeKeyUnknown` variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk-crypto/src/error.rs | 7 ------- crates/matrix-sdk-crypto/src/olm/account.rs | 4 ---- .../src/types/one_time_keys.rs | 17 +---------------- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 4400da9d696..2e3348dce11 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -305,13 +305,6 @@ pub enum SessionCreationError { )] OneTimeKeyMissing(OwnedUserId, OwnedDeviceId), - /// The one-time key algorithm is unsupported. - #[error( - "Tried to create a new Olm session for {0} {1}, but the one-time \ - key algorithm is unsupported" - )] - OneTimeKeyUnknown(OwnedUserId, OwnedDeviceId), - /// Failed to verify the one-time key signatures. #[error( "Failed to verify the signature of a one-time key, key: {one_time_key:?}, \ diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index ac01502566c..b10340feeea 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -962,10 +962,6 @@ impl Account { let result = match first_key { OneTimeKey::SignedKey(key) => Ok(PrekeyBundle::Olm3DH { key }), - _ => Err(SessionCreationError::OneTimeKeyUnknown( - device.user_id().to_owned(), - device.device_id().into(), - )), }; trace!(?result, "Finished searching for a valid pre-key bundle"); diff --git a/crates/matrix-sdk-crypto/src/types/one_time_keys.rs b/crates/matrix-sdk-crypto/src/types/one_time_keys.rs index 33943787286..54b0f5d7c7b 100644 --- a/crates/matrix-sdk-crypto/src/types/one_time_keys.rs +++ b/crates/matrix-sdk-crypto/src/types/one_time_keys.rs @@ -103,10 +103,6 @@ impl SignedKey { pub enum OneTimeKey { /// A signed Curve25519 one-time key. SignedKey(SignedKey), - - /// An unsigned Curve25519 one-time key. - #[serde(serialize_with = "serialize_curve_key")] - Key(Curve25519PublicKey), } impl OneTimeKey { @@ -121,17 +117,7 @@ impl OneTimeKey { let key: SignedKey = key.deserialize_as()?; Ok(OneTimeKey::SignedKey(key)) } - _ => match algorithm.as_str() { - "curve25519" => { - let key: String = key.deserialize_as()?; - Ok(OneTimeKey::Key( - Curve25519PublicKey::from_base64(&key).map_err(serde::de::Error::custom)?, - )) - } - _ => { - Err(serde::de::Error::custom(format!("Unsupported key algorithm {algorithm}"))) - } - }, + _ => Err(serde::de::Error::custom(format!("Unsupported key algorithm {algorithm}"))), } } } @@ -141,7 +127,6 @@ impl OneTimeKey { pub fn fallback(&self) -> bool { match self { OneTimeKey::SignedKey(s) => s.fallback(), - OneTimeKey::Key(_) => false, } } } From 49f7fe90a9983f863c43b903ac63fa1880980807 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 10 Oct 2024 10:57:09 +0200 Subject: [PATCH 411/979] crypto-ffi: Expose `has_verification_violation` for `UserIdentity` --- bindings/matrix-sdk-crypto-ffi/src/users.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bindings/matrix-sdk-crypto-ffi/src/users.rs b/bindings/matrix-sdk-crypto-ffi/src/users.rs index dfffd9fe546..4f5f54ea85e 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/users.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/users.rs @@ -18,6 +18,8 @@ pub enum UserIdentity { user_signing_key: String, /// The public self-signing key of our identity. self_signing_key: String, + /// True if this identity was verified at some point but is not anymore. + has_verification_violation: bool, }, /// The user identity of other users. Other { @@ -27,6 +29,8 @@ pub enum UserIdentity { master_key: String, /// The public self-signing key of our identity. self_signing_key: String, + /// True if this identity was verified at some point but is not anymore. + has_verification_violation: bool, }, } @@ -44,6 +48,7 @@ impl UserIdentity { master_key: serde_json::to_string(&master)?, user_signing_key: serde_json::to_string(&user_signing)?, self_signing_key: serde_json::to_string(&self_signing)?, + has_verification_violation: i.has_verification_violation(), } } SdkUserIdentity::Other(i) => { @@ -54,6 +59,7 @@ impl UserIdentity { user_id: i.user_id().to_string(), master_key: serde_json::to_string(&master)?, self_signing_key: serde_json::to_string(&self_signing)?, + has_verification_violation: i.has_verification_violation(), } } }) From 5107f5f23ad5a27460b961668fd19070026011dd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 30 Oct 2024 14:41:24 +0100 Subject: [PATCH 412/979] chore(ffi): in `Client::account_url` return early when we're not an oidc session This avoids one spammy log for sessions not using oidc. --- bindings/matrix-sdk-ffi/src/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index be6176a58df..f405a5bf827 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -606,6 +606,10 @@ impl Client { &self, action: Option, ) -> Result, ClientError> { + if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) { + return Ok(None); + } + match self.inner.oidc().account_management_url(action.map(Into::into)).await { Ok(url) => Ok(url.map(|u| u.to_string())), Err(e) => { From 3c4845976856019600989e7394d05287204ff070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 1 Nov 2024 12:20:23 +0100 Subject: [PATCH 413/979] fix: Upgrade Ruma to 0.11.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings in a fix for KeyId::key_name. Signed-off-by: Kévin Commaille --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12c22b9df21..30db5f2af15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4575,9 +4575,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d719b9e1ce5b34a1e0b6e2ba4707f7923ce7fb3474881d771466456d68f3e485" +checksum = "e94984418ae8a5e1160e6c87608141330e9ae26330abf22e3d15416efa96d48a" dependencies = [ "assign", "js_int", @@ -4617,9 +4617,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4717eb215175df5087fdd79da2c9a4198c9a50fe747db0afbc23c8ac18a25da8" +checksum = "ad71c7f49abaa047ba228339d34f9aaefa4d8b50ebeb8e859d0340cc2138bda8" dependencies = [ "as_variant", "base64 0.22.1", @@ -4650,9 +4650,9 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969cfed397d22f0338d99457409aa9c9dd4def4a5ce8d6567e914a320bad30da" +checksum = "be86dccf3504588c1f4dc1bda4ce1f8bbd646fc6dda40c77cc7de6e203e62dad" dependencies = [ "as_variant", "indexmap 2.6.0", diff --git a/Cargo.toml b/Cargo.toml index 8ad1eacd0f0..e05f04ac6b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ once_cell = "1.16.0" pin-project-lite = "0.2.9" rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } -ruma = { version = "0.11.0", features = [ +ruma = { version = "0.11.1", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -59,7 +59,7 @@ ruma = { version = "0.11.0", features = [ "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = "0.14.0" +ruma-common = "0.14.1" serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" From 70bcddfba5e19f2d0fd11577edb55b9868f89678 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 1 Nov 2024 11:52:13 +0000 Subject: [PATCH 414/979] fix(crypto): Fix spelling error in a warning message. --- crates/matrix-sdk-crypto/src/store/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index de19b08a4db..92fdc6f32ec 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -215,7 +215,7 @@ impl KeyQueryManager { Err(_) => { warn!( user_id = ?user, - "The user has a pending `/key/query` request which did \ + "The user has a pending `/keys/query` request which did \ not finish yet, some devices might be missing." ); From 5d141fce13fce1ef73231c9af1858a073e385bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 30 Oct 2024 11:59:18 +0100 Subject: [PATCH 415/979] task(room_directory_search): add 'server' parameter to the room directory search Changelog: a new optional `via_server` parameter was added to `sdk::RoomDirectorySearch::search`, to specify which homeserver to use for searching rooms. In the FFI layer, this parameter is called `via_server_name`. --- .../src/room_directory_search.rs | 32 +++++++++++++++- .../matrix-sdk/src/room_directory_search.rs | 37 +++++++++++++------ .../src/tests/room_directory_search.rs | 4 +- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_directory_search.rs b/bindings/matrix-sdk-ffi/src/room_directory_search.rs index 9d37b60d45e..73baceff5e5 100644 --- a/bindings/matrix-sdk-ffi/src/room_directory_search.rs +++ b/bindings/matrix-sdk-ffi/src/room_directory_search.rs @@ -18,6 +18,7 @@ use std::{fmt::Debug, sync::Arc}; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch; +use ruma::ServerName; use tokio::sync::RwLock; use super::RUNTIME; @@ -68,6 +69,12 @@ impl From for RoomDescriptio } } +/// A helper for performing room searches in the room directory. +/// The way this is intended to be used is: +/// +/// 1. Register a callback using [`RoomDirectorySearch::results`]. +/// 2. Start the room search with [`RoomDirectorySearch::search`]. +/// 3. To get more results, use [`RoomDirectorySearch::next_page`]. #[derive(uniffi::Object)] pub struct RoomDirectorySearch { pub(crate) inner: RwLock, @@ -81,28 +88,49 @@ impl RoomDirectorySearch { #[matrix_sdk_ffi_macros::export] impl RoomDirectorySearch { + /// Asks the server for the next page of the current search. pub async fn next_page(&self) -> Result<(), ClientError> { let mut inner = self.inner.write().await; inner.next_page().await?; Ok(()) } - pub async fn search(&self, filter: Option, batch_size: u32) -> Result<(), ClientError> { + /// Starts a filtered search for the server. + /// + /// If the `filter` is not provided it will search for all the rooms. + /// You can specify a `batch_size` to control the number of rooms to fetch + /// per request. + /// + /// If the `via_server` is not provided it will search in the current + /// homeserver by default. + /// + /// This method will clear the current search results and start a new one. + pub async fn search( + &self, + filter: Option, + batch_size: u32, + via_server_name: Option, + ) -> Result<(), ClientError> { + let server = via_server_name.map(ServerName::parse).transpose()?; let mut inner = self.inner.write().await; - inner.search(filter, batch_size).await?; + inner.search(filter, batch_size, server).await?; Ok(()) } + /// Get the number of pages that have been loaded so far. pub async fn loaded_pages(&self) -> Result { let inner = self.inner.read().await; Ok(inner.loaded_pages() as u32) } + /// Get whether the search is at the last page. pub async fn is_at_last_page(&self) -> Result { let inner = self.inner.read().await; Ok(inner.is_at_last_page()) } + /// Registers a callback to receive new search results when starting a + /// search or getting new paginated results. pub async fn results( &self, listener: Box, diff --git a/crates/matrix-sdk/src/room_directory_search.rs b/crates/matrix-sdk/src/room_directory_search.rs index 3ac5893b1bd..8a4746d4dd8 100644 --- a/crates/matrix-sdk/src/room_directory_search.rs +++ b/crates/matrix-sdk/src/room_directory_search.rs @@ -24,7 +24,7 @@ use ruma::{ OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, }; -use crate::{Client, Result}; +use crate::{Client, OwnedServerName, Result}; /// This struct represents a single result of a room directory search. /// @@ -108,7 +108,7 @@ impl SearchState { /// let homeserver = Url::parse("http://localhost:8080")?; /// let client = Client::new(homeserver).await?; /// let mut room_directory_search = RoomDirectorySearch::new(client); -/// room_directory_search.search(None, 10).await?; +/// room_directory_search.search(None, 10, None).await?; /// let (results, mut stream) = room_directory_search.results(); /// room_directory_search.next_page().await?; /// anyhow::Ok(()) @@ -118,6 +118,7 @@ impl SearchState { pub struct RoomDirectorySearch { batch_size: u32, filter: Option, + server: Option, search_state: SearchState, client: Client, results: ObservableVector, @@ -129,6 +130,7 @@ impl RoomDirectorySearch { Self { batch_size: 0, filter: None, + server: None, search_state: Default::default(), client, results: ObservableVector::new(), @@ -138,17 +140,26 @@ impl RoomDirectorySearch { /// Starts a filtered search for the server. /// /// If the `filter` is not provided it will search for all the rooms. - /// You can specify a `batch_size`` to control the number of rooms to fetch + /// You can specify a `batch_size` to control the number of rooms to fetch /// per request. /// + /// If the `via_server` is not provided it will search in the current + /// homeserver by default. + /// /// This method will clear the current search results and start a new one. // Should never be used concurrently with another `next_page` or a // `search`. - pub async fn search(&mut self, filter: Option, batch_size: u32) -> Result<()> { + pub async fn search( + &mut self, + filter: Option, + batch_size: u32, + via_server: Option, + ) -> Result<()> { self.filter = filter; self.batch_size = batch_size; self.search_state = Default::default(); self.results.clear(); + self.server = via_server; self.next_page().await } @@ -165,6 +176,7 @@ impl RoomDirectorySearch { let mut request = PublicRoomsFilterRequest::new(); request.filter = filter; + request.server = self.server.clone(); request.limit = Some(self.batch_size.into()); request.since = self.search_state.next_token().map(ToOwned::to_owned); @@ -196,7 +208,7 @@ impl RoomDirectorySearch { (self.results.len() as f64 / self.batch_size as f64).ceil() as usize } - /// Get whether the search is at the last page + /// Get whether the search is at the last page. pub fn is_at_last_page(&self) -> bool { self.search_state.is_at_end() } @@ -208,7 +220,7 @@ mod tests { use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk_test::{async_test, test_json}; - use ruma::{directory::Filter, serde::Raw, RoomAliasId, RoomId}; + use ruma::{directory::Filter, owned_server_name, serde::Raw, RoomAliasId, RoomId}; use serde_json::Value as JsonValue; use stream_assert::assert_pending; use wiremock::{ @@ -302,7 +314,8 @@ mod tests { .mount(&server) .await; - room_directory_search.search(None, 1).await.unwrap(); + let via_server = owned_server_name!("some.server.org"); + room_directory_search.search(None, 1, Some(via_server)).await.unwrap(); let (results, mut stream) = room_directory_search.results(); assert_pending!(stream); assert_eq!(results.len(), 1); @@ -321,7 +334,7 @@ mod tests { .mount(&server) .await; - room_directory_search.search(None, 1).await.unwrap(); + room_directory_search.search(None, 1, None).await.unwrap(); let (initial_results, mut stream) = room_directory_search.results(); assert_eq!(initial_results, vec![get_first_page_description()].into()); assert!(!room_directory_search.is_at_last_page()); @@ -376,7 +389,7 @@ mod tests { .mount(&server) .await; - room_directory_search.search(None, 1).await.unwrap(); + room_directory_search.search(None, 1, None).await.unwrap(); let (results, mut stream) = room_directory_search.results(); assert_eq!(results, vec![get_first_page_description()].into()); @@ -414,7 +427,7 @@ mod tests { .mount(&server) .await; - room_directory_search.search(Some("bleecker.street".into()), 1).await.unwrap(); + room_directory_search.search(Some("bleecker.street".into()), 1, None).await.unwrap(); let (initial_results, mut stream) = room_directory_search.results(); assert_eq!(initial_results, vec![get_first_page_description()].into()); assert!(!room_directory_search.is_at_last_page()); @@ -450,7 +463,7 @@ mod tests { .mount(&server) .await; - room_directory_search.search(None, 1).await.unwrap(); + room_directory_search.search(None, 1, None).await.unwrap(); let (initial_results, mut stream) = room_directory_search.results(); assert_eq!(initial_results, vec![get_first_page_description()].into()); assert!(!room_directory_search.is_at_last_page()); @@ -465,7 +478,7 @@ mod tests { .mount(&server) .await; - room_directory_search.search(Some("bleecker.street".into()), 1).await.unwrap(); + room_directory_search.search(Some("bleecker.street".into()), 1, None).await.unwrap(); let results_batch: Vec> = stream.next().await.unwrap(); assert_matches!(&results_batch[0], VectorDiff::Clear); diff --git a/testing/matrix-sdk-integration-testing/src/tests/room_directory_search.rs b/testing/matrix-sdk-integration-testing/src/tests/room_directory_search.rs index 40b358a9810..b6c20a665d9 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/room_directory_search.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/room_directory_search.rs @@ -46,7 +46,7 @@ async fn test_room_directory_search_filter() -> Result<()> { let mut room_directory_search = RoomDirectorySearch::new(alice); let (values, mut stream) = room_directory_search.results(); assert!(values.is_empty()); - room_directory_search.search(Some(search_string), 10).await?; + room_directory_search.search(Some(search_string), 10, None).await?; let results_batch: Vec> = stream.next().await.unwrap(); assert_eq!(results_batch.len(), 1); @@ -63,7 +63,7 @@ async fn test_room_directory_search_filter() -> Result<()> { assert_pending!(stream); // This should reset the state completely - room_directory_search.search(None, 25).await?; + room_directory_search.search(None, 25, None).await?; let results_batch = stream.next().await.unwrap(); assert_matches!(&results_batch[0], VectorDiff::Clear); assert_matches!(&results_batch[1], VectorDiff::Append { values } => { assert_eq!(values.len(), 25); }); From c08194aa448bfd3df32f5d547560295e830a4229 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 25 Oct 2024 14:07:31 +0200 Subject: [PATCH 416/979] chore(ffi): introduce `AsyncRuntimeDropped` helper This avoids proliferation of `ManuallyDrop` in the code base, by having a single type that's used for dropping under an async runtime. --- bindings/matrix-sdk-ffi/src/client.rs | 20 ++------- bindings/matrix-sdk-ffi/src/room_list.rs | 11 ++--- bindings/matrix-sdk-ffi/src/room_preview.rs | 23 ++--------- bindings/matrix-sdk-ffi/src/utils.rs | 45 +++++++++++++++++++++ 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index f405a5bf827..18bfd4cc8fb 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1,7 +1,6 @@ use std::{ collections::HashMap, fmt::Debug, - mem::ManuallyDrop, path::Path, sync::{Arc, RwLock}, }; @@ -79,6 +78,7 @@ use crate::{ ruma::AuthData, sync_service::{SyncService, SyncServiceBuilder}, task_handle::TaskHandle, + utils::AsyncRuntimeDropped, ClientError, }; @@ -184,26 +184,12 @@ impl From for TransmissionProgress { #[derive(uniffi::Object)] pub struct Client { - pub(crate) inner: ManuallyDrop, + pub(crate) inner: AsyncRuntimeDropped, delegate: RwLock>>, session_verification_controller: Arc>>, } -impl Drop for Client { - fn drop(&mut self) { - // Dropping the inner OlmMachine must happen within a tokio context - // because deadpool drops sqlite connections in the DB pool on tokio's - // blocking threadpool to avoid blocking async worker threads. - let _guard = RUNTIME.enter(); - // SAFETY: self.inner is never used again, which is the only requirement - // for ManuallyDrop::drop to be used safely. - unsafe { - ManuallyDrop::drop(&mut self.inner); - } - } -} - impl Client { pub async fn new( sdk_client: MatrixClient, @@ -224,7 +210,7 @@ impl Client { }); let client = Client { - inner: ManuallyDrop::new(sdk_client), + inner: AsyncRuntimeDropped::new(sdk_client), delegate: RwLock::new(None), session_verification_controller, }; diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 27bc20dcc34..a1a10e6646a 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -1,12 +1,6 @@ #![allow(deprecated)] -use std::{ - fmt::Debug, - mem::{ManuallyDrop, MaybeUninit}, - ptr::addr_of_mut, - sync::Arc, - time::Duration, -}; +use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration}; use eyeball_im::VectorDiff; use futures_util::{pin_mut, StreamExt, TryFutureExt}; @@ -34,6 +28,7 @@ use crate::{ room_preview::RoomPreview, timeline::{EventTimelineItem, Timeline}, timeline_event_filter::TimelineEventTypeFilter, + utils::AsyncRuntimeDropped, TaskHandle, RUNTIME, }; @@ -635,7 +630,7 @@ impl RoomListItem { let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?; - Ok(Arc::new(RoomPreview::new(ManuallyDrop::new(client), room_preview))) + Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview))) } /// Build a full `Room` FFI object, filling its associated timeline. diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 08d3b626573..505081fc526 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -1,33 +1,16 @@ -use std::mem::ManuallyDrop; - use anyhow::Context as _; -use async_compat::TOKIO1 as RUNTIME; use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client}; use ruma::space::SpaceRoomJoinRule; use tracing::warn; -use crate::{client::JoinRule, error::ClientError, room::Membership}; +use crate::{client::JoinRule, error::ClientError, room::Membership, utils::AsyncRuntimeDropped}; /// A room preview for a room. It's intended to be used to represent rooms that /// aren't joined yet. #[derive(uniffi::Object)] pub struct RoomPreview { inner: SdkRoomPreview, - client: ManuallyDrop, -} - -impl Drop for RoomPreview { - fn drop(&mut self) { - // Dropping the inner OlmMachine must happen within a tokio context - // because deadpool drops sqlite connections in the DB pool on tokio's - // blocking threadpool to avoid blocking async worker threads. - let _guard = RUNTIME.enter(); - // SAFETY: self.client is never used again, which is the only requirement - // for ManuallyDrop::drop to be used safely. - unsafe { - ManuallyDrop::drop(&mut self.client); - } - } + client: AsyncRuntimeDropped, } #[matrix_sdk_ffi_macros::export] @@ -65,7 +48,7 @@ impl RoomPreview { } impl RoomPreview { - pub(crate) fn new(client: ManuallyDrop, inner: SdkRoomPreview) -> Self { + pub(crate) fn new(client: AsyncRuntimeDropped, inner: SdkRoomPreview) -> Self { Self { client, inner } } } diff --git a/bindings/matrix-sdk-ffi/src/utils.rs b/bindings/matrix-sdk-ffi/src/utils.rs index 06fe82fc468..8868573e344 100644 --- a/bindings/matrix-sdk-ffi/src/utils.rs +++ b/bindings/matrix-sdk-ffi/src/utils.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::{mem::ManuallyDrop, ops::Deref}; + +use async_compat::TOKIO1 as RUNTIME; use ruma::UInt; use tracing::warn; @@ -21,3 +24,45 @@ pub(crate) fn u64_to_uint(u: u64) -> UInt { UInt::MAX }) } + +/// Tiny wrappers for data types that must be dropped in the context of an async +/// runtime. +/// +/// This is useful whenever such a data type may transitively call some +/// runtime's `block_on` function in their `Drop` impl (since we lack async drop +/// at the moment), like done in some `deadpool` drop impls. +pub(crate) struct AsyncRuntimeDropped(ManuallyDrop); + +impl AsyncRuntimeDropped { + /// Create a new wrapper for this type that will be dropped under an async + /// runtime. + pub fn new(val: T) -> Self { + Self(ManuallyDrop::new(val)) + } +} + +impl Drop for AsyncRuntimeDropped { + fn drop(&mut self) { + let _guard = RUNTIME.enter(); + // SAFETY: self.inner is never used again, which is the only requirement + // for ManuallyDrop::drop to be used safely. + unsafe { + ManuallyDrop::drop(&mut self.0); + } + } +} + +// What is an `AsyncRuntimeDropped`, if not a `T` in disguise? +impl Deref for AsyncRuntimeDropped { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Clone for AsyncRuntimeDropped { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} From 5717eb1722b155ff06e1eaeaefdfbee362cf3eb2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 4 Nov 2024 15:12:18 +0100 Subject: [PATCH 417/979] chore(ui): Display the real error of `Error::EventCache` (#4207) This patch displays the wrapped error. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 0fd2ba165f3..34cd89686ad 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -432,7 +432,7 @@ impl RoomListService { #[derive(Debug, Error)] pub enum Error { /// Error from [`matrix_sdk::SlidingSync`]. - #[error("SlidingSync failed: {0}")] + #[error(transparent)] SlidingSync(SlidingSyncError), /// An operation has been requested on an unknown list. @@ -446,10 +446,10 @@ pub enum Error { #[error("A timeline instance already exists for room {0}")] TimelineAlreadyExists(OwnedRoomId), - #[error("An error occurred while initializing the timeline")] - InitializingTimeline(#[source] timeline::Error), + #[error(transparent)] + InitializingTimeline(#[from] timeline::Error), - #[error("The attached event cache ran into an error")] + #[error(transparent)] EventCache(#[from] EventCacheError), } From 4002136cfb67293555271adf7a26cfa0fbdaa258 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 4 Nov 2024 15:00:16 +0100 Subject: [PATCH 418/979] feat(ui): Remove `RoomListService::new_with_encryption`. This patch removes `RoomListService::new_with_encryption`. This feature is not used, not useful since it's best to use `EncryptionSyncService`, and it can be racy depending on how it's used. To avoid potential errors and bugs, it's preferable to remove this code. --- .../src/room_list_service/mod.rs | 44 ++----------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 34cd89686ad..57b832e9b43 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -120,21 +120,10 @@ impl RoomListService { /// already pre-configured. /// /// This won't start an encryption sync, and it's the user's responsibility - /// to create one in this case using `EncryptionSync`. + /// to create one in this case using + /// [`EncryptionSyncService`][crate::encryption_sync_service::EncryptionSyncService]. pub async fn new(client: Client) -> Result { - Self::new_internal(client, false).await - } - - /// Create a new `RoomList` that enables encryption. - /// - /// This will include syncing the encryption information, so there must not - /// be any instance of `EncryptionSync` running in the background. - pub async fn new_with_encryption(client: Client) -> Result { - Self::new_internal(client, true).await - } - - async fn new_internal(client: Client, with_encryption: bool) -> Result { - let mut builder = client + let builder = client .sliding_sync("room-list") .map_err(Error::SlidingSync)? .with_account_data_extension( @@ -148,16 +137,6 @@ impl RoomListService { enabled: Some(true), })); - if with_encryption { - builder = builder - .with_e2ee_extension( - assign!(http::request::E2EE::default(), { enabled: Some(true) }), - ) - .with_to_device_extension( - assign!(http::request::ToDevice::default(), { enabled: Some(true) }), - ); - } - let sliding_sync = builder .add_cached_list( SlidingSyncList::builder(ALL_ROOMS_LIST_NAME) @@ -577,23 +556,6 @@ mod tests { Ok(()) } - #[async_test] - async fn test_no_to_device_and_e2ee_if_not_explicitly_set() -> Result<(), Error> { - let (client, _) = new_client().await; - - let no_encryption = RoomListService::new(client.clone()).await?; - let extensions = no_encryption.sliding_sync.extensions_config(); - assert_eq!(extensions.e2ee.enabled, None); - assert_eq!(extensions.to_device.enabled, None); - - let with_encryption = RoomListService::new_with_encryption(client).await?; - let extensions = with_encryption.sliding_sync.extensions_config(); - assert_eq!(extensions.e2ee.enabled, Some(true)); - assert_eq!(extensions.to_device.enabled, Some(true)); - - Ok(()) - } - #[async_test] async fn test_expire_sliding_sync_session_manually() -> Result<(), Error> { let (client, server) = new_client().await; From 494532d579ce51e99ccd5f1b96ee8e7004c884a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:41:44 +0000 Subject: [PATCH 419/979] chore(deps): bump crate-ci/typos from 1.26.8 to 1.27.0 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.26.8 to 1.27.0. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.26.8...v1.27.0) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fbe6964e6d..2de47b34852 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,7 +304,7 @@ jobs: uses: actions/checkout@v4 - name: Check the spelling of the files in our repo - uses: crate-ci/typos@v1.26.8 + uses: crate-ci/typos@v1.27.0 clippy: name: Run clippy From ee252437d1af6842bbbaa560c1ed3ffcf1dbbd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 4 Nov 2024 13:26:25 +0100 Subject: [PATCH 420/979] fix(pinned_events): get pinned event ids from the HS if the sync doesn't contain it This should take care of a bug that caused pinned events to be incorrectly removed when the new pinned event ids list was based on an empty one if the required state of the room didn't contain any pinned events info --- benchmarks/benches/room_bench.rs | 10 ++++--- bindings/matrix-sdk-ffi/src/room_info.rs | 3 +- crates/matrix-sdk-base/src/rooms/normal.rs | 6 ++-- .../matrix-sdk-base/src/sliding_sync/mod.rs | 6 ++-- crates/matrix-sdk-ui/src/timeline/mod.rs | 28 +++++++++++++++---- .../src/timeline/pinned_events_loader.rs | 5 ++-- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 2 +- .../tests/integration/timeline/mod.rs | 27 ++++++++++++++---- crates/matrix-sdk/src/room/mod.rs | 28 +++++++++++++++++++ crates/matrix-sdk/tests/integration/client.rs | 2 +- 10 files changed, 90 insertions(+), 27 deletions(-) diff --git a/benchmarks/benches/room_bench.rs b/benchmarks/benches/room_bench.rs index 5b010b6ee31..2283cc955ab 100644 --- a/benchmarks/benches/room_bench.rs +++ b/benchmarks/benches/room_bench.rs @@ -171,8 +171,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) { ); let room = client.get_room(&room_id).expect("Room not found"); - assert!(!room.pinned_event_ids().is_empty()); - assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT); + let pinned_event_ids = room.pinned_event_ids().unwrap_or_default(); + assert!(!pinned_event_ids.is_empty()); + assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT); let count = PINNED_EVENTS_COUNT; let name = format!("{count} pinned events"); @@ -191,8 +192,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) { group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| { b.to_async(&runtime).iter(|| async { - assert!(!room.pinned_event_ids().is_empty()); - assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT); + let pinned_event_ids = room.pinned_event_ids().unwrap_or_default(); + assert!(!pinned_event_ids.is_empty()); + assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT); // Reset cache so it always loads the events from the mocked endpoint client.event_cache().empty_immutable_cache().await; diff --git a/bindings/matrix-sdk-ffi/src/room_info.rs b/bindings/matrix-sdk-ffi/src/room_info.rs index 3f04eed962b..96de331fd10 100644 --- a/bindings/matrix-sdk-ffi/src/room_info.rs +++ b/bindings/matrix-sdk-ffi/src/room_info.rs @@ -67,7 +67,8 @@ impl RoomInfo { for (id, level) in power_levels_map.iter() { user_power_levels.insert(id.to_string(), *level); } - let pinned_event_ids = room.pinned_event_ids().iter().map(|id| id.to_string()).collect(); + let pinned_event_ids = + room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect(); Ok(Self { id: room.room_id().to_string(), diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index cfe1625b7af..7181d4d4824 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1019,7 +1019,7 @@ impl Room { } /// Returns the current pinned event ids for this room. - pub fn pinned_event_ids(&self) -> Vec { + pub fn pinned_event_ids(&self) -> Option> { self.inner.read().pinned_event_ids() } } @@ -1596,8 +1596,8 @@ impl RoomInfo { } /// Returns the current pinned event ids for this room. - pub fn pinned_event_ids(&self) -> Vec { - self.base_info.pinned_events.clone().map(|c| c.pinned).unwrap_or_default() + pub fn pinned_event_ids(&self) -> Option> { + self.base_info.pinned_events.clone().map(|c| c.pinned) } /// Checks if an `EventId` is currently pinned. diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index f613d177b6e..9350492a098 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -2509,7 +2509,7 @@ mod tests { // The newly created room has no pinned event ids let room = client.get_room(room_id).unwrap(); let pinned_event_ids = room.pinned_event_ids(); - assert!(pinned_event_ids.is_empty()); + assert_matches!(pinned_event_ids, None); // Load new pinned event id let mut room_response = http::response::Room::new(); @@ -2522,7 +2522,7 @@ mod tests { let response = response_with_room(room_id, room_response); client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); - let pinned_event_ids = room.pinned_event_ids(); + let pinned_event_ids = room.pinned_event_ids().unwrap_or_default(); assert_eq!(pinned_event_ids.len(), 1); assert_eq!(pinned_event_ids[0], pinned_event_id); @@ -2536,7 +2536,7 @@ mod tests { )); let response = response_with_room(room_id, room_response); client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); - let pinned_event_ids = room.pinned_event_ids(); + let pinned_event_ids = room.pinned_event_ids().unwrap(); assert!(pinned_event_ids.is_empty()); } diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 93168e1732a..b5bd98b10c2 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -729,10 +729,18 @@ impl Timeline { /// Adds a new pinned event by sending an updated `m.room.pinned_events` /// event containing the new event id. /// - /// Returns `true` if we sent the request, `false` if the event was already + /// This method will first try to get the pinned events from the current + /// room's state and if it fails to do so it'll try to load them from the + /// homeserver. + /// + /// Returns `true` if we pinned the event, `false` if the event was already /// pinned. pub async fn pin_event(&self, event_id: &EventId) -> Result { - let mut pinned_event_ids = self.room().pinned_event_ids(); + let mut pinned_event_ids = if let Some(event_ids) = self.room().pinned_event_ids() { + event_ids + } else { + self.room().load_pinned_events().await?.unwrap_or_default() + }; let event_id = event_id.to_owned(); if pinned_event_ids.contains(&event_id) { Ok(false) @@ -744,13 +752,21 @@ impl Timeline { } } - /// Adds a new pinned event by sending an updated `m.room.pinned_events` + /// Removes a pinned event by sending an updated `m.room.pinned_events` /// event without the event id we want to remove. /// - /// Returns `true` if we sent the request, `false` if the event wasn't - /// pinned. + /// This method will first try to get the pinned events from the current + /// room's state and if it fails to do so it'll try to load them from the + /// homeserver. + /// + /// Returns `true` if we unpinned the event, `false` if the event wasn't + /// pinned before. pub async fn unpin_event(&self, event_id: &EventId) -> Result { - let mut pinned_event_ids = self.room().pinned_event_ids(); + let mut pinned_event_ids = if let Some(event_ids) = self.room().pinned_event_ids() { + event_ids + } else { + self.room().load_pinned_events().await?.unwrap_or_default() + }; let event_id = event_id.to_owned(); if let Some(idx) = pinned_event_ids.iter().position(|e| *e == *event_id) { pinned_event_ids.remove(idx); diff --git a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs index 450ccae0e94..1deb853607d 100644 --- a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs +++ b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs @@ -61,6 +61,7 @@ impl PinnedEventsLoader { let pinned_event_ids: Vec = self .room .pinned_event_ids() + .unwrap_or_default() .into_iter() .rev() .take(self.max_events_to_load) @@ -134,7 +135,7 @@ pub trait PinnedEventsRoom: SendOutsideWasm + SyncOutsideWasm { ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>>; /// Get the pinned event ids for a room. - fn pinned_event_ids(&self) -> Vec; + fn pinned_event_ids(&self) -> Option>; /// Checks whether an event id is pinned in this room. /// @@ -168,7 +169,7 @@ impl PinnedEventsRoom for Room { .boxed() } - fn pinned_event_ids(&self) -> Vec { + fn pinned_event_ids(&self) -> Option> { self.clone_info().pinned_event_ids() } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 26cadd3b6dc..356a2ad2228 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -351,7 +351,7 @@ impl PinnedEventsRoom for TestRoomDataProvider { unimplemented!(); } - fn pinned_event_ids(&self) -> Vec { + fn pinned_event_ids(&self) -> Option> { unimplemented!(); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index e9d28a84e3a..b6bc4211140 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -628,7 +628,7 @@ async fn test_pin_event_is_sent_successfully() { // Pinning a remote event succeeds. setup - .mock_response(ResponseTemplate::new(200).set_body_json(json!({ + .mock_pin_unpin_response(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$42" }))) .await; @@ -662,7 +662,7 @@ async fn test_pin_event_is_returning_an_error() { assert!(!timeline.items().await.is_empty()); // Pinning a remote event fails. - setup.mock_response(ResponseTemplate::new(400)).await; + setup.mock_pin_unpin_response(ResponseTemplate::new(400)).await; let event_id = setup.event_id(); assert!(timeline.pin_event(event_id).await.is_err()); @@ -680,7 +680,7 @@ async fn test_unpin_event_is_sent_successfully() { // Unpinning a remote event succeeds. setup - .mock_response(ResponseTemplate::new(200).set_body_json(json!({ + .mock_pin_unpin_response(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$42" }))) .await; @@ -714,7 +714,7 @@ async fn test_unpin_event_is_returning_an_error() { assert!(!timeline.items().await.is_empty()); // Unpinning a remote event fails. - setup.mock_response(ResponseTemplate::new(400)).await; + setup.mock_pin_unpin_response(ResponseTemplate::new(400)).await; let event_id = setup.event_id(); assert!(timeline.unpin_event(event_id).await.is_err()); @@ -834,7 +834,13 @@ impl PinningTestSetup<'_> { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - Self { event_id, room_id, client, server, sync_settings, sync_builder } + let setup = Self { event_id, room_id, client, server, sync_settings, sync_builder }; + + // This is necessary to get an empty list of pinned events when there are no + // pinned events state event in the required state + setup.mock_get_empty_pinned_events_state_response().await; + + setup } async fn timeline(&self) -> Timeline { @@ -847,7 +853,7 @@ impl PinningTestSetup<'_> { self.server.reset().await; } - async fn mock_response(&self, response: ResponseTemplate) { + async fn mock_pin_unpin_response(&self, response: ResponseTemplate) { Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.pinned_events/.*?")) .and(header("authorization", "Bearer 1234")) @@ -856,6 +862,15 @@ impl PinningTestSetup<'_> { .await; } + async fn mock_get_empty_pinned_events_state_response(&self) { + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.pinned_events/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(404).set_body_json(json!({}))) + .mount(&self.server) + .await; + } + async fn mock_sync(&mut self, is_using_pinned_state_event: bool) { let f = EventFactory::new().sender(user_id!("@a:b.c")); let mut joined_room_builder = JoinedRoomBuilder::new(self.room_id) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 0be442f9167..f974b2ec769 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -30,6 +30,7 @@ use futures_util::{ future::{try_join, try_join_all}, stream::FuturesUnordered, }; +use http::StatusCode; #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] pub use identity_status_changes::IdentityStatusChanges; #[cfg(feature = "e2e-encryption")] @@ -91,6 +92,7 @@ use ruma::{ VideoMessageEventContent, }, name::RoomNameEventContent, + pinned_events::RoomPinnedEventsEventContent, power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, server_acl::RoomServerAclEventContent, topic::RoomTopicEventContent, @@ -3130,6 +3132,32 @@ impl Room { .await?; Ok(()) } + + /// Load pinned state events for a room from the `/state` endpoint in the + /// home server. + pub async fn load_pinned_events(&self) -> Result>> { + let response = self + .client + .send( + get_state_events_for_key::v3::Request::new( + self.room_id().to_owned(), + StateEventType::RoomPinnedEvents, + "".to_owned(), + ), + None, + ) + .await; + + match response { + Ok(response) => { + Ok(Some(response.content.deserialize_as::()?.pinned)) + } + Err(http_error) => match http_error.as_client_api_error() { + Some(error) if error.status_code == StatusCode::NOT_FOUND => Ok(None), + _ => Err(http_error.into()), + }, + } + } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 6e044c28567..589087296a5 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -1388,5 +1388,5 @@ async fn test_restore_room() { let room = client.get_room(room_id).unwrap(); assert!(room.is_favourite()); - assert!(!room.pinned_event_ids().is_empty()); + assert!(!room.pinned_event_ids().unwrap().is_empty()); } From 7f7b996d240308f59ea363bcb17d45960cea1d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 31 Oct 2024 09:05:09 +0100 Subject: [PATCH 421/979] refactor(ffi): modify `Client::resolve_room_alias` function Breaking: `ffi::Client::resolve_room_alias` now returns `Result, ClientError>` instead of `Result`. This allows the client to match the 3 possible cases: - The room alias exists. - The room alias does not exist. - The function failed internally. --- bindings/matrix-sdk-ffi/src/client.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 18bfd4cc8fb..e822bae7e7a 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1027,10 +1027,18 @@ impl Client { pub async fn resolve_room_alias( &self, room_alias: String, - ) -> Result { + ) -> Result, ClientError> { let room_alias = RoomAliasId::parse(&room_alias)?; let response = self.inner.resolve_room_alias(&room_alias).await?; Ok(response.into()) + match self.inner.resolve_room_alias(&room_alias).await { + Ok(response) => Ok(Some(response.into())), + Err(HttpError::Reqwest(http_error)) => match http_error.status() { + Some(StatusCode::NOT_FOUND) => Ok(None), + _ => Err(http_error.into()), + }, + Err(error) => Err(error.into()), + } } /// Given a room id, get the preview of a room, to interact with it. From 6828f9372008af9a5d650354f2883d9215fb8568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 30 Oct 2024 16:07:59 +0100 Subject: [PATCH 422/979] feat(ffi): add `Client::is_room_alias_available` function --- bindings/matrix-sdk-ffi/src/client.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index e822bae7e7a..ea37febad8f 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -22,6 +22,7 @@ use matrix_sdk::{ }, OidcAuthorizationData, OidcSession, }, + reqwest::StatusCode, ruma::{ api::client::{ media::get_content_thumbnail::v3::Method, @@ -40,7 +41,7 @@ use matrix_sdk::{ EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId, }, sliding_sync::Version as SdkSlidingSyncVersion, - AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens, + AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens, }; use matrix_sdk_ui::notification_client::{ NotificationClient as MatrixNotificationClient, @@ -1029,8 +1030,6 @@ impl Client { room_alias: String, ) -> Result, ClientError> { let room_alias = RoomAliasId::parse(&room_alias)?; - let response = self.inner.resolve_room_alias(&room_alias).await?; - Ok(response.into()) match self.inner.resolve_room_alias(&room_alias).await { Ok(response) => Ok(Some(response.into())), Err(HttpError::Reqwest(http_error)) => match http_error.status() { @@ -1041,6 +1040,11 @@ impl Client { } } + /// Checks if a room alias exists in the current homeserver. + pub async fn room_alias_exists(&self, room_alias: String) -> Result { + self.resolve_room_alias(room_alias).await.map(|ret| ret.is_some()) + } + /// Given a room id, get the preview of a room, to interact with it. /// /// The list of `via_servers` must be a list of servers that know @@ -1124,6 +1128,23 @@ impl Client { Ok(()) } + + /// Checks if a room alias is available in the current homeserver. + pub async fn is_room_alias_available(&self, alias: String) -> Result { + let alias = RoomAliasId::parse(alias)?; + match self.inner.resolve_room_alias(&alias).await { + // The room alias was resolved, so it's already in use. + Ok(_) => Ok(false), + Err(HttpError::Reqwest(error)) => { + match error.status() { + // The room alias wasn't found, so it's available. + Some(StatusCode::NOT_FOUND) => Ok(true), + _ => Err(HttpError::Reqwest(error).into()), + } + } + Err(error) => Err(error.into()), + } + } } #[matrix_sdk_ffi_macros::export(callback_interface)] From 06e6cba1561c421f9c69b0ff59bff12959029c72 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 29 Oct 2024 17:59:41 +0100 Subject: [PATCH 423/979] chore(event cache store): Support multiple parent key types for dependent requests This makes it possible to have different kinds of *parent key*, to update a dependent request. A dependent request waits for the parent key to be set, before it can be acted upon; before, it could only be an event id, because a dependent request would only wait for an event to be sent. In a soon future, we're going to support uploading medias as requests, and some subsequent requests will depend on this, but won't be able to rely on an event id (since an upload doesn't return an event/event id). Since this changes the format of `DependentQueuedRequest`, which is directly serialized into the state stores, I've also cleared the table, to not have to migrate the data in there. Dependent requests are supposed to be transient anyways, so it would be a bug if they were many of them in the queue. Since a migration was needed anyways, I've also removed the `rename` annotations (that supported a previous format) for the `DependentQueuedRequestKind` enum. --- .../src/store/integration_tests.rs | 18 ++++-- .../matrix-sdk-base/src/store/memory_store.rs | 8 +-- crates/matrix-sdk-base/src/store/mod.rs | 2 +- .../matrix-sdk-base/src/store/send_queue.rs | 26 +++++--- crates/matrix-sdk-base/src/store/traits.rs | 19 +++--- .../src/state_store/migrations.rs | 22 ++++++- .../src/state_store/mod.rs | 10 ++-- .../migrations/state_store/008_send_queue.sql | 7 +++ crates/matrix-sdk-sqlite/src/state_store.rs | 26 +++++--- crates/matrix-sdk/src/send_queue.rs | 59 ++++++++++++++----- 10 files changed, 143 insertions(+), 54 deletions(-) create mode 100644 crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index d83eef9793e..a49f4d79492 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -33,7 +33,9 @@ use ruma::{ }; use serde_json::{json, value::Value as JsonValue}; -use super::{DependentQueuedRequestKind, DynStateStore, ServerCapabilities}; +use super::{ + send_queue::SentRequestKey, DependentQueuedRequestKind, DynStateStore, ServerCapabilities, +}; use crate::{ deserialized_responses::MemberEvent, store::{ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, StateStoreExt}, @@ -1384,13 +1386,19 @@ impl StateStoreIntegrationTests for DynStateStore { assert_eq!(dependents.len(), 1); assert_eq!(dependents[0].parent_transaction_id, txn0); assert_eq!(dependents[0].own_transaction_id, child_txn); - assert!(dependents[0].event_id.is_none()); + assert!(dependents[0].parent_key.is_none()); assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent); // Update the event id. let event_id = owned_event_id!("$1"); - let num_updated = - self.update_dependent_queued_request(room_id, &txn0, event_id.clone()).await.unwrap(); + let num_updated = self + .update_dependent_queued_request( + room_id, + &txn0, + SentRequestKey::Event(event_id.clone()), + ) + .await + .unwrap(); assert_eq!(num_updated, 1); // It worked. @@ -1398,7 +1406,7 @@ impl StateStoreIntegrationTests for DynStateStore { assert_eq!(dependents.len(), 1); assert_eq!(dependents[0].parent_transaction_id, txn0); assert_eq!(dependents[0].own_transaction_id, child_txn); - assert_eq!(dependents[0].event_id.as_ref(), Some(&event_id)); + assert_eq!(dependents[0].parent_key.as_ref(), Some(&SentRequestKey::Event(event_id))); assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent); // Now remove it. diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index e55ec14caf3..aaeda6e3d9d 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -36,7 +36,7 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use super::{ - send_queue::{ChildTransactionId, QueuedRequest, SerializableEventContent}, + send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey, SerializableEventContent}, traits::{ComposerDraft, ServerCapabilities}, DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo, StateChanges, StateStore, StoreError, @@ -907,7 +907,7 @@ impl StateStore for MemoryStore { kind: content, parent_transaction_id: parent_transaction_id.to_owned(), own_transaction_id, - event_id: None, + parent_key: None, }, ); Ok(()) @@ -917,13 +917,13 @@ impl StateStore for MemoryStore { &self, room: &RoomId, parent_txn_id: &TransactionId, - event_id: OwnedEventId, + sent_parent_key: SentRequestKey, ) -> Result { let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap(); let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default(); let mut num_updated = 0; for d in dependents.iter_mut().filter(|item| item.parent_transaction_id == parent_txn_id) { - d.event_id = Some(event_id.clone()); + d.parent_key = Some(sent_parent_key.clone()); num_updated += 1; } Ok(num_updated) diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 392dd6e5bb8..cd4b7c67cd1 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -76,7 +76,7 @@ pub use self::{ memory_store::MemoryStore, send_queue::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, - QueuedRequest, QueuedRequestKind, SerializableEventContent, + QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent, }, traits::{ ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities, diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 9a70c579641..6a695c62959 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -150,18 +150,15 @@ pub enum QueueWedgeError { #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DependentQueuedRequestKind { /// The event should be edited. - #[serde(rename = "Edit")] EditEvent { /// The new event for the content. new_content: SerializableEventContent, }, /// The event should be redacted/aborted/removed. - #[serde(rename = "Redact")] RedactEvent, /// The event should be reacted to, with the given key. - #[serde(rename = "React")] ReactEvent { /// Key used for the reaction. key: String, @@ -207,6 +204,23 @@ impl From for OwnedTransactionId { } } +/// A unique key (identifier) indicating that a transaction has been +/// successfully sent to the server. +/// +/// The owning child transactions can now be resolved. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum SentRequestKey { + /// The parent transaction returned an event when it succeeded. + Event(OwnedEventId), +} + +impl SentRequestKey { + /// Converts the current parent key into an event id, if possible. + pub fn into_event_id(self) -> Option { + as_variant!(self, Self::Event) + } +} + /// A request to be sent, depending on a [`QueuedRequest`] to be sent first. /// /// Depending on whether the parent request has been sent or not, this will @@ -231,11 +245,7 @@ pub struct DependentQueuedRequest { /// If the parent request has been sent, the parent's request identifier /// returned by the server once the local echo has been sent out. - /// - /// Note: this is the event id used for the depended-on event after it's - /// been sent, not for a possible event that could have been sent - /// because of this [`DependentQueuedRequest`]. - pub event_id: Option, + pub parent_key: Option, } #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index e3ef3c9de59..e1bfd517432 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -41,8 +41,9 @@ use ruma::{ use serde::{Deserialize, Serialize}; use super::{ - ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, - QueuedRequest, SerializableEventContent, StateChanges, StoreError, + send_queue::SentRequestKey, ChildTransactionId, DependentQueuedRequest, + DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, SerializableEventContent, + StateChanges, StoreError, }; use crate::{ deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState}, @@ -416,15 +417,19 @@ pub trait StateStore: AsyncTraitDeps { content: DependentQueuedRequestKind, ) -> Result<(), Self::Error>; - /// Update a set of dependent send queue requests with an event id, - /// effectively marking them as ready. + /// Update a set of dependent send queue requests with a key identifying the + /// homeserver's response, effectively marking them as ready. + /// + /// ⚠ Beware! There's no verification applied that the parent key type is + /// compatible with the dependent event type. The invalid state may be + /// lazily filtered out in `load_dependent_queued_requests`. /// /// Returns the number of updated requests. async fn update_dependent_queued_request( &self, room_id: &RoomId, parent_txn_id: &TransactionId, - event_id: OwnedEventId, + sent_parent_key: SentRequestKey, ) -> Result; /// Remove a specific dependent send queue request by id. @@ -697,10 +702,10 @@ impl StateStore for EraseStateStoreError { &self, room_id: &RoomId, parent_txn_id: &TransactionId, - event_id: OwnedEventId, + sent_parent_key: SentRequestKey, ) -> Result { self.0 - .update_dependent_queued_request(room_id, parent_txn_id, event_id) + .update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key) .await .map_err(Into::into) } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index 98b55daea22..8e559cfea9d 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -46,7 +46,7 @@ use super::{ }; use crate::IndexeddbStateStoreError; -const CURRENT_DB_VERSION: u32 = 11; +const CURRENT_DB_VERSION: u32 = 12; const CURRENT_META_DB_VERSION: u32 = 2; /// Sometimes Migrations can't proceed without having to drop existing @@ -235,6 +235,9 @@ pub async fn upgrade_inner_db( if old_version < 11 { db = migrate_to_v11(db).await?; } + if old_version < 12 { + db = migrate_to_v12(db).await?; + } } db.close(); @@ -771,6 +774,23 @@ async fn migrate_to_v11(db: IdbDatabase) -> Result { apply_migration(db, 11, migration).await } +/// Drop entries from the [`keys::DEPENDENT_SEND_QUEUE`] table. +async fn migrate_to_v12(db: IdbDatabase) -> Result { + let tx = + db.transaction_on_one_with_mode(keys::DEPENDENT_SEND_QUEUE, IdbTransactionMode::Readwrite)?; + + let store = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?; + store.clear()?; + + tx.await.into_result()?; + + let name = db.name(); + db.close(); + + // Update the version of the database. + Ok(IdbDatabase::open_u32(&name, 12)?.await?) +} + #[cfg(all(test, target_arch = "wasm32"))] mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 3dd7fab7a6b..f8ac92af8f0 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -26,8 +26,8 @@ use matrix_sdk_base::{ deserialized_responses::RawAnySyncOrStrippedState, store::{ ChildTransactionId, ComposerDraft, DependentQueuedRequest, DependentQueuedRequestKind, - QueuedRequest, QueuedRequestKind, SerializableEventContent, ServerCapabilities, - StateChanges, StateStore, StoreError, + QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent, + ServerCapabilities, StateChanges, StateStore, StoreError, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, StateStoreDataKey, StateStoreDataValue, }; @@ -1548,7 +1548,7 @@ impl_state_store!({ kind: content, parent_transaction_id: parent_txn_id.to_owned(), own_transaction_id: own_txn_id, - event_id: None, + parent_key: None, }); // Save the new vector into db. @@ -1563,7 +1563,7 @@ impl_state_store!({ &self, room_id: &RoomId, parent_txn_id: &TransactionId, - event_id: OwnedEventId, + parent_key: SentRequestKey, ) -> Result { let encoded_key = self.encode_key(keys::DEPENDENT_SEND_QUEUE, room_id); @@ -1586,7 +1586,7 @@ impl_state_store!({ // Modify all requests that match. let mut num_updated = 0; for entry in prev.iter_mut().filter(|entry| entry.parent_transaction_id == parent_txn_id) { - entry.event_id = Some(event_id.clone()); + entry.parent_key = Some(parent_key.clone()); num_updated += 1; } diff --git a/crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql b/crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql new file mode 100644 index 00000000000..d1bec740d87 --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql @@ -0,0 +1,7 @@ +-- Delete all previous entries in the dependent send queue table, because the format changed. +DELETE FROM "dependent_send_queue_events"; + +-- Rename its "event_id" column to "parent_key", while we're at it. +ALTER TABLE "dependent_send_queue_events" + RENAME COLUMN "event_id" + TO "parent_key"; diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 86f2b95d525..7c951d4ffdf 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -13,7 +13,7 @@ use matrix_sdk_base::{ store::{ migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, - SerializableEventContent, + SentRequestKey, SerializableEventContent, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, @@ -69,7 +69,7 @@ mod keys { /// This is used to figure whether the sqlite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in /// the [`SqliteStateStore::run_migrations`] function.. -const DATABASE_VERSION: u8 = 8; +const DATABASE_VERSION: u8 = 9; /// A sqlite based cryptostore. #[derive(Clone)] @@ -297,6 +297,16 @@ impl SqliteStateStore { }) .await?; } + + if from < 9 && to >= 9 { + conn.with_transaction(move |txn| { + // Run the migration. + txn.execute_batch(include_str!("../migrations/state_store/008_send_queue.sql"))?; + txn.set_db_version(9) + }) + .await?; + } + Ok(()) } @@ -1854,10 +1864,10 @@ impl StateStore for SqliteStateStore { &self, room_id: &RoomId, parent_txn_id: &TransactionId, - event_id: OwnedEventId, + parent_key: SentRequestKey, ) -> Result { let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id); - let event_id = self.serialize_value(&event_id)?; + let parent_key = self.serialize_value(&parent_key)?; // See comment in `save_send_queue_event`. let parent_txn_id = parent_txn_id.to_string(); @@ -1866,9 +1876,9 @@ impl StateStore for SqliteStateStore { .await? .with_transaction(move |txn| { Ok(txn.prepare_cached( - "UPDATE dependent_send_queue_events SET event_id = ? WHERE parent_transaction_id = ? and room_id = ?", + "UPDATE dependent_send_queue_events SET parent_key = ? WHERE parent_transaction_id = ? and room_id = ?", )? - .execute((event_id, parent_txn_id, room_id))?) + .execute((parent_key, parent_txn_id, room_id))?) }) .await } @@ -1908,7 +1918,7 @@ impl StateStore for SqliteStateStore { .acquire() .await? .prepare( - "SELECT own_transaction_id, parent_transaction_id, event_id, content FROM dependent_send_queue_events WHERE room_id = ? ORDER BY ROWID", + "SELECT own_transaction_id, parent_transaction_id, parent_key, content FROM dependent_send_queue_events WHERE room_id = ? ORDER BY ROWID", |mut stmt| { stmt.query((room_id,))? .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))) @@ -1922,7 +1932,7 @@ impl StateStore for SqliteStateStore { dependent_events.push(DependentQueuedRequest { own_transaction_id: entry.0.into(), parent_transaction_id: entry.1.into(), - event_id: entry.2.map(|bytes| self.deserialize_value(&bytes)).transpose()?, + parent_key: entry.2.map(|bytes| self.deserialize_value(&bytes)).transpose()?, kind: self.deserialize_json(&entry.3)?, }); } diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index fcee608eed2..1c570c1cdbf 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -54,7 +54,7 @@ use std::{ use matrix_sdk_base::{ store::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, - QueuedRequest, QueuedRequestKind, SerializableEventContent, + QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent, }, RoomState, StoreError, }; @@ -772,7 +772,11 @@ impl QueueStorage { // Update all dependent requests. store - .update_dependent_queued_request(&self.room_id, transaction_id, event_id.to_owned()) + .update_dependent_queued_request( + &self.room_id, + transaction_id, + SentRequestKey::Event(event_id.to_owned()), + ) .await?; let removed = store.remove_send_queue_request(&self.room_id, transaction_id).await?; @@ -953,9 +957,17 @@ impl QueueStorage { ) -> Result { let store = client.store(); + let parent_key = de.parent_key; + match de.kind { DependentQueuedRequestKind::EditEvent { new_content } => { - if let Some(event_id) = de.event_id { + if let Some(parent_key) = parent_key { + let Some(event_id) = parent_key.into_event_id() else { + return Err(RoomSendQueueError::StorageError( + RoomSendQueueStorageError::InvalidParentKey, + )); + }; + // The parent event has been sent, so send an edit event. let room = client .get_room(&self.room_id) @@ -1030,7 +1042,13 @@ impl QueueStorage { } DependentQueuedRequestKind::RedactEvent => { - if let Some(event_id) = de.event_id { + if let Some(parent_key) = parent_key { + let Some(event_id) = parent_key.into_event_id() else { + return Err(RoomSendQueueError::StorageError( + RoomSendQueueStorageError::InvalidParentKey, + )); + }; + // The parent event has been sent; send a redaction. let room = client .get_room(&self.room_id) @@ -1064,10 +1082,16 @@ impl QueueStorage { } DependentQueuedRequestKind::ReactEvent { key } => { - if let Some(event_id) = de.event_id { + if let Some(parent_key) = parent_key { + let Some(parent_event_id) = parent_key.into_event_id() else { + return Err(RoomSendQueueError::StorageError( + RoomSendQueueStorageError::InvalidParentKey, + )); + }; + // Queue the reaction event in the send queue 🧠. let react_event = - ReactionEventContent::new(Annotation::new(event_id, key)).into(); + ReactionEventContent::new(Annotation::new(parent_event_id, key)).into(); let serializable = SerializableEventContent::from_raw( Raw::new(&react_event) .map_err(RoomSendQueueStorageError::JsonSerialization)?, @@ -1303,6 +1327,11 @@ pub enum RoomSendQueueStorageError { #[error(transparent)] JsonSerialization(#[from] serde_json::Error), + /// A parent key was expected to be of a certain type, and it was another + /// type instead. + #[error("a dependent event had an invalid parent key type")] + InvalidParentKey, + /// The client is shutting down. #[error("The client is shutting down.")] ClientShuttingDown, @@ -1599,14 +1628,14 @@ mod tests { ) .unwrap(), }, - event_id: None, + parent_key: None, }; let res = canonicalize_dependent_requests(&[edit]); assert_eq!(res.len(), 1); assert_matches!(&res[0].kind, DependentQueuedRequestKind::EditEvent { .. }); assert_eq!(res[0].parent_transaction_id, txn); - assert!(res[0].event_id.is_none()); + assert!(res[0].parent_key.is_none()); } #[test] @@ -1619,7 +1648,7 @@ mod tests { own_transaction_id: ChildTransactionId::new(), parent_transaction_id: txn.clone(), kind: DependentQueuedRequestKind::RedactEvent, - event_id: None, + parent_key: None, }; let edit = DependentQueuedRequest { @@ -1631,7 +1660,7 @@ mod tests { ) .unwrap(), }, - event_id: None, + parent_key: None, }; inputs.push({ @@ -1670,7 +1699,7 @@ mod tests { ) .unwrap(), }, - event_id: None, + parent_key: None, }) .collect::>(); @@ -1701,7 +1730,7 @@ mod tests { own_transaction_id: child1.clone(), kind: DependentQueuedRequestKind::RedactEvent, parent_transaction_id: txn1.clone(), - event_id: None, + parent_key: None, }, // This one pertains to txn2. DependentQueuedRequest { @@ -1713,7 +1742,7 @@ mod tests { .unwrap(), }, parent_transaction_id: txn2.clone(), - event_id: None, + parent_key: None, }, ]; @@ -1743,7 +1772,7 @@ mod tests { own_transaction_id: react_id.clone(), kind: DependentQueuedRequestKind::ReactEvent { key: "🧠".to_owned() }, parent_transaction_id: txn.clone(), - event_id: None, + parent_key: None, }; let edit_id = ChildTransactionId::new(); @@ -1756,7 +1785,7 @@ mod tests { .unwrap(), }, parent_transaction_id: txn, - event_id: None, + parent_key: None, }; let res = canonicalize_dependent_requests(&[react, edit]); From c2a921cb584a3a762c86591d70f65c083ba08020 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 29 Oct 2024 19:24:27 +0100 Subject: [PATCH 424/979] chore(send queue): move sending of an event to an helper function --- crates/matrix-sdk/src/send_queue.rs | 65 ++++++++++++++++------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 1c570c1cdbf..b34853d13d6 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -65,7 +65,7 @@ use ruma::{ EventContent as _, }, serde::Raw, - EventId, OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, + OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, }; use tokio::sync::{broadcast, Notify, RwLock}; use tracing::{debug, error, info, instrument, trace, warn}; @@ -464,32 +464,24 @@ impl RoomSendQueue { continue; }; - let (event, event_type) = match &queued_request.kind { - QueuedRequestKind::Event { content } => content.raw(), - }; - - match room - .send_raw(event_type, event) - .with_transaction_id(&queued_request.transaction_id) - .with_request_config(RequestConfig::short_retry()) - .await - { - Ok(res) => { - trace!(txn_id = %queued_request.transaction_id, event_id = %res.event_id, "successfully sent"); - - match queue.mark_as_sent(&queued_request.transaction_id, &res.event_id).await { - Ok(()) => { + match Self::handle_request(&room, &queued_request).await { + Ok(parent_key) => match queue + .mark_as_sent(&queued_request.transaction_id, parent_key.clone()) + .await + { + Ok(()) => match parent_key { + SentRequestKey::Event(event_id) => { let _ = updates.send(RoomSendQueueUpdate::SentEvent { transaction_id: queued_request.transaction_id, - event_id: res.event_id, + event_id, }); } + }, - Err(err) => { - warn!("unable to mark queued event as sent: {err}"); - } + Err(err) => { + warn!("unable to mark queued request as sent: {err}"); } - } + }, Err(err) => { let is_recoverable = match err { @@ -563,6 +555,27 @@ impl RoomSendQueue { info!("exited sending task"); } + /// Handles a single request and returns the [`SentRequestKey`] on success. + async fn handle_request( + room: &Room, + request: &QueuedRequest, + ) -> Result { + match &request.kind { + QueuedRequestKind::Event { content } => { + let (event, event_type) = content.raw(); + + let res = room + .send_raw(event_type, event) + .with_transaction_id(&request.transaction_id) + .with_request_config(RequestConfig::short_retry()) + .await?; + + trace!(txn_id = %request.transaction_id, event_id = %res.event_id, "event successfully sent"); + Ok(SentRequestKey::Event(res.event_id)) + } + } + } + /// Returns whether the room is enabled, at the room level. pub fn is_enabled(&self) -> bool { self.inner.locally_enabled.load(Ordering::SeqCst) @@ -761,7 +774,7 @@ impl QueueStorage { async fn mark_as_sent( &self, transaction_id: &TransactionId, - event_id: &EventId, + parent_key: SentRequestKey, ) -> Result<(), RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; @@ -771,13 +784,7 @@ impl QueueStorage { let store = client.store(); // Update all dependent requests. - store - .update_dependent_queued_request( - &self.room_id, - transaction_id, - SentRequestKey::Event(event_id.to_owned()), - ) - .await?; + store.update_dependent_queued_request(&self.room_id, transaction_id, parent_key).await?; let removed = store.remove_send_queue_request(&self.room_id, transaction_id).await?; From 1f2e8c5007b54c17b98a035016592a6e66aff2d9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 29 Oct 2024 19:56:29 +0100 Subject: [PATCH 425/979] refactor!(event cache store): store the serialized `QueuedRequestKind`, not a raw event Changelog: The send queue will now store a serialized `QueuedRequestKind` instead of a raw event, which breaks the format. As a result, all send queues have been emptied. --- .../src/store/integration_tests.rs | 14 ++++++------- .../matrix-sdk-base/src/store/memory_store.rs | 21 +++++++++---------- .../matrix-sdk-base/src/store/send_queue.rs | 6 ++++++ crates/matrix-sdk-base/src/store/traits.rs | 12 +++++------ .../src/state_store/migrations.rs | 13 +++++++----- .../src/state_store/mod.rs | 8 +++---- .../migrations/state_store/008_send_queue.sql | 3 +++ crates/matrix-sdk-sqlite/src/state_store.rs | 8 +++---- crates/matrix-sdk/src/send_queue.rs | 14 ++++++------- 9 files changed, 55 insertions(+), 44 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index a49f4d79492..131f919c2c4 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1212,7 +1212,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into()) .unwrap(); - self.save_send_queue_request(room_id, txn0.clone(), event0).await.unwrap(); + self.save_send_queue_request(room_id, txn0.clone(), event0.into()).await.unwrap(); // Reading it will work. let pending = self.load_send_queue_requests(room_id).await.unwrap(); @@ -1236,7 +1236,7 @@ impl StateStoreIntegrationTests for DynStateStore { ) .unwrap(); - self.save_send_queue_request(room_id, txn, event).await.unwrap(); + self.save_send_queue_request(room_id, txn, event.into()).await.unwrap(); } // Reading all the events should work. @@ -1286,7 +1286,7 @@ impl StateStoreIntegrationTests for DynStateStore { &RoomMessageEventContent::text_plain("wow that's a cool test").into(), ) .unwrap(); - self.update_send_queue_request(room_id, txn2, event0).await.unwrap(); + self.update_send_queue_request(room_id, txn2, event0.into()).await.unwrap(); // And it is reflected. let pending = self.load_send_queue_requests(room_id).await.unwrap(); @@ -1334,7 +1334,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into()) .unwrap(); - self.save_send_queue_request(room_id2, txn.clone(), event).await.unwrap(); + self.save_send_queue_request(room_id2, txn.clone(), event.into()).await.unwrap(); } // Add and remove one event for room3. @@ -1344,7 +1344,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into()) .unwrap(); - self.save_send_queue_request(room_id3, txn.clone(), event).await.unwrap(); + self.save_send_queue_request(room_id3, txn.clone(), event.into()).await.unwrap(); self.remove_send_queue_request(room_id3, &txn).await.unwrap(); } @@ -1365,7 +1365,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into()) .unwrap(); - self.save_send_queue_request(room_id, txn0.clone(), event0).await.unwrap(); + self.save_send_queue_request(room_id, txn0.clone(), event0.into()).await.unwrap(); // No dependents, to start with. assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty()); @@ -1425,7 +1425,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event1 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into()) .unwrap(); - self.save_send_queue_request(room_id, txn1.clone(), event1).await.unwrap(); + self.save_send_queue_request(room_id, txn1.clone(), event1.into()).await.unwrap(); self.save_dependent_queued_request( room_id, diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index aaeda6e3d9d..2ba91d6f200 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -36,7 +36,7 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use super::{ - send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey, SerializableEventContent}, + send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey}, traits::{ComposerDraft, ServerCapabilities}, DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo, StateChanges, StateStore, StoreError, @@ -806,15 +806,14 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, - content: SerializableEventContent, + kind: QueuedRequestKind, ) -> Result<(), Self::Error> { - self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().push( - QueuedRequest { - kind: QueuedRequestKind::Event { content }, - transaction_id, - error: None, - }, - ); + self.send_queue_events + .write() + .unwrap() + .entry(room_id.to_owned()) + .or_default() + .push(QueuedRequest { kind, transaction_id, error: None }); Ok(()) } @@ -822,7 +821,7 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, transaction_id: &TransactionId, - content: SerializableEventContent, + kind: QueuedRequestKind, ) -> Result { if let Some(entry) = self .send_queue_events @@ -833,7 +832,7 @@ impl StateStore for MemoryStore { .iter_mut() .find(|item| item.transaction_id == transaction_id) { - entry.kind = QueuedRequestKind::Event { content }; + entry.kind = kind; entry.error = None; Ok(true) } else { diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 6a695c62959..7efe0f13633 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -78,6 +78,12 @@ pub enum QueuedRequestKind { }, } +impl From for QueuedRequestKind { + fn from(content: SerializableEventContent) -> Self { + Self::Event { content } + } +} + /// A request to be sent with a send queue. #[derive(Clone)] pub struct QueuedRequest { diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index e1bfd517432..4dabd9eefac 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -42,8 +42,8 @@ use serde::{Deserialize, Serialize}; use super::{ send_queue::SentRequestKey, ChildTransactionId, DependentQueuedRequest, - DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, SerializableEventContent, - StateChanges, StoreError, + DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, StateChanges, + StoreError, }; use crate::{ deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState}, @@ -357,7 +357,7 @@ pub trait StateStore: AsyncTraitDeps { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, - content: SerializableEventContent, + request: QueuedRequestKind, ) -> Result<(), Self::Error>; /// Updates a send queue request with the given content, and resets its @@ -375,7 +375,7 @@ pub trait StateStore: AsyncTraitDeps { &self, room_id: &RoomId, transaction_id: &TransactionId, - content: SerializableEventContent, + content: QueuedRequestKind, ) -> Result; /// Remove a request previously inserted with @@ -640,7 +640,7 @@ impl StateStore for EraseStateStoreError { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, - content: SerializableEventContent, + content: QueuedRequestKind, ) -> Result<(), Self::Error> { self.0.save_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into) } @@ -649,7 +649,7 @@ impl StateStore for EraseStateStoreError { &self, room_id: &RoomId, transaction_id: &TransactionId, - content: SerializableEventContent, + content: QueuedRequestKind, ) -> Result { self.0.update_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index 8e559cfea9d..5d86dcf61a6 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -774,13 +774,16 @@ async fn migrate_to_v11(db: IdbDatabase) -> Result { apply_migration(db, 11, migration).await } -/// Drop entries from the [`keys::DEPENDENT_SEND_QUEUE`] table. +/// The format of data serialized into the send queue and dependent send queue +/// tables have changed, clear both. async fn migrate_to_v12(db: IdbDatabase) -> Result { - let tx = - db.transaction_on_one_with_mode(keys::DEPENDENT_SEND_QUEUE, IdbTransactionMode::Readwrite)?; + let store_keys = &[keys::DEPENDENT_SEND_QUEUE, keys::ROOM_SEND_QUEUE]; + let tx = db.transaction_on_multi_with_mode(store_keys, IdbTransactionMode::Readwrite)?; - let store = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?; - store.clear()?; + for store_name in store_keys { + let store = tx.object_store(store_name)?; + store.clear()?; + } tx.await.into_result()?; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index f8ac92af8f0..39156c70a25 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -1328,7 +1328,7 @@ impl_state_store!({ &self, room_id: &RoomId, transaction_id: OwnedTransactionId, - content: SerializableEventContent, + kind: QueuedRequestKind, ) -> Result<()> { let encoded_key = self.encode_key(keys::ROOM_SEND_QUEUE, room_id); @@ -1352,7 +1352,7 @@ impl_state_store!({ // Push the new request. prev.push(PersistedQueuedRequest { room_id: room_id.to_owned(), - kind: Some(QueuedRequestKind::Event { content }), + kind: Some(kind), transaction_id, error: None, is_wedged: None, @@ -1371,7 +1371,7 @@ impl_state_store!({ &self, room_id: &RoomId, transaction_id: &TransactionId, - content: SerializableEventContent, + kind: QueuedRequestKind, ) -> Result { let encoded_key = self.encode_key(keys::ROOM_SEND_QUEUE, room_id); @@ -1394,7 +1394,7 @@ impl_state_store!({ // Modify the one request. if let Some(entry) = prev.iter_mut().find(|entry| entry.transaction_id == transaction_id) { - entry.kind = Some(QueuedRequestKind::Event { content }); + entry.kind = Some(kind); // Reset the error state. entry.error = None; // Remove migrated fields. diff --git a/crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql b/crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql index d1bec740d87..f6afcbe54af 100644 --- a/crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql +++ b/crates/matrix-sdk-sqlite/migrations/state_store/008_send_queue.sql @@ -5,3 +5,6 @@ DELETE FROM "dependent_send_queue_events"; ALTER TABLE "dependent_send_queue_events" RENAME COLUMN "event_id" TO "parent_key"; + +--- Delete all previous entries in the send queue, since the content's format has changed. +DELETE FROM "send_queue_events"; diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 7c951d4ffdf..23570681a96 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -13,7 +13,7 @@ use matrix_sdk_base::{ store::{ migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, - SentRequestKey, SerializableEventContent, + SentRequestKey, }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, @@ -1684,7 +1684,7 @@ impl StateStore for SqliteStateStore { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, - content: SerializableEventContent, + content: QueuedRequestKind, ) -> Result<(), Self::Error> { let room_id_key = self.encode_key(keys::SEND_QUEUE, room_id); let room_id_value = self.serialize_value(&room_id.to_owned())?; @@ -1709,7 +1709,7 @@ impl StateStore for SqliteStateStore { &self, room_id: &RoomId, transaction_id: &TransactionId, - content: SerializableEventContent, + content: QueuedRequestKind, ) -> Result { let room_id = self.encode_key(keys::SEND_QUEUE, room_id); @@ -1778,7 +1778,7 @@ impl StateStore for SqliteStateStore { for entry in res { requests.push(QueuedRequest { transaction_id: entry.0.into(), - kind: QueuedRequestKind::Event { content: self.deserialize_json(&entry.1)? }, + kind: self.deserialize_json(&entry.1)?, error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?, }); } diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index b34853d13d6..01c50eddd73 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -345,7 +345,7 @@ impl RoomSendQueue { let content = SerializableEventContent::from_raw(content, event_type); - let transaction_id = self.inner.queue.push(content.clone()).await?; + let transaction_id = self.inner.queue.push(content.clone().into()).await?; trace!(%transaction_id, "manager sends a raw event to the background task"); self.inner.notifier.notify_one(); @@ -698,13 +698,13 @@ impl QueueStorage { /// Returns the transaction id chosen to identify the request. async fn push( &self, - serializable: SerializableEventContent, + request: QueuedRequestKind, ) -> Result { let transaction_id = TransactionId::new(); self.client()? .store() - .save_send_queue_request(&self.room_id, transaction_id.clone(), serializable) + .save_send_queue_request(&self.room_id, transaction_id.clone(), request) .await?; Ok(transaction_id) @@ -861,7 +861,7 @@ impl QueueStorage { let edited = self .client()? .store() - .update_send_queue_request(&self.room_id, transaction_id, serializable) + .update_send_queue_request(&self.room_id, transaction_id, serializable.into()) .await?; Ok(edited) @@ -1027,7 +1027,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, de.own_transaction_id.into(), - serializable, + serializable.into(), ) .await .map_err(RoomSendQueueStorageError::StorageError)?; @@ -1037,7 +1037,7 @@ impl QueueStorage { .update_send_queue_request( &self.room_id, &de.parent_transaction_id, - new_content, + new_content.into(), ) .await .map_err(RoomSendQueueStorageError::StorageError)?; @@ -1109,7 +1109,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, de.own_transaction_id.into(), - serializable, + serializable.into(), ) .await .map_err(RoomSendQueueStorageError::StorageError)?; From a739ddfc8433f6d317b78547eff078f67f00d668 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 29 Oct 2024 19:57:41 +0100 Subject: [PATCH 426/979] chore(event cache store): update test to reflect that previous events and dependent events are cleared Because the latest migration would clear events to-be-sent from the send queue, we need to reflect this in this test. --- crates/matrix-sdk-sqlite/src/state_store.rs | 46 +++++++++------------ 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 23570681a96..4f453577651 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -2010,9 +2010,8 @@ mod migration_tests { }, }; - use assert_matches::assert_matches; use matrix_sdk_base::{ - store::{QueueWedgeError, SerializableEventContent}, + store::{ChildTransactionId, DependentQueuedRequestKind, SerializableEventContent}, sync::UnreadNotificationsCount, RoomState, StateStore, }; @@ -2273,10 +2272,10 @@ mod migration_tests { } #[async_test] - pub async fn test_migrating_v7_to_v8() { + pub async fn test_migrating_v7_to_v9() { let path = new_path(); - let room_a_id = room_id!("!room_a:dummy.local"); + let room_id = room_id!("!room_a:dummy.local"); let wedged_event_transaction_id = TransactionId::new(); let local_event_transaction_id = TransactionId::new(); @@ -2288,38 +2287,33 @@ mod migration_tests { let wedge_tx = wedged_event_transaction_id.clone(); let local_tx = local_event_transaction_id.clone(); - conn.with_transaction(move |txn| { - add_send_queue_event_v7(&db, txn, &wedge_tx, room_a_id, true)?; - add_send_queue_event_v7(&db, txn, &local_tx, room_a_id, false)?; + db.save_dependent_queued_request( + room_id, + &local_tx, + ChildTransactionId::new(), + DependentQueuedRequestKind::RedactEvent, + ) + .await + .unwrap(); + conn.with_transaction(move |txn| { + add_send_queue_event_v7(&db, txn, &wedge_tx, room_id, true)?; + add_send_queue_event_v7(&db, txn, &local_tx, room_id, false)?; Result::<_, Error>::Ok(()) }) .await .unwrap(); } - // This transparently migrates to the latest version. + // This transparently migrates to the latest version, which clears up all + // requests and dependent requests. let store = SqliteStateStore::open(path, Some(SECRET)).await.unwrap(); - let requests = store.load_send_queue_requests(room_a_id).await.unwrap(); - - assert_eq!(requests.len(), 2); - - let migrated_wedged = - requests.iter().find(|e| e.transaction_id == wedged_event_transaction_id).unwrap(); - assert!(migrated_wedged.is_wedged()); - assert_matches!( - migrated_wedged.error.clone(), - Some(QueueWedgeError::GenericApiError { .. }) - ); - - let migrated_ok = requests - .iter() - .find(|e| e.transaction_id == local_event_transaction_id.clone()) - .unwrap(); + let requests = store.load_send_queue_requests(room_id).await.unwrap(); + assert!(requests.is_empty()); - assert!(!migrated_ok.is_wedged()); - assert!(migrated_ok.error.is_none()); + let dependent_requests = store.load_dependent_queued_requests(room_id).await.unwrap(); + assert!(dependent_requests.is_empty()); } fn add_send_queue_event_v7( From 7a422fe1262d2bc8e7cf797464595a8441f7a30d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 4 Nov 2024 17:27:09 +0100 Subject: [PATCH 427/979] chore(send queue): rename `de` to `dependent_request` --- crates/matrix-sdk/src/send_queue.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 01c50eddd73..6a330844f0d 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -960,13 +960,13 @@ impl QueueStorage { async fn try_apply_single_dependent_request( &self, client: &Client, - de: DependentQueuedRequest, + dependent_request: DependentQueuedRequest, ) -> Result { let store = client.store(); - let parent_key = de.parent_key; + let parent_key = dependent_request.parent_key; - match de.kind { + match dependent_request.kind { DependentQueuedRequestKind::EditEvent { new_content } => { if let Some(parent_key) = parent_key { let Some(event_id) = parent_key.into_event_id() else { @@ -1026,7 +1026,7 @@ impl QueueStorage { store .save_send_queue_request( &self.room_id, - de.own_transaction_id.into(), + dependent_request.own_transaction_id.into(), serializable.into(), ) .await @@ -1036,7 +1036,7 @@ impl QueueStorage { let edited = store .update_send_queue_request( &self.room_id, - &de.parent_transaction_id, + &dependent_request.parent_transaction_id, new_content.into(), ) .await @@ -1068,8 +1068,9 @@ impl QueueStorage { // Note: no reason is provided because we materialize the intent of "cancel // sending the parent event". - if let Err(err) = - room.redact(&event_id, None, Some(de.own_transaction_id.into())).await + if let Err(err) = room + .redact(&event_id, None, Some(dependent_request.own_transaction_id.into())) + .await { warn!("error when sending a redact for {event_id}: {err}"); return Ok(false); @@ -1078,7 +1079,10 @@ impl QueueStorage { // The parent event is still local (sending must have failed); redact the local // echo. let removed = store - .remove_send_queue_request(&self.room_id, &de.parent_transaction_id) + .remove_send_queue_request( + &self.room_id, + &dependent_request.parent_transaction_id, + ) .await .map_err(RoomSendQueueStorageError::StorageError)?; @@ -1108,7 +1112,7 @@ impl QueueStorage { store .save_send_queue_request( &self.room_id, - de.own_transaction_id.into(), + dependent_request.own_transaction_id.into(), serializable.into(), ) .await From 478dc0ea90b6e34d127d1db1a3b7e288b0fe8f49 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 30 Oct 2024 15:34:02 +0100 Subject: [PATCH 428/979] chore(base): refactor internal helpers related to media Notably, make it super clear what parameters are required to create the attachment type, since the function doesn't consume the whole `AttachmentConfig` for realz. --- crates/matrix-sdk/src/encryption/mod.rs | 13 ++-- crates/matrix-sdk/src/media.rs | 13 ++-- crates/matrix-sdk/src/room/mod.rs | 79 +++++++++++++++---------- 3 files changed, 59 insertions(+), 46 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index b59c3e96d0a..a6ecc1f7754 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -459,7 +459,7 @@ impl Client { data: &[u8], thumbnail: Option, send_progress: SharedObservable, - ) -> Result<(MediaSource, Option, Option>)> { + ) -> Result<(MediaSource, Option<(MediaSource, Box)>)> { let upload_thumbnail = self.upload_encrypted_thumbnail(thumbnail, content_type, send_progress.clone()); @@ -470,10 +470,9 @@ impl Client { .await }; - let ((thumbnail_source, thumbnail_info), file) = - try_join(upload_thumbnail, upload_attachment).await?; + let (thumbnail, file) = try_join(upload_thumbnail, upload_attachment).await?; - Ok((MediaSource::Encrypted(Box::new(file)), thumbnail_source, thumbnail_info)) + Ok((MediaSource::Encrypted(Box::new(file)), thumbnail)) } /// Uploads an encrypted thumbnail to the media repository, and returns @@ -483,9 +482,9 @@ impl Client { thumbnail: Option, content_type: &mime::Mime, send_progress: SharedObservable, - ) -> Result<(Option, Option>)> { + ) -> Result)>> { let Some(thumbnail) = thumbnail else { - return Ok((None, None)); + return Ok(None); }; let mut cursor = Cursor::new(thumbnail.data); @@ -501,7 +500,7 @@ impl Client { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) }); - Ok((Some(MediaSource::Encrypted(Box::new(file))), Some(Box::new(thumbnail_info)))) + Ok(Some((MediaSource::Encrypted(Box::new(file)), Box::new(thumbnail_info)))) } /// Claim one-time keys creating new Olm sessions. diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index c4553dba731..a25466e0a98 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -613,7 +613,7 @@ impl Media { data: Vec, thumbnail: Option, send_progress: SharedObservable, - ) -> Result<(MediaSource, Option, Option>)> { + ) -> Result<(MediaSource, Option<(MediaSource, Box)>)> { let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone()); let upload_attachment = async move { @@ -623,10 +623,9 @@ impl Media { .map_err(Error::from) }; - let ((thumbnail_source, thumbnail_info), response) = - try_join(upload_thumbnail, upload_attachment).await?; + let (thumbnail, response) = try_join(upload_thumbnail, upload_attachment).await?; - Ok((MediaSource::Plain(response.content_uri), thumbnail_source, thumbnail_info)) + Ok((MediaSource::Plain(response.content_uri), thumbnail)) } /// Uploads an unencrypted thumbnail to the media repository, and returns @@ -635,9 +634,9 @@ impl Media { &self, thumbnail: Option, send_progress: SharedObservable, - ) -> Result<(Option, Option>)> { + ) -> Result)>> { let Some(thumbnail) = thumbnail else { - return Ok((None, None)); + return Ok(None); }; let response = self @@ -654,6 +653,6 @@ impl Media { { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } ); - Ok((Some(MediaSource::Plain(url)), Some(Box::new(thumbnail_info)))) + Ok(Some((MediaSource::Plain(url), Box::new(thumbnail_info)))) } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index f974b2ec769..667b77e6e40 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -87,7 +87,7 @@ use ruma::{ history_visibility::HistoryVisibility, message::{ AudioInfo, AudioMessageEventContent, FileInfo, FileMessageEventContent, - ImageMessageEventContent, MessageType, RoomMessageEventContent, + FormattedBody, ImageMessageEventContent, MessageType, RoomMessageEventContent, UnstableAudioDetailsContentBlock, UnstableVoiceContentBlock, VideoInfo, VideoMessageEventContent, }, @@ -1962,7 +1962,7 @@ impl Room { }; #[cfg(feature = "e2e-encryption")] - let (media_source, thumbnail_source, thumbnail_info) = if self.is_encrypted().await? { + let (media_source, thumbnail) = if self.is_encrypted().await? { self.client .upload_encrypted_media_and_thumbnail(content_type, &data, thumbnail, send_progress) .await? @@ -1981,7 +1981,7 @@ impl Room { }; #[cfg(not(feature = "e2e-encryption"))] - let (media_source, thumbnail_source, thumbnail_info) = self + let (media_source, thumbnail) = self .client .media() .upload_plain_media_and_thumbnail(content_type, data.clone(), thumbnail, send_progress) @@ -2000,7 +2000,7 @@ impl Room { } if let Some(((data, height, width), source)) = - thumbnail_cache_info.zip(thumbnail_source.as_ref()) + thumbnail_cache_info.zip(thumbnail.as_ref().map(|tuple| &tuple.0)) { debug!("caching the thumbnail"); @@ -2024,21 +2024,19 @@ impl Room { } } - let msg_type = self.make_attachment_message( - content_type, - media_source, - thumbnail_source, - thumbnail_info, - filename, - config, + let content = Self::make_attachment_event( + self.make_attachment_type( + content_type, + filename, + media_source, + config.caption, + config.formatted_caption, + config.info, + thumbnail, + ), + mentions, ); - let mut content = RoomMessageEventContent::new(msg_type); - - if let Some(mentions) = mentions { - content = content.add_mentions(mentions); - } - let mut fut = self.send(content); if let Some(txn_id) = txn_id { fut = fut.with_transaction_id(txn_id); @@ -2048,33 +2046,37 @@ impl Room { /// Creates the inner [`MessageType`] for an already-uploaded media file /// provided by its source. - fn make_attachment_message( + #[allow(clippy::too_many_arguments)] + fn make_attachment_type( &self, content_type: &Mime, - source: MediaSource, - thumbnail_source: Option, - thumbnail_info: Option>, filename: &str, - config: AttachmentConfig, + source: MediaSource, + caption: Option, + formatted_caption: Option, + info: Option, + thumbnail: Option<(MediaSource, Box)>, ) -> MessageType { - // if config.caption is set, use it as body, and filename as the file name - // otherwise, body is the filename, and the filename is not set. + // If caption is set, use it as body, and filename as the file name; otherwise, + // body is the filename, and the filename is not set. // https://github.com/tulir/matrix-spec-proposals/blob/body-as-caption/proposals/2530-body-as-caption.md - let (body, filename) = match config.caption { + let (body, filename) = match caption { Some(caption) => (caption, Some(filename.to_owned())), None => (filename.to_owned(), None), }; + let (thumbnail_source, thumbnail_info) = thumbnail.unzip(); + match content_type.type_() { mime::IMAGE => { - let info = assign!(config.info.map(ImageInfo::from).unwrap_or_default(), { + let info = assign!(info.map(ImageInfo::from).unwrap_or_default(), { mimetype: Some(content_type.as_ref().to_owned()), thumbnail_source, thumbnail_info }); let content = assign!(ImageMessageEventContent::new(body, source), { info: Some(Box::new(info)), - formatted: config.formatted_caption, + formatted: formatted_caption, filename }); MessageType::Image(content) @@ -2084,7 +2086,7 @@ impl Room { let mut content = AudioMessageEventContent::new(body, source); if let Some(AttachmentInfo::Voice { audio_info, waveform: Some(waveform_vec) }) = - &config.info + &info { if let Some(duration) = audio_info.duration { let waveform = waveform_vec.iter().map(|v| (*v).into()).collect(); @@ -2094,7 +2096,7 @@ impl Room { content.voice = Some(UnstableVoiceContentBlock::new()); } - let mut audio_info = config.info.map(AudioInfo::from).unwrap_or_default(); + let mut audio_info = info.map(AudioInfo::from).unwrap_or_default(); audio_info.mimetype = Some(content_type.as_ref().to_owned()); let content = content.info(Box::new(audio_info)); @@ -2102,21 +2104,21 @@ impl Room { } mime::VIDEO => { - let info = assign!(config.info.map(VideoInfo::from).unwrap_or_default(), { + let info = assign!(info.map(VideoInfo::from).unwrap_or_default(), { mimetype: Some(content_type.as_ref().to_owned()), thumbnail_source, thumbnail_info }); let content = assign!(VideoMessageEventContent::new(body, source), { info: Some(Box::new(info)), - formatted: config.formatted_caption, + formatted: formatted_caption, filename }); MessageType::Video(content) } _ => { - let info = assign!(config.info.map(FileInfo::from).unwrap_or_default(), { + let info = assign!(info.map(FileInfo::from).unwrap_or_default(), { mimetype: Some(content_type.as_ref().to_owned()), thumbnail_source, thumbnail_info @@ -2129,6 +2131,19 @@ impl Room { } } + /// Creates the [`RoomMessageEventContent`] based on the message type and + /// mentions. + fn make_attachment_event( + msg_type: MessageType, + mentions: Option, + ) -> RoomMessageEventContent { + let mut content = RoomMessageEventContent::new(msg_type); + if let Some(mentions) = mentions { + content = content.add_mentions(mentions); + } + content + } + /// Update the power levels of a select set of users of this room. /// /// Issue a `power_levels` state event request to the server, changing the From 590c2dd9fd44662795be339b72ad54b4e1b8640a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 4 Nov 2024 15:47:59 +0100 Subject: [PATCH 429/979] fix(timeline): update responses after a successful decryption Fixes #4196. --- .../src/timeline/event_handler.rs | 28 +++- .../src/timeline/tests/encryption.rs | 130 +++++++++++++++++- 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 228eb96e606..c482a1a43b1 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -1190,8 +1190,34 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } - Flow::Remote { position: TimelineItemPosition::Update(idx), .. } => { + Flow::Remote { + event_id: decrypted_event_id, + position: TimelineItemPosition::Update(idx), + .. + } => { trace!("Updating timeline item at position {idx}"); + + // Update all events that replied to this previously encrypted message. + self.items.for_each(|mut entry| { + let Some(event_item) = entry.as_event() else { return }; + let Some(message) = event_item.content.as_message() else { return }; + let Some(in_reply_to) = message.in_reply_to() else { return }; + if *decrypted_event_id == in_reply_to.event_id { + trace!(reply_event_id = ?event_item.identifier(), "Updating response to edited event"); + let in_reply_to = InReplyToDetails { + event_id: in_reply_to.event_id.clone(), + event: TimelineDetails::Ready(Box::new( + RepliedToEvent::from_timeline_item(&item), + )), + }; + let new_reply_content = + TimelineItemContent::Message(message.with_in_reply_to(in_reply_to)); + let new_reply_item = + entry.with_kind(event_item.with_content(new_reply_content, None)); + ObservableVectorTransactionEntry::set(&mut entry, new_reply_item); + } + }); + let internal_id = self.items[*idx].internal_id.clone(); self.items.set(*idx, TimelineItem::new(item, internal_id)); } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index 0bf9d80d4ce..d7e0f812d6f 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -20,17 +20,19 @@ use std::{ sync::{Arc, Mutex}, }; +use as_variant::as_variant; use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use matrix_sdk::{ + assert_next_matches_with_timeout, crypto::{decrypt_room_key_export, types::events::UtdCause, OlmMachine}, test_utils::test_client_builder, }; use matrix_sdk_base::deserialized_responses::{SyncTimelineEvent, UnableToDecryptReason}; -use matrix_sdk_test::{async_test, BOB}; +use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::{ - assign, + assign, event_id, events::room::encrypted::{ EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, RoomEncryptedEventContent, @@ -44,7 +46,7 @@ use stream_assert::assert_next_matches; use super::TestTimeline; use crate::{ - timeline::{EncryptedMessage, TimelineItemContent}, + timeline::{EncryptedMessage, TimelineDetails, TimelineItemContent}, unable_to_decrypt_hook::{UnableToDecryptHook, UnableToDecryptInfo, UtdHookManager}, }; @@ -557,6 +559,128 @@ async fn test_utd_cause_for_missing_membership_is_unknown() { assert_eq!(*cause, UtdCause::Unknown); } +#[async_test] +async fn test_retry_decryption_updates_response() { + const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU"; + const SESSION_KEY: &[u8] = b"\ + -----BEGIN MEGOLM SESSION DATA-----\n\ + ASKcWoiAVUM97482UAi83Avce62hSLce7i5JhsqoF6xeAAAACqt2Cg3nyJPRWTTMXxXH7TXnkfdlmBXbQtq5\ + bpHo3LRijcq2Gc6TXilESCmJN14pIsfKRJrWjZ0squ/XsoTFytuVLWwkNaW3QF6obeg2IoVtJXLMPdw3b2vO\ + vgwGY3OMP0XafH13j1vcb6YLzvgLkZQLnYvd47hv3yK/9GmKS9tokuaQ7dCVYckYcIOS09EDTs70YdxUd5WG\ + rQynATCLFP1p/NAGv70r9MK7Cy/mNpjD0r4qC7UEDIoi1kOWzHgnLo19wtvwsb8Fg8ATxcs3Wmtj8hIUYpDx\ + ia4sM10zbytUuaPUAfCDf42IyxdmOnGe1CueXhgI71y+RW0s0argNqUt7jB70JT0o9CyX6UBGRaqLk2MPY9T\ + hUu5J8X3UgIa6rcbWigzohzWm9rdbEHFrSWqjpfQYMaAKQQgETrjSy4XTrp2RhC2oNqG/hylI4ab+F4X6fpH\ + DYP1NqNMP5g36xNu7LhDnrUB5qsPjYOmWORxGLfudpF3oLYCSlr3DgHqEIB6HjQblLZ3KQuPBse3zxyROTnS\ + AhdPH4a/z1wioFtKNVph3hecsiKEdqnz4Y2coSIdhz58mJ9JWNQoFAENE5CSsoEZAGvafYZVpW4C75YY2zq1\ + wIeiFi1dT43/jLAUGkslsi1VvnyfUu8qO404RxYO3XHoGLMFoFLOO+lZ+VGci2Vz10AhxJhEBHxRKxw4k2uB\ + HztoSJUr/2Y\n\ + -----END MEGOLM SESSION DATA-----"; + + let timeline = TestTimeline::new(); + let mut stream = timeline.subscribe_events().await; + + let original_event_id = event_id!("$original"); + let f = &timeline.factory; + timeline + .handle_live_event( + f.event(RoomEncryptedEventContent::new( + EncryptedEventScheme::MegolmV1AesSha2( + MegolmV1AesSha2ContentInit { + ciphertext: "\ + AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\ + cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\ + YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\ + CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\ + hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\ + QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ" + .to_owned(), + sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA".to_owned(), + device_id: "NLAZCWIOCO".into(), + session_id: SESSION_ID.into(), + } + .into(), + ), + None, + )) + .event_id(original_event_id) + .sender(&BOB) + .into_utd_sync_timeline_event(), + ) + .await; + + timeline + .handle_live_event(f.text_msg("well said!").reply_to(original_event_id).sender(&ALICE)) + .await; + + // We receive the UTD. + { + let event = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + assert_let!( + TimelineItemContent::UnableToDecrypt(EncryptedMessage::MegolmV1AesSha2 { + session_id, + .. + }) = event.content() + ); + assert_eq!(session_id, SESSION_ID); + } + + // We receive the text response. + { + let event = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + let msg = event.content().as_message().unwrap(); + assert_eq!(msg.body(), "well said!"); + + let reply_details = msg.in_reply_to().unwrap(); + assert_eq!(reply_details.event_id, original_event_id); + + let replied_to = as_variant!(&reply_details.event, TimelineDetails::Ready).unwrap(); + assert!(replied_to.content.as_unable_to_decrypt().is_some()); + } + + // Import a room key backup. + let own_user_id = user_id!("@example:morheus.localhost"); + let exported_keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "1234").unwrap(); + + let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await; + olm_machine.store().import_exported_room_keys(exported_keys, |_, _| {}).await.unwrap(); + + // Retry decrypting the UTD. + timeline + .controller + .retry_event_decryption_test( + room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost"), + olm_machine, + Some(iter::once(SESSION_ID.to_owned()).collect()), + ) + .await; + + // The response is updated. + { + let event = assert_next_matches_with_timeout!( + stream, + VectorDiff::Set { index: 1, value } => value + ); + + let msg = event.content().as_message().unwrap(); + assert_eq!(msg.body(), "well said!"); + + let reply_details = msg.in_reply_to().unwrap(); + assert_eq!(reply_details.event_id, original_event_id); + + let replied_to = as_variant!(&reply_details.event, TimelineDetails::Ready).unwrap(); + assert_eq!(replied_to.content.as_message().unwrap().body(), "It's a secret to everybody"); + } + + // The event itself is decrypted. + { + let event = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value); + assert_matches!(event.encryption_info(), Some(_)); + assert_let!(TimelineItemContent::Message(message) = event.content()); + assert_eq!(message.body(), "It's a secret to everybody"); + assert!(!event.is_highlighted()); + } +} + fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { let raw = Raw::from_json( to_raw_value(&json!({ From 90d6a37b315cdbb41f9c9f23719b5223aa09be89 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 4 Nov 2024 15:58:20 +0100 Subject: [PATCH 430/979] refactor(timeline): factor out in-reply-to updates --- .../src/timeline/event_handler.rs | 96 +++++++------------ .../timeline/event_item/content/message.rs | 10 +- .../src/timeline/tests/redaction.rs | 10 +- 3 files changed, 43 insertions(+), 73 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index c482a1a43b1..20358cae735 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -547,25 +547,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let internal_id = item.internal_id.to_owned(); // Update all events that replied to this message with the edited content. - self.items.for_each(|mut entry| { - let Some(event_item) = entry.as_event() else { return }; - let Some(message) = event_item.content.as_message() else { return }; - let Some(in_reply_to) = message.in_reply_to() else { return }; - if replacement.event_id == in_reply_to.event_id { - trace!(reply_event_id = ?event_item.identifier(), "Updating response to edited event"); - let in_reply_to = InReplyToDetails { - event_id: in_reply_to.event_id.clone(), - event: TimelineDetails::Ready(Box::new( - RepliedToEvent::from_timeline_item(&new_item), - )), - }; - let new_reply_content = - TimelineItemContent::Message(message.with_in_reply_to(in_reply_to)); - let new_reply_item = - entry.with_kind(event_item.with_content(new_reply_content, None)); - ObservableVectorTransactionEntry::set(&mut entry, new_reply_item); - } - }); + Self::maybe_update_responses(self.items, &replacement.event_id, &new_item); // Update the event itself. self.items.set(item_pos, TimelineItem::new(new_item, internal_id)); @@ -945,7 +927,13 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { debug!("event item is already redacted"); } else { let new_item = item.redact(&self.meta.room_version); - self.items.set(idx, TimelineItem::new(new_item, item.internal_id.to_owned())); + let internal_id = item.internal_id.to_owned(); + + // Look for any timeline event that's a reply to the redacted event, and redact + // the replied-to event there as well. + Self::maybe_update_responses(self.items, &redacted, &new_item); + + self.items.set(idx, TimelineItem::new(new_item, internal_id)); self.result.items_updated += 1; } } else { @@ -954,26 +942,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } else { debug!("Timeline item not found, discarding redaction"); }; - - // Look for any timeline event that's a reply to the redacted event, and redact - // the replied-to event there as well. - self.items.for_each(|mut entry| { - let Some(event_item) = entry.as_event() else { return }; - let Some(message) = event_item.content.as_message() else { return }; - let Some(in_reply_to) = message.in_reply_to() else { return }; - let TimelineDetails::Ready(replied_to_event) = &in_reply_to.event else { return }; - if redacted == in_reply_to.event_id { - let replied_to_event = replied_to_event.redact(&self.meta.room_version); - let in_reply_to = InReplyToDetails { - event_id: in_reply_to.event_id.clone(), - event: TimelineDetails::Ready(Box::new(replied_to_event)), - }; - let content = TimelineItemContent::Message(message.with_in_reply_to(in_reply_to)); - let new_item = entry.with_kind(event_item.with_content(content, None)); - - ObservableVectorTransactionEntry::set(&mut entry, new_item); - } - }); } /// Attempts to redact a reaction, local or remote. @@ -1198,25 +1166,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("Updating timeline item at position {idx}"); // Update all events that replied to this previously encrypted message. - self.items.for_each(|mut entry| { - let Some(event_item) = entry.as_event() else { return }; - let Some(message) = event_item.content.as_message() else { return }; - let Some(in_reply_to) = message.in_reply_to() else { return }; - if *decrypted_event_id == in_reply_to.event_id { - trace!(reply_event_id = ?event_item.identifier(), "Updating response to edited event"); - let in_reply_to = InReplyToDetails { - event_id: in_reply_to.event_id.clone(), - event: TimelineDetails::Ready(Box::new( - RepliedToEvent::from_timeline_item(&item), - )), - }; - let new_reply_content = - TimelineItemContent::Message(message.with_in_reply_to(in_reply_to)); - let new_reply_item = - entry.with_kind(event_item.with_content(new_reply_content, None)); - ObservableVectorTransactionEntry::set(&mut entry, new_reply_item); - } - }); + Self::maybe_update_responses(self.items, decrypted_event_id, &item); let internal_id = self.items[*idx].internal_id.clone(); self.items.set(*idx, TimelineItem::new(item, internal_id)); @@ -1229,6 +1179,34 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } + /// After updating the timeline item `new_item` which id is + /// `target_event_id`, update other items that are responses to this item. + fn maybe_update_responses( + items: &mut ObservableVectorTransaction<'_, Arc>, + target_event_id: &EventId, + new_item: &EventTimelineItem, + ) { + items.for_each(|mut entry| { + let Some(event_item) = entry.as_event() else { return }; + let Some(message) = event_item.content.as_message() else { return }; + let Some(in_reply_to) = message.in_reply_to() else { return }; + if target_event_id == in_reply_to.event_id { + trace!(reply_event_id = ?event_item.identifier(), "Updating response to edited event"); + let in_reply_to = InReplyToDetails { + event_id: in_reply_to.event_id.clone(), + event: TimelineDetails::Ready(Box::new( + RepliedToEvent::from_timeline_item(new_item), + )), + }; + let new_reply_content = + TimelineItemContent::Message(message.with_in_reply_to(in_reply_to)); + let new_reply_item = + entry.with_kind(event_item.with_content(new_reply_content, None)); + ObservableVectorTransactionEntry::set(&mut entry, new_reply_item); + } + }); + } + fn pending_reactions( &mut self, content: &TimelineItemContent, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 4e361e3862b..09ee071f923 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -35,7 +35,7 @@ use ruma::{ }, html::RemoveReplyFallback, serde::Raw, - OwnedEventId, OwnedUserId, RoomVersionId, UserId, + OwnedEventId, OwnedUserId, UserId, }; use tracing::{error, trace}; @@ -337,14 +337,6 @@ impl RepliedToEvent { } } - pub(in crate::timeline) fn redact(&self, room_version: &RoomVersionId) -> Self { - Self { - content: self.content.redact(room_version), - sender: self.sender.clone(), - sender_profile: self.sender_profile.clone(), - } - } - /// Try to create a `RepliedToEvent` from a `TimelineEvent` by providing the /// room. pub async fn try_from_timeline_event_for_room( diff --git a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs index a42b6f3afcf..e592660f074 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs @@ -91,17 +91,17 @@ async fn test_redact_replied_to_event() { timeline.handle_live_event(f.redaction(first_item.event_id().unwrap()).sender(&ALICE)).await; - let first_item_again = - assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value); - assert_matches!(first_item_again.content(), TimelineItemContent::RedactedMessage); - assert_matches!(first_item_again.original_json(), None); - let second_item_again = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value); let message = second_item_again.content().as_message().unwrap(); let in_reply_to = message.in_reply_to().unwrap(); assert_let!(TimelineDetails::Ready(replied_to_event) = &in_reply_to.event); assert_matches!(replied_to_event.content(), TimelineItemContent::RedactedMessage); + + let first_item_again = + assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value); + assert_matches!(first_item_again.content(), TimelineItemContent::RedactedMessage); + assert_matches!(first_item_again.original_json(), None); } #[async_test] From 04275d7c273b295a9f4f491bc51828926f19b192 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 5 Nov 2024 09:46:57 +0100 Subject: [PATCH 431/979] refactor!(room list): remove unneeded argument from `RoomList::entries_with_dynamic_adapters` Changelog: the parameter `room_info_notable_update_receiver` was removed from `RoomList::entries_with_dynamic_adapters`, since it could be inferred internally instead. --- bindings/matrix-sdk-ffi/src/room_list.rs | 6 +----- .../matrix-sdk-ui/src/room_list_service/room_list.rs | 2 +- .../tests/integration/room_list_service.rs | 10 ++++------ labs/multiverse/src/main.rs | 5 +---- .../src/tests/sliding_sync/room.rs | 6 ++---- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index a1a10e6646a..412ee88cce4 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -183,7 +183,6 @@ impl RoomList { listener: Box, ) -> Arc { let this = self.clone(); - let client = self.room_list_service.inner.client(); let utd_hook = self.room_list_service.utd_hook.clone(); // The following code deserves a bit of explanation. @@ -231,10 +230,7 @@ impl RoomList { // borrowing `this`, which is going to live long enough since it will live as // long as `entries_stream` and `dynamic_entries_controller`. let (entries_stream, dynamic_entries_controller) = - this.inner.entries_with_dynamic_adapters( - page_size.try_into().unwrap(), - client.room_info_notable_update_receiver(), - ); + this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap()); // FFI dance to make those values consumable by foreign language, nothing fancy // here, that's the real code for this method. diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index 8b4ecf9ab4c..c3a7dd35b92 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -144,8 +144,8 @@ impl RoomList { pub fn entries_with_dynamic_adapters( &self, page_size: usize, - room_info_notable_update_receiver: broadcast::Receiver, ) -> (impl Stream>> + '_, RoomListDynamicEntriesController) { + let room_info_notable_update_receiver = self.client.room_info_notable_update_receiver(); let list = self.sliding_sync_list.clone(); let filter_fn_cell = AsyncCell::shared(); 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 f586c528927..ba944cb38a9 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -1196,15 +1196,14 @@ async fn test_loading_states() -> Result<(), Error> { #[async_test] async fn test_dynamic_entries_stream() -> Result<(), Error> { - let (client, server, room_list) = new_room_list_service().await?; + let (_client, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); let all_rooms = room_list.all_rooms().await?; - let (dynamic_entries_stream, dynamic_entries) = - all_rooms.entries_with_dynamic_adapters(5, client.room_info_notable_update_receiver()); + let (dynamic_entries_stream, dynamic_entries) = all_rooms.entries_with_dynamic_adapters(5); pin_mut!(dynamic_entries_stream); sync_then_assert_request_and_fake_response! { @@ -1599,15 +1598,14 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> { #[async_test] async fn test_room_sorting() -> Result<(), Error> { - let (client, server, room_list) = new_room_list_service().await?; + let (_client, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); let all_rooms = room_list.all_rooms().await?; - let (stream, dynamic_entries) = - all_rooms.entries_with_dynamic_adapters(10, client.room_info_notable_update_receiver()); + let (stream, dynamic_entries) = all_rooms.entries_with_dynamic_adapters(10); pin_mut!(stream); sync_then_assert_request_and_fake_response! { diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 810766fa7cb..d88ffa92aba 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -182,7 +182,6 @@ impl App { Default::default(); let timelines = Arc::new(Mutex::new(HashMap::new())); - let c = client.clone(); let r = rooms.clone(); let ri = room_infos.clone(); let ur = ui_rooms.clone(); @@ -192,14 +191,12 @@ impl App { let all_rooms = room_list_service.all_rooms().await?; let listen_task = spawn(async move { - let client = c; let rooms = r; let room_infos = ri; let ui_rooms = ur; let timelines = t; - let (stream, entries_controller) = all_rooms - .entries_with_dynamic_adapters(50_000, client.room_info_notable_update_receiver()); + let (stream, entries_controller) = all_rooms.entries_with_dynamic_adapters(50_000); entries_controller.set_filter(Box::new(new_filter_non_left())); pin_mut!(stream); diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 9dd0958f5ec..e8b41d9c528 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -771,8 +771,7 @@ async fn test_delayed_decryption_latest_event() -> Result<()> { // Get the room list of Alice. let alice_all_rooms = alice_sync_service.room_list_service().all_rooms().await.unwrap(); - let (alice_room_list_stream, entries) = alice_all_rooms - .entries_with_dynamic_adapters(10, alice.room_info_notable_update_receiver()); + let (alice_room_list_stream, entries) = alice_all_rooms.entries_with_dynamic_adapters(10); entries.set_filter(Box::new(new_filter_all(vec![]))); pin_mut!(alice_room_list_stream); @@ -935,8 +934,7 @@ async fn test_room_info_notable_update_deduplication() -> Result<()> { alice_room.enable_encryption().await.unwrap(); let alice_room_list = alice_sync_service.room_list_service().all_rooms().await.unwrap(); - let (alice_rooms, alice_room_controller) = alice_room_list - .entries_with_dynamic_adapters(10, alice.room_info_notable_update_receiver()); + let (alice_rooms, alice_room_controller) = alice_room_list.entries_with_dynamic_adapters(10); alice_room_controller.set_filter(Box::new(new_filter_all(vec![]))); From 2fa54e5cfaa9778d449218513350aa1e9c5eca57 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Thu, 19 Sep 2024 12:19:13 +0200 Subject: [PATCH 432/979] Activate share_pos on the room-list sliding sync instance --- .../src/room_list_service/mod.rs | 4 +- .../tests/integration/room_list_service.rs | 95 ++++++++++++++++++- .../tests/integration/sliding_sync.rs | 11 +++ 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 57b832e9b43..5a52f610757 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -135,7 +135,9 @@ impl RoomListService { })) .with_typing_extension(assign!(http::request::Typing::default(), { enabled: Some(true), - })); + })) + // We don't deal with encryption device messages here so this is safe + .share_pos(); let sliding_sync = builder .add_cached_list( 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 ba944cb38a9..8ebaade4579 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -6,7 +6,11 @@ use std::{ use assert_matches::assert_matches; use eyeball_im::VectorDiff; use futures_util::{pin_mut, FutureExt, StreamExt}; -use matrix_sdk::{test_utils::logged_in_client_with_server, Client}; +use matrix_sdk::{ + config::RequestConfig, + test_utils::{logged_in_client_with_server, set_client_session, test_client_builder}, + Client, +}; use matrix_sdk_base::sync::UnreadNotificationsCount; use matrix_sdk_test::{async_test, mocks::mock_encryption_state}; use matrix_sdk_ui::{ @@ -23,6 +27,7 @@ use ruma::{ }; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; +use tempfile::TempDir; use tokio::{spawn, sync::mpsc::channel, task::yield_now}; use wiremock::{ matchers::{header, method, path}, @@ -38,12 +43,30 @@ async fn new_room_list_service() -> Result<(Client, MockServer, RoomListService) Ok((client, server, room_list)) } +async fn new_persistent_room_list_service( + store_path: &std::path::Path, +) -> Result<(MockServer, RoomListService), Error> { + let server = MockServer::start().await; + let client = test_client_builder(Some(server.uri().to_string())) + .request_config(RequestConfig::new().disable_retry()) + .sqlite_store(store_path, None) + .build() + .await + .unwrap(); + set_client_session(&client).await; + + let room_list = RoomListService::new(client.clone()).await?; + + Ok((server, room_list)) +} + // Same macro as in the main, with additional checking that the state // before/after the sync loop match those we expect. macro_rules! sync_then_assert_request_and_fake_response { ( [$server:ident, $room_list:ident, $stream:ident] $( states = $pre_state:pat => $post_state:pat, )? + $( assert pos $pos:expr, )? assert request $assert_request:tt { $( $request_json:tt )* }, respond with = $( ( code $code:expr ) )? { $( $response_json:tt )* } $( , after delay = $response_delay:expr )? @@ -53,6 +76,7 @@ macro_rules! sync_then_assert_request_and_fake_response { [$server, $room_list, $stream] sync matches Some(Ok(_)), $( states = $pre_state => $post_state, )? + $( assert pos $pos, )? assert request $assert_request { $( $request_json )* }, respond with = $( ( code $code ) )? { $( $response_json )* }, $( after delay = $response_delay, )? @@ -63,6 +87,7 @@ macro_rules! sync_then_assert_request_and_fake_response { [$server:ident, $room_list:ident, $stream:ident] sync matches $sync_result:pat, $( states = $pre_state:pat => $post_state:pat, )? + $( assert pos $pos:expr, )? assert request $assert_request:tt { $( $request_json:tt )* }, respond with = $( ( code $code:expr ) )? { $( $response_json:tt )* } $( , after delay = $response_delay:expr )? @@ -80,6 +105,7 @@ macro_rules! sync_then_assert_request_and_fake_response { let next = super::sliding_sync_then_assert_request_and_fake_response! { [$server, $stream] sync matches $sync_result, + $( assert pos $pos, )? assert request $assert_request { $( $request_json )* }, respond with = $( ( code $code ) )? { $( $response_json )* }, $( after delay = $response_delay, )? @@ -481,6 +507,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { sync_then_assert_request_and_fake_response! { [server, room_list, sync] states = Init => SettingUp, + assert pos None::, assert request >= { "lists": { ALL_ROOMS: { @@ -509,6 +536,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { sync_then_assert_request_and_fake_response! { [server, room_list, sync] states = SettingUp => Running, + assert pos Some("0"), assert request >= { "lists": { ALL_ROOMS: { @@ -537,6 +565,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { sync_then_assert_request_and_fake_response! { [server, room_list, sync] states = Running => Running, + assert pos Some("1"), assert request >= { "lists": { ALL_ROOMS: { @@ -560,6 +589,70 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { Ok(()) } +#[async_test] +async fn test_sync_resumes_from_previous_state_after_restart() -> Result<(), Error> { + let tmp_dir = TempDir::new().unwrap(); + let store_path = tmp_dir.path(); + + { + let (server, room_list) = new_persistent_room_list_service(store_path).await?; + let sync = room_list.sync(); + pin_mut!(sync); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + states = Init => SettingUp, + assert pos None::, + assert request >= { + "lists": { + ALL_ROOMS: { + "ranges": [[0, 19]], + }, + }, + }, + respond with = { + "pos": "0", + "lists": { + ALL_ROOMS: { + "count": 10, + }, + }, + "rooms": {}, + }, + }; + } + + { + let (server, room_list) = new_persistent_room_list_service(store_path).await?; + let sync = room_list.sync(); + pin_mut!(sync); + + sync_then_assert_request_and_fake_response! { + [server, room_list, sync] + states = Init => SettingUp, + assert pos Some("0"), + assert request >= { + "lists": { + ALL_ROOMS: { + "ranges": [[0, 19]], + }, + }, + }, + respond with = { + "pos": "1", + "lists": { + ALL_ROOMS: { + "count": 10, + }, + }, + "rooms": {}, + }, + }; + } + + Ok(()) +} + #[async_test] async fn test_sync_resumes_from_error() -> Result<(), Error> { let (_, server, room_list) = new_room_list_service().await?; diff --git a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs index b267fa96bb7..5ec1dda2c00 100644 --- a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs @@ -58,6 +58,7 @@ impl Match for SlidingSyncMatcher { macro_rules! sliding_sync_then_assert_request_and_fake_response { ( [$server:ident, $stream:ident] + $( assert pos $pos:expr, )? assert request $sign:tt { $( $request_json:tt )* }, respond with = $( ( code $code:expr ) )? { $( $response_json:tt )* } $( , after delay = $response_delay:expr )? @@ -66,6 +67,7 @@ macro_rules! sliding_sync_then_assert_request_and_fake_response { sliding_sync_then_assert_request_and_fake_response! { [$server, $stream] sync matches Some(Ok(_)), + $( assert pos $pos, )? assert request $sign { $( $request_json )* }, respond with = $( ( code $code ) )? { $( $response_json )* }, $( after delay = $response_delay, )? @@ -75,6 +77,7 @@ macro_rules! sliding_sync_then_assert_request_and_fake_response { ( [$server:ident, $stream:ident] sync matches $sync_result:pat, + $( assert pos $pos:expr, )? assert request $sign:tt { $( $request_json:tt )* }, respond with = $( ( code $code:expr ) )? { $( $response_json:tt )* } $( , after delay = $response_delay:expr )? @@ -117,6 +120,14 @@ macro_rules! sliding_sync_then_assert_request_and_fake_response { root.remove("txn_id"); } + // Validate `pos` from the query parameter if specified. + $( + match $pos { + Some(pos) => assert!(wiremock::matchers::query_param("pos", pos).matches(request)), + None => assert!(wiremock::matchers::query_param_is_missing("pos").matches(request)), + } + )? + if let Err(error) = assert_json_diff::assert_json_matches_no_panic( &json_value, &json!({ $( $request_json )* }), From 8865e2ff7463ff0b520f53f1a3ffc5500fbb3d86 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Mon, 7 Oct 2024 21:32:45 +0200 Subject: [PATCH 433/979] RoomListLoadingState now yields immediately with current value This fixes a problem when doing an incremental sync at launch, where `NotLoaded` event would not be dispatched until data became available or timeout is reached, leading to app waiting for it. --- .../src/room_list_service/room_list.rs | 4 +- .../tests/integration/room_list_service.rs | 41 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index c3a7dd35b92..8056c14dac8 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -114,8 +114,10 @@ impl RoomList { } /// Get a subscriber to the room list loading state. + /// + /// This method will send out the current loading state as the first update. pub fn loading_state(&self) -> Subscriber { - self.loading_state.subscribe() + self.loading_state.subscribe_reset() } /// Get a stream of rooms. 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 8ebaade4579..0c9429203e3 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -599,6 +599,13 @@ async fn test_sync_resumes_from_previous_state_after_restart() -> Result<(), Err let sync = room_list.sync(); pin_mut!(sync); + let all_rooms = room_list.all_rooms().await?; + let mut all_rooms_loading_state = all_rooms.loading_state(); + + // The loading is not loaded. + assert_next_matches!(all_rooms_loading_state, RoomListLoadingState::NotLoaded); + assert_pending!(all_rooms_loading_state); + sync_then_assert_request_and_fake_response! { [server, room_list, sync] states = Init => SettingUp, @@ -627,6 +634,20 @@ async fn test_sync_resumes_from_previous_state_after_restart() -> Result<(), Err let sync = room_list.sync(); pin_mut!(sync); + let all_rooms = room_list.all_rooms().await?; + let mut all_rooms_loading_state = all_rooms.loading_state(); + + // Wait on Tokio to run all the tasks. Necessary only when testing. + yield_now().await; + + // We already have a state stored so the list should already be loaded + assert_next_matches!( + all_rooms_loading_state, + RoomListLoadingState::Loaded { maximum_number_of_rooms: Some(10) } + ); + assert_pending!(all_rooms_loading_state); + + // pos has been restored and is used when doing the req sync_then_assert_request_and_fake_response! { [server, room_list, sync] states = Init => SettingUp, @@ -642,12 +663,22 @@ async fn test_sync_resumes_from_previous_state_after_restart() -> Result<(), Err "pos": "1", "lists": { ALL_ROOMS: { - "count": 10, + "count": 12, }, }, "rooms": {}, }, }; + + // Wait on Tokio to run all the tasks. Necessary only when testing. + yield_now().await; + + // maximum_number_of_rooms changed so we should get a new loaded state + assert_next_matches!( + all_rooms_loading_state, + RoomListLoadingState::Loaded { maximum_number_of_rooms: Some(12) } + ); + assert_pending!(all_rooms_loading_state); } Ok(()) @@ -1139,8 +1170,7 @@ async fn test_loading_states() -> Result<(), Error> { let mut all_rooms_loading_state = all_rooms.loading_state(); // The loading is not loaded. - assert_matches!(all_rooms_loading_state.get(), RoomListLoadingState::NotLoaded); - assert_pending!(all_rooms_loading_state); + assert_next_matches!(all_rooms_loading_state, RoomListLoadingState::NotLoaded); sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -1246,11 +1276,10 @@ async fn test_loading_states() -> Result<(), Error> { pin_mut!(sync); // The loading state is loaded! Indeed, there is data loaded from the cache. - assert_matches!( - all_rooms_loading_state.get(), + assert_next_matches!( + all_rooms_loading_state, RoomListLoadingState::Loaded { maximum_number_of_rooms: Some(12) } ); - assert_pending!(all_rooms_loading_state); sync_then_assert_request_and_fake_response! { [server, room_list, sync] From ace96e372f1bb79b4abaf11a3187c17783d93379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 4 Nov 2024 15:47:56 +0100 Subject: [PATCH 434/979] chore: Fix a warning from an invalid Cargo.toml config for the OIDC example --- examples/oidc_cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/oidc_cli/Cargo.toml b/examples/oidc_cli/Cargo.toml index a5c8617dd02..80d45d4c32a 100644 --- a/examples/oidc_cli/Cargo.toml +++ b/examples/oidc_cli/Cargo.toml @@ -13,7 +13,7 @@ test = false anyhow = { workspace = true } axum = "0.7.4" dirs = "5.0.1" -futures-util = { workspace = true, default-features = false } +futures-util = { workspace = true } matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui" } rand = { workspace = true } serde = { workspace = true } From b233aa64d297d61218e2fb19e89125a0b62c36fa Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 5 Nov 2024 14:43:40 +0100 Subject: [PATCH 435/979] chore(timeline): rename `TimelineItemPosition::Update` to `UpdateDecrypted` --- .../src/timeline/controller/state.rs | 6 +++--- .../matrix-sdk-ui/src/timeline/event_handler.rs | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index b30102525fb..3668622dacd 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -241,7 +241,7 @@ impl TimelineState { let handle_one_res = txn .handle_remote_event( event.into(), - TimelineItemPosition::Update(idx), + TimelineItemPosition::UpdateDecrypted(idx), room_data_provider, settings, &mut day_divider_adjuster, @@ -447,7 +447,7 @@ impl TimelineStateTransaction<'_> { TimelineItemPosition::End { origin } | TimelineItemPosition::Start { origin } => origin, - TimelineItemPosition::Update(idx) => self + TimelineItemPosition::UpdateDecrypted(idx) => self .items .get(idx) .and_then(|item| item.as_event()) @@ -703,7 +703,7 @@ impl TimelineStateTransaction<'_> { self.meta.all_events.push_back(event_meta.base_meta()); } - TimelineItemPosition::Update(_) => { + TimelineItemPosition::UpdateDecrypted(_) => { if let Some(event) = self.meta.all_events.iter_mut().find(|e| e.event_id == event_meta.event_id) { diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 20358cae735..941823b967a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -275,10 +275,11 @@ pub(super) enum TimelineItemPosition { /// recent). End { origin: RemoteEventOrigin }, - /// A single item is updated. + /// A single item is updated, after it's been successfully decrypted. /// - /// This only happens when a UTD must be replaced with the decrypted event. - Update(usize), + /// This happens when an item that was a UTD must be replaced with the + /// decrypted event. + UpdateDecrypted(usize), } /// The outcome of handling a single event with @@ -480,7 +481,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { if !self.result.item_added { trace!("No new item added"); - if let Flow::Remote { position: TimelineItemPosition::Update(idx), .. } = self.ctx.flow + if let Flow::Remote { position: TimelineItemPosition::UpdateDecrypted(idx), .. } = + self.ctx.flow { // If add was not called, that means the UTD event is one that // wouldn't normally be visible. Remove it. @@ -574,7 +576,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: PendingEdit, ) { match position { - TimelineItemPosition::Start { .. } | TimelineItemPosition::Update(_) => { + TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted(_) => { // Only insert the edit if there wasn't any other edit // before. // @@ -1010,7 +1012,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { | TimelineItemPosition::End { origin } => origin, // For updates, reuse the origin of the encrypted event. - TimelineItemPosition::Update(idx) => self.items[idx] + TimelineItemPosition::UpdateDecrypted(idx) => self.items[idx] .as_event() .and_then(|ev| Some(ev.as_remote()?.origin)) .unwrap_or_else(|| { @@ -1160,7 +1162,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Remote { event_id: decrypted_event_id, - position: TimelineItemPosition::Update(idx), + position: TimelineItemPosition::UpdateDecrypted(idx), .. } => { trace!("Updating timeline item at position {idx}"); From 933033cc2532c53abc2e68ccb783979323dc5183 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 5 Nov 2024 13:05:17 +0100 Subject: [PATCH 436/979] fix(sdk): Do not always remove empty chunks from `LinkedChunk`. This patch introduces `EmptyChunk`, a new enum used to represent whether empty chunks must be removed/unlink or kept from the `LinkedChunk`. It is used by `LinkedChunk::remove_item_at`. Why is it important? For example, imagine the following situation: - one inserts a single event in a new chunk (possible if a (sliding) sync is done with `timeline_limit=1`), - one inserts many events at the position of the previous event, with one of the new events being a duplicate of the first event (possible if a (sliding) sync is done with `timeline_limit=10` this time), - prior to this patch, the older event was removed, resulting in an empty chunk, which was removed from the `LinkedChunk`, invalidating the insertion position! So, with this patch: - `RoomEvents::remove_events` does remove empty chunks, but - `RoomEvents::remove_events_and_update_insert_position` does NOT remove empty chunks, they are kept in case the position wants to insert in this same chunk. --- .../src/event_cache/linked_chunk/as_vector.rs | 17 +- .../src/event_cache/linked_chunk/mod.rs | 174 +++++++++++++++--- .../matrix-sdk/src/event_cache/room/events.rs | 109 ++++++++++- 3 files changed, 264 insertions(+), 36 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs index 4a39e5d5a74..f392f8a233b 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs @@ -452,7 +452,10 @@ mod tests { use imbl::{vector, Vector}; - use super::{super::LinkedChunk, VectorDiff}; + use super::{ + super::{EmptyChunk, LinkedChunk}, + VectorDiff, + }; fn apply_and_assert_eq( accumulator: &mut Vector, @@ -614,7 +617,10 @@ mod tests { ); let removed_item = linked_chunk - .remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap()) + .remove_item_at( + linked_chunk.item_position(|item| *item == 'c').unwrap(), + EmptyChunk::Remove, + ) .unwrap(); assert_eq!(removed_item, 'c'); assert_items_eq!( @@ -634,7 +640,10 @@ mod tests { apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]); let removed_item = linked_chunk - .remove_item_at(linked_chunk.item_position(|item| *item == 'z').unwrap()) + .remove_item_at( + linked_chunk.item_position(|item| *item == 'z').unwrap(), + EmptyChunk::Remove, + ) .unwrap(); assert_eq!(removed_item, 'z'); assert_items_eq!( @@ -773,7 +782,7 @@ mod tests { continue; }; - linked_chunk.remove_item_at(position).expect("Failed to remove an item"); + linked_chunk.remove_item_at(position, EmptyChunk::Remove).expect("Failed to remove an item"); } } } diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs index 99fd02fe5bc..c3c69a77719 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs @@ -408,9 +408,18 @@ impl LinkedChunk { /// Remove item at a specified position in the [`LinkedChunk`]. /// - /// Because the `position` can be invalid, this method returns a - /// `Result`. - pub fn remove_item_at(&mut self, position: Position) -> Result { + /// `position` must point to a valid item, otherwise the method returns + /// `Err`. + /// + /// The chunk containing the item represented by `position` may be empty + /// once the item has been removed. In this case, the chunk can be removed + /// if `empty_chunk` contains [`EmptyChunk::Remove`], otherwise the chunk is + /// kept if `empty_chunk` contains [`EmptyChunk::Keep`]. + pub fn remove_item_at( + &mut self, + position: Position, + empty_chunk: EmptyChunk, + ) -> Result { let chunk_identifier = position.chunk_identifier(); let item_index = position.index(); @@ -446,9 +455,9 @@ impl LinkedChunk { } }; - // If the `chunk` can be unlinked, and if the `chunk` is not the first one, we - // can remove it. - if can_unlink_chunk && chunk.is_first_chunk().not() { + // If removing empty chunk is desired, and if the `chunk` can be unlinked, and + // if the `chunk` is not the first one, we can remove it. + if empty_chunk.remove() && can_unlink_chunk && chunk.is_first_chunk().not() { // Unlink `chunk`. chunk.unlink(&mut self.updates); @@ -1336,6 +1345,21 @@ where } } +/// A type representing what to do when the system has to handle an empty chunk. +pub(crate) enum EmptyChunk { + /// Keep the empty chunk. + Keep, + + /// Remove the empty chunk. + Remove, +} + +impl EmptyChunk { + fn remove(&self) -> bool { + matches!(self, Self::Remove) + } +} + #[cfg(test)] mod tests { use std::ops::Not; @@ -1343,8 +1367,8 @@ mod tests { use assert_matches::assert_matches; use super::{ - Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Error, LinkedChunk, - Position, + Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, EmptyChunk, Error, + LinkedChunk, Position, }; #[test] @@ -1950,21 +1974,21 @@ mod tests { // that. The chunk is removed. { let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap(); - let removed_item = linked_chunk.remove_item_at(position_of_f)?; + let removed_item = linked_chunk.remove_item_at(position_of_f, EmptyChunk::Remove)?; assert_eq!(removed_item, 'f'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']); assert_eq!(linked_chunk.len(), 10); let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap(); - let removed_item = linked_chunk.remove_item_at(position_of_e)?; + let removed_item = linked_chunk.remove_item_at(position_of_e, EmptyChunk::Remove)?; assert_eq!(removed_item, 'e'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']); assert_eq!(linked_chunk.len(), 9); let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap(); - let removed_item = linked_chunk.remove_item_at(position_of_d)?; + let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?; assert_eq!(removed_item, 'd'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']); @@ -1985,19 +2009,19 @@ mod tests { // that. The chunk is NOT removed because it's the first chunk. { let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap(); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'a'); assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']); assert_eq!(linked_chunk.len(), 7); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'b'); assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']); assert_eq!(linked_chunk.len(), 6); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'c'); assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']); @@ -2017,19 +2041,19 @@ mod tests { // that. The chunk is removed. { let first_position = linked_chunk.item_position(|item| *item == 'g').unwrap(); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'g'); assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']); assert_eq!(linked_chunk.len(), 4); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'h'); assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']); assert_eq!(linked_chunk.len(), 3); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'i'); assert_items_eq!(linked_chunk, [] ['j', 'k']); @@ -2050,7 +2074,7 @@ mod tests { // The chunk is removed. { let position_of_k = linked_chunk.item_position(|item| *item == 'k').unwrap(); - let removed_item = linked_chunk.remove_item_at(position_of_k)?; + let removed_item = linked_chunk.remove_item_at(position_of_k, EmptyChunk::Remove)?; assert_eq!(removed_item, 'k'); #[rustfmt::skip] @@ -2058,7 +2082,7 @@ mod tests { assert_eq!(linked_chunk.len(), 1); let position_of_j = linked_chunk.item_position(|item| *item == 'j').unwrap(); - let removed_item = linked_chunk.remove_item_at(position_of_j)?; + let removed_item = linked_chunk.remove_item_at(position_of_j, EmptyChunk::Remove)?; assert_eq!(removed_item, 'j'); assert_items_eq!(linked_chunk, []); @@ -2092,27 +2116,27 @@ mod tests { let _ = linked_chunk.updates().unwrap().take(); let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap(); - let removed_item = linked_chunk.remove_item_at(position_of_c)?; + let removed_item = linked_chunk.remove_item_at(position_of_c, EmptyChunk::Remove)?; assert_eq!(removed_item, 'c'); assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']); assert_eq!(linked_chunk.len(), 3); let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap(); - let removed_item = linked_chunk.remove_item_at(position_of_d)?; + let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?; assert_eq!(removed_item, 'd'); assert_items_eq!(linked_chunk, ['a', 'b'] [-]); assert_eq!(linked_chunk.len(), 2); let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap(); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'a'); assert_items_eq!(linked_chunk, ['b'] [-]); assert_eq!(linked_chunk.len(), 1); - let removed_item = linked_chunk.remove_item_at(first_position)?; + let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'b'); assert_items_eq!(linked_chunk, [] [-]); @@ -2134,6 +2158,110 @@ mod tests { Ok(()) } + #[test] + fn test_remove_item_at_and_keep_empty_chunks() -> Result<(), Error> { + use super::Update::*; + + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h']); + assert_eq!(linked_chunk.len(), 8); + + // Ignore previous updates. + let _ = linked_chunk.updates().unwrap().take(); + + // Remove all items from the same chunk. The chunk is empty after that. The + // chunk is NOT removed because we asked to keep it. + { + let position = linked_chunk.item_position(|item| *item == 'd').unwrap(); + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'd'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['e', 'f'] ['g', 'h']); + assert_eq!(linked_chunk.len(), 7); + + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'e'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['f'] ['g', 'h']); + assert_eq!(linked_chunk.len(), 6); + + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'f'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['g', 'h']); + assert_eq!(linked_chunk.len(), 5); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(1), 0) }, + RemoveItem { at: Position(ChunkIdentifier(1), 0) }, + RemoveItem { at: Position(ChunkIdentifier(1), 0) }, + ] + ); + } + + // Remove all items from the same chunk. The chunk is empty after that. The + // chunk is NOT removed because we asked to keep it. + { + let position = linked_chunk.item_position(|item| *item == 'g').unwrap(); + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'g'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['h']); + assert_eq!(linked_chunk.len(), 4); + + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'h'); + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] []); + assert_eq!(linked_chunk.len(), 3); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(2), 0) }, + RemoveItem { at: Position(ChunkIdentifier(2), 0) }, + ] + ); + } + + // Remove all items from the same chunk. The chunk is empty after that. The + // chunk is NOT removed because we asked to keep it. + { + let position = linked_chunk.item_position(|item| *item == 'a').unwrap(); + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'a'); + assert_items_eq!(linked_chunk, ['b', 'c'] [] []); + assert_eq!(linked_chunk.len(), 2); + + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'b'); + assert_items_eq!(linked_chunk, ['c'] [] []); + assert_eq!(linked_chunk.len(), 1); + + let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; + + assert_eq!(removed_item, 'c'); + assert_items_eq!(linked_chunk, [] [] []); + assert_eq!(linked_chunk.len(), 0); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + RemoveItem { at: Position(ChunkIdentifier(0), 0) }, + ] + ); + } + + Ok(()) + } + #[test] fn test_insert_gap_at() -> Result<(), Error> { use super::Update::*; diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 19d27b2e80d..0f714e9498b 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -20,7 +20,7 @@ use tracing::{debug, error, warn}; use super::super::{ deduplicator::{Decoration, Deduplicator}, - linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}, + linked_chunk::{Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position}, }; /// An alias for the real event type. @@ -229,7 +229,14 @@ impl RoomEvents { }; self.chunks - .remove_item_at(event_position) + .remove_item_at( + event_position, + // If removing an event results in an empty chunk, the empty chunk is removed + // because nothing is going to be inserted in it apparently, otherwise the + // `Self::remove_events_and_update_insert_position` method would have been + // used. + EmptyChunk::Remove, + ) .expect("Failed to remove an event we have just found"); } } @@ -280,7 +287,12 @@ impl RoomEvents { }; self.chunks - .remove_item_at(event_position) + .remove_item_at( + event_position, + // If removing an event results in an empty chunk, the empty chunk is kept + // because maybe something is going to be inserted in it! + EmptyChunk::Keep, + ) .expect("Failed to remove an event we have just found"); // A `Position` is composed of a `ChunkIdentifier` and an index. @@ -405,6 +417,37 @@ mod tests { ); } + #[test] + fn test_push_events_with_duplicates_on_a_chunk_of_one_event() { + let (event_id_0, event_0) = new_event("$ev0"); + + let mut room_events = RoomEvents::new(); + + // The first chunk can never be removed, so let's create a gap, then a new + // chunk. + room_events.push_gap(Gap { prev_token: "hello".to_owned() }); + room_events.push_events([event_0.clone()]); + + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (2, 0)), + ] + ); + + // Everything is alright. Now let's push a duplicated event. + room_events.push_events([event_0]); + + // The event has been removed, then the chunk was empty, so removed, and a new + // chunk has been created with identifier 3. + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (3, 0)), + ] + ); + } + #[test] fn test_push_gap() { let (event_id_0, event_0) = new_event("$ev0"); @@ -511,6 +554,36 @@ mod tests { ); } + #[test] + fn test_insert_events_at_with_duplicates_on_a_chunk_of_one_event() { + let (event_id_0, event_0) = new_event("$ev0"); + + let mut room_events = RoomEvents::new(); + + // The first chunk can never be removed, so let's create a gap, then a new + // chunk. + room_events.push_gap(Gap { prev_token: "hello".to_owned() }); + room_events.push_events([event_0.clone()]); + + let position_of_event_0 = room_events + .events() + .find_map(|(position, event)| { + (event.event_id().unwrap() == event_id_0).then_some(position) + }) + .unwrap(); + + room_events.insert_events_at([event_0], position_of_event_0).unwrap(); + + // Event has been removed, the chunk was empty, but it was kept so that the + // position was still valid and the new event can be inserted. + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (2, 0)), + ] + ); + } + #[test] fn test_insert_gap_at() { let (event_id_0, event_0) = new_event("$ev0"); @@ -656,17 +729,20 @@ mod tests { // Push some events. let mut room_events = RoomEvents::new(); - room_events.push_events([event_0, event_1, event_2, event_3]); + room_events.push_events([event_0, event_1]); + room_events.push_gap(Gap { prev_token: "hello".to_owned() }); + room_events.push_events([event_2, event_3]); assert_events_eq!( room_events.events(), [ (event_id_0 at (0, 0)), (event_id_1 at (0, 1)), - (event_id_2 at (0, 2)), - (event_id_3 at (0, 3)), + (event_id_2 at (2, 0)), + (event_id_3 at (2, 1)), ] ); + assert_eq!(room_events.chunks().count(), 3); // Remove some events. room_events.remove_events(vec![event_id_1, event_id_3]); @@ -675,9 +751,20 @@ mod tests { room_events.events(), [ (event_id_0 at (0, 0)), - (event_id_2 at (0, 1)), + (event_id_2 at (2, 0)), + ] + ); + + // Ensure chunks are removed once empty. + room_events.remove_events(vec![event_id_2]); + + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), ] ); + assert_eq!(room_events.chunks().count(), 2); } #[test] @@ -717,6 +804,8 @@ mod tests { room_events.push_gap(Gap { prev_token: "raclette".to_owned() }); room_events.push_events([event_7, event_8]); + assert_eq!(room_events.chunks().count(), 3); + fn position_of(room_events: &RoomEvents, event_id: &EventId) -> Position { room_events .events() @@ -832,7 +921,7 @@ mod tests { { let previous_position = position; room_events.remove_events_and_update_insert_position( - vec![event_id_2, event_id_3, event_id_7], + vec![event_id_2, event_id_3, event_id_7, event_id_8], &mut position, ); @@ -848,9 +937,11 @@ mod tests { room_events.events(), [ (event_id_6 at (0, 0)), - (event_id_8 at (2, 0)), ] ); } + + // Ensure no chunk has been removed. + assert_eq!(room_events.chunks().count(), 3); } } From bb2d19a1d8f3a196ae3d5ab6c518db0eb60b0a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 5 Nov 2024 17:35:54 +0100 Subject: [PATCH 437/979] feat(ffi): add room alias format validation --- bindings/matrix-sdk-ffi/src/lib.rs | 1 + bindings/matrix-sdk-ffi/src/room_alias.rs | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 bindings/matrix-sdk-ffi/src/room_alias.rs diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 0e404a848ff..a9f1efdb06d 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -30,6 +30,7 @@ mod timeline_event_filter; mod tracing; mod utils; mod widget; +mod room_alias; use async_compat::TOKIO1 as RUNTIME; use matrix_sdk::ruma::events::room::{ diff --git a/bindings/matrix-sdk-ffi/src/room_alias.rs b/bindings/matrix-sdk-ffi/src/room_alias.rs new file mode 100644 index 00000000000..564374b2deb --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/room_alias.rs @@ -0,0 +1,7 @@ +use ruma::RoomAliasId; + +/// Verifies the passed `String` matches the expected room alias format. +#[matrix_sdk_ffi_macros::export] +fn is_room_alias_format_valid(alias: String) -> bool { + RoomAliasId::parse(alias).is_ok() +} \ No newline at end of file From fbc914f58636b3efb46663a0f3f7598fc6b3bfc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 5 Nov 2024 17:37:10 +0100 Subject: [PATCH 438/979] feat(ffi): add room display name to room alias transformation --- Cargo.lock | 23 +++++----- bindings/matrix-sdk-ffi/src/lib.rs | 2 +- bindings/matrix-sdk-ffi/src/room_alias.rs | 9 +++- crates/matrix-sdk-base/Cargo.toml | 1 + crates/matrix-sdk-base/src/rooms/mod.rs | 54 +++++++++++++++++++++++ 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30db5f2af15..98c2e7b3d74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -597,7 +597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.8", "serde", ] @@ -2965,6 +2965,7 @@ dependencies = [ "matrix-sdk-store-encryption", "matrix-sdk-test", "once_cell", + "regex", "ruma", "serde", "serde_json", @@ -4143,7 +4144,7 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -4385,14 +4386,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -4406,13 +4407,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -4423,9 +4424,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index a9f1efdb06d..9ed31580edd 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -16,6 +16,7 @@ mod notification; mod notification_settings; mod platform; mod room; +mod room_alias; mod room_directory_search; mod room_info; mod room_list; @@ -30,7 +31,6 @@ mod timeline_event_filter; mod tracing; mod utils; mod widget; -mod room_alias; use async_compat::TOKIO1 as RUNTIME; use matrix_sdk::ruma::events::room::{ diff --git a/bindings/matrix-sdk-ffi/src/room_alias.rs b/bindings/matrix-sdk-ffi/src/room_alias.rs index 564374b2deb..125cad31512 100644 --- a/bindings/matrix-sdk-ffi/src/room_alias.rs +++ b/bindings/matrix-sdk-ffi/src/room_alias.rs @@ -1,7 +1,14 @@ +use matrix_sdk::DisplayName; use ruma::RoomAliasId; /// Verifies the passed `String` matches the expected room alias format. #[matrix_sdk_ffi_macros::export] fn is_room_alias_format_valid(alias: String) -> bool { RoomAliasId::parse(alias).is_ok() -} \ No newline at end of file +} + +/// Transforms a Room's display name into a valid room alias name. +#[matrix_sdk_ffi_macros::export] +fn room_alias_name_from_room_display_name(room_name: String) -> String { + DisplayName::Named(room_name).to_room_alias_name() +} diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 04aac0d4ec3..0c8d575d4b2 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -67,6 +67,7 @@ tokio = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } uniffi = { workspace = true, optional = true } +regex = "1.11.1" [dev-dependencies] assert_matches = { workspace = true } diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index fac5b164f99..9aa9b102750 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -15,6 +15,7 @@ pub use normal::{ Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState, RoomStateFilter, }; +use regex::Regex; use ruma::{ assign, events::{ @@ -64,6 +65,38 @@ pub enum DisplayName { Empty, } +const WHITESPACE_REGEX: &str = r"\s+"; +const INVALID_SYMBOLS_REGEX: &str = r"[#,:]+"; + +impl DisplayName { + /// Transforms the current display name into the name part of a + /// `RoomAliasId`. + pub fn to_room_alias_name(&self) -> String { + let room_name = match self { + Self::Named(name) => name, + Self::Aliased(name) => name, + Self::Calculated(name) => name, + Self::EmptyWas(name) => name, + Self::Empty => "", + }; + + let whitespace_regex = + Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid"); + let symbol_regex = + Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid"); + + // Replace whitespaces with `-` + let sanitised = whitespace_regex.replace_all(room_name, "-"); + // Remove non-ASCII characters and ASCII control characters + let sanitised = + String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control())); + // Remove other problematic ASCII symbols + let sanitised = symbol_regex.replace_all(&sanitised, ""); + // Lowercased + sanitised.to_lowercase() + } +} + impl fmt::Display for DisplayName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -541,6 +574,7 @@ mod tests { use ruma::events::tag::{TagInfo, TagName, Tags}; use super::{BaseRoomInfo, RoomNotableTags}; + use crate::DisplayName; #[test] fn test_handle_notable_tags_favourite() { @@ -571,4 +605,24 @@ mod tests { base_room_info.handle_notable_tags(&tags); assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not()); } + + #[test] + fn test_room_alias_from_room_display_name_lowercases() { + assert_eq!("roomalias", DisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()); + } + + #[test] + fn test_room_alias_from_room_display_name_removes_whitespace() { + assert_eq!("room-alias", DisplayName::Named("Room Alias".to_owned()).to_room_alias_name()); + } + + #[test] + fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() { + assert_eq!("roomalias", DisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()); + } + + #[test] + fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() { + assert_eq!("roomalias", DisplayName::Named("#Room,Alias:".to_owned()).to_room_alias_name()); + } } From e5d4ea5964ff6ae8f0ed0a7a848fe2b284ea46b2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 29 Oct 2024 17:10:57 +0100 Subject: [PATCH 439/979] chore(base): Simplify `&*` with `.as_ref()` or `.deref()`. This patch replaces a `&*` by a `.as_ref()` and a `.deref()`. The result is the same but it's just simpler for newcomers to understand what happens. --- crates/matrix-sdk-base/src/client.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 36485c13e26..8dd67f83bde 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -13,11 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "e2e-encryption")] -use std::ops::Deref; use std::{ collections::{BTreeMap, BTreeSet}, fmt, iter, + ops::Deref, sync::Arc, }; @@ -252,12 +251,12 @@ impl BaseClient { /// Get a reference to the store. #[allow(unknown_lints, clippy::explicit_auto_deref)] pub fn store(&self) -> &DynStateStore { - &*self.store + self.store.deref() } /// Get a reference to the event cache store. pub fn event_cache_store(&self) -> &DynEventCacheStore { - &*self.event_cache_store + self.event_cache_store.as_ref() } /// Is the client logged in. From 9f11bced1039e4e5c58acf522e5d0eb4fe2acf11 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 30 Oct 2024 11:06:15 +0100 Subject: [PATCH 440/979] chore: Rename `BackingStore::Error` to `BackingStore::LockingError`. The idea is to avoid name conflicts when implementing other traits that use the `Error` associated type. --- crates/matrix-sdk-common/src/store_locks.rs | 8 ++++---- crates/matrix-sdk-crypto/src/store/mod.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-common/src/store_locks.rs b/crates/matrix-sdk-common/src/store_locks.rs index d97dac03a35..6ee4de48a0f 100644 --- a/crates/matrix-sdk-common/src/store_locks.rs +++ b/crates/matrix-sdk-common/src/store_locks.rs @@ -58,7 +58,7 @@ use crate::{ #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait BackingStore { - type Error: Error + Send + Sync; + type LockError: Error + Send + Sync; /// Try to take a lock using the given store. async fn try_lock( @@ -66,7 +66,7 @@ pub trait BackingStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result; + ) -> Result; } /// Small state machine to handle wait times. @@ -400,7 +400,7 @@ mod tests { #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl BackingStore for TestStore { - type Error = DummyError; + type LockError = DummyError; /// Try to take a lock using the given store. async fn try_lock( @@ -408,7 +408,7 @@ mod tests { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result { Ok(self.try_take_leased_lock(lease_duration_ms, key, holder)) } } diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 92fdc6f32ec..9b8d06e3139 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -1925,14 +1925,14 @@ pub struct LockableCryptoStore(Arc>); #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl matrix_sdk_common::store_locks::BackingStore for LockableCryptoStore { - type Error = CryptoStoreError; + type LockError = CryptoStoreError; async fn try_lock( &self, lease_duration_ms: u32, key: &str, holder: &str, - ) -> std::result::Result { + ) -> std::result::Result { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await } } From e24d9b3ce31a5b0a8ef46d488634ae634a205a50 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 30 Oct 2024 15:07:48 +0100 Subject: [PATCH 441/979] feat(base): Create `LockableEventCacheStore`. --- .../src/event_cache_store/memory_store.rs | 9 ++++++++ .../src/event_cache_store/mod.rs | 23 ++++++++++++++++++- .../src/event_cache_store/traits.rs | 17 ++++++++++++++ .../src/event_cache_store.rs | 9 ++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs index 15fdb23708e..88703ed6fb1 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs @@ -51,6 +51,15 @@ impl MemoryStore { impl EventCacheStore for MemoryStore { type Error = EventCacheStoreError; + async fn try_take_leased_lock( + &self, + lease_duration_ms: u32, + key: &str, + holder: &str, + ) -> Result { + todo!() + } + async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { // Avoid duplication. Let's try to remove it first. self.remove_media_content(request).await?; diff --git a/crates/matrix-sdk-base/src/event_cache_store/mod.rs b/crates/matrix-sdk-base/src/event_cache_store/mod.rs index f6458a580ac..6709ec66420 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/mod.rs @@ -19,7 +19,7 @@ //! into the event cache for the actual storage. By default this brings an //! in-memory store. -use std::str::Utf8Error; +use std::{str::Utf8Error, sync::Arc}; #[cfg(any(test, feature = "testing"))] #[macro_use] @@ -27,6 +27,7 @@ pub mod integration_tests; mod memory_store; mod traits; +use matrix_sdk_common::store_locks::BackingStore; pub use matrix_sdk_store_encryption::Error as StoreEncryptionError; #[cfg(any(test, feature = "testing"))] @@ -83,3 +84,23 @@ impl EventCacheStoreError { /// An `EventCacheStore` specific result type. pub type Result = std::result::Result; + +/// A type that wraps the [`EventCacheStore`] but implements [`BackingStore`] to +/// make it usable inside the cross process lock. +#[derive(Clone, Debug)] +struct LockableEventCacheStore(Arc); + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl BackingStore for LockableEventCacheStore { + type LockError = EventCacheStoreError; + + async fn try_lock( + &self, + lease_duration_ms: u32, + key: &str, + holder: &str, + ) -> std::result::Result { + self.0.try_take_leased_lock(lease_duration_ms, key, holder).await + } +} diff --git a/crates/matrix-sdk-base/src/event_cache_store/traits.rs b/crates/matrix-sdk-base/src/event_cache_store/traits.rs index 08691b06729..a288750f6b1 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/traits.rs @@ -29,6 +29,14 @@ pub trait EventCacheStore: AsyncTraitDeps { /// The error type used by this event cache store. type Error: fmt::Debug + Into; + /// Try to take a lock using the given store. + async fn try_take_leased_lock( + &self, + lease_duration_ms: u32, + key: &str, + holder: &str, + ) -> Result; + /// Add a media file's content in the media store. /// /// # Arguments @@ -105,6 +113,15 @@ impl fmt::Debug for EraseEventCacheStoreError { impl EventCacheStore for EraseEventCacheStoreError { type Error = EventCacheStoreError; + async fn try_take_leased_lock( + &self, + lease_duration_ms: u32, + key: &str, + holder: &str, + ) -> Result { + self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into) + } + async fn add_media_content( &self, request: &MediaRequest, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 5a2bf2462e8..2b760731125 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -140,6 +140,15 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { impl EventCacheStore for SqliteEventCacheStore { type Error = Error; + async fn try_take_leased_lock( + &self, + lease_duration_ms: u32, + key: &str, + holder: &str, + ) -> Result { + todo!() + } + async fn add_media_content(&self, request: &MediaRequest, content: Vec) -> Result<()> { let uri = self.encode_key(keys::MEDIA, request.source.unique_key()); let format = self.encode_key(keys::MEDIA, request.format.unique_key()); From 16a86587ea7ba99c01f6bde387a6d73252221f7e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 30 Oct 2024 15:28:50 +0100 Subject: [PATCH 442/979] refactor: Implement `try_take_leased_lock` on `MemoryStore`. --- .../src/event_cache_store/memory_store.rs | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs index 88703ed6fb1..0cad147a664 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroUsize, sync::RwLock as StdRwLock}; +use std::{ + collections::{hash_map::Entry, HashMap}, + num::NonZeroUsize, + sync::RwLock as StdRwLock, + time::{Duration, Instant}, +}; use async_trait::async_trait; use matrix_sdk_common::ring_buffer::RingBuffer; @@ -28,6 +33,7 @@ use crate::media::{MediaRequest, UniqueKey as _}; #[derive(Debug)] pub struct MemoryStore { media: StdRwLock)>>, + leases: StdRwLock>, } // SAFETY: `new_unchecked` is safe because 20 is not zero. @@ -35,7 +41,10 @@ const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) impl Default for MemoryStore { fn default() -> Self { - Self { media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)) } + Self { + media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)), + leases: Default::default(), + } } } @@ -57,7 +66,44 @@ impl EventCacheStore for MemoryStore { key: &str, holder: &str, ) -> Result { - todo!() + let now = Instant::now(); + let expiration = now + Duration::from_millis(lease_duration_ms.into()); + + match self.leases.write().unwrap().entry(key.to_owned()) { + // There is an existing holder. + Entry::Occupied(mut entry) => { + let (current_holder, current_expiration) = entry.get_mut(); + + if current_holder == holder { + // We had the lease before, extend it. + *current_expiration = expiration; + + Ok(true) + } else { + // We didn't have it. + if *current_expiration < now { + // Steal it! + *current_holder = holder.to_owned(); + *current_expiration = expiration; + + Ok(true) + } else { + // We tried our best. + Ok(false) + } + } + } + + // There is no holder, easy. + Entry::Vacant(entry) => { + entry.insert(( + holder.to_owned(), + Instant::now() + Duration::from_millis(lease_duration_ms.into()), + )); + + Ok(true) + } + } } async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { From 37304c8cdcbe45013a0e55420626421cabdce01c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 30 Oct 2024 16:04:51 +0100 Subject: [PATCH 443/979] refactor: Implement `try_take_leased_lock` on `SqliteEventCacheStore` --- .../event_cache_store/002_lease_locks.sql | 5 +++ .../src/event_cache_store.rs | 41 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 crates/matrix-sdk-sqlite/migrations/event_cache_store/002_lease_locks.sql diff --git a/crates/matrix-sdk-sqlite/migrations/event_cache_store/002_lease_locks.sql b/crates/matrix-sdk-sqlite/migrations/event_cache_store/002_lease_locks.sql new file mode 100644 index 00000000000..e1e32d2c2ad --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/event_cache_store/002_lease_locks.sql @@ -0,0 +1,5 @@ +CREATE TABLE "lease_locks" ( + "key" TEXT PRIMARY KEY NOT NULL, + "holder" TEXT NOT NULL, + "expiration" REAL NOT NULL +); diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 2b760731125..1fc66e31117 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -7,6 +7,7 @@ use matrix_sdk_base::{ media::{MediaRequest, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; +use ruma::MilliSecondsSinceUnixEpoch; use rusqlite::OptionalExtension; use tokio::fs; use tracing::debug; @@ -26,8 +27,8 @@ mod keys { /// /// This is used to figure whether the SQLite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in -/// the [`SqliteEventCacheStore::run_migrations`] function. -const DATABASE_VERSION: u8 = 1; +/// the [`run_migrations`] function. +const DATABASE_VERSION: u8 = 2; /// A SQLite-based event cache store. #[derive(Clone)] @@ -133,6 +134,14 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { .await?; } + if version < 2 { + conn.with_transaction(|txn| { + txn.execute_batch(include_str!("../migrations/event_cache_store/002_lease_locks.sql"))?; + txn.set_db_version(2) + }) + .await?; + } + Ok(()) } @@ -145,8 +154,32 @@ impl EventCacheStore for SqliteEventCacheStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { - todo!() + ) -> Result { + let key = key.to_owned(); + let holder = holder.to_owned(); + + let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into(); + let expiration = now + lease_duration_ms as u64; + + let num_touched = self + .acquire() + .await? + .with_transaction(move |txn| { + txn.execute( + "INSERT INTO lease_locks (key, holder, expiration) + VALUES (?1, ?2, ?3) + ON CONFLICT (key) + DO + UPDATE SET holder = ?2, expiration = ?3 + WHERE holder = ?2 + OR expiration < ?4 + ", + (key, holder, expiration, now), + ) + }) + .await?; + + Ok(num_touched == 1) } async fn add_media_content(&self, request: &MediaRequest, content: Vec) -> Result<()> { From 94c507dd38b57f411861ffa5581d806940bc21d2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 30 Oct 2024 16:13:53 +0100 Subject: [PATCH 444/979] test: Testing the cross-process event cache store. --- .../event_cache_store/integration_tests.rs | 76 +++++++++++++++++++ .../src/event_cache_store/memory_store.rs | 1 + .../src/event_cache_store.rs | 5 +- 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs index ccb92f05005..b9960489980 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs @@ -233,3 +233,79 @@ macro_rules! event_cache_store_integration_tests { } }; } + +/// Macro generating tests for the event cache store, related to time (mostly +/// for the cross-process lock). +#[allow(unused_macros)] +#[macro_export] +macro_rules! event_cache_store_integration_tests_time { + () => { + #[cfg(not(target_arch = "wasm32"))] + mod event_cache_store_integration_tests_time { + use std::time::Duration; + + use matrix_sdk_test::async_test; + use $crate::event_cache_store::IntoEventCacheStore; + + use super::get_event_cache_store; + + #[async_test] + async fn test_lease_locks() { + let store = get_event_cache_store().await.unwrap().into_event_cache_store(); + + let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); + assert!(acquired0); + + // Should extend the lease automatically (same holder). + let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); + assert!(acquired2); + + // Should extend the lease automatically (same holder + time is ok). + let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); + assert!(acquired3); + + // Another attempt at taking the lock should fail, because it's taken. + let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); + assert!(!acquired4); + + // Even if we insist. + let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); + assert!(!acquired5); + + // That's a nice test we got here, go take a little nap. + tokio::time::sleep(Duration::from_millis(50)).await; + + // Still too early. + let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); + assert!(!acquired55); + + // Ok you can take another nap then. + tokio::time::sleep(Duration::from_millis(250)).await; + + // At some point, we do get the lock. + let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap(); + assert!(acquired6); + + tokio::time::sleep(Duration::from_millis(1)).await; + + // The other gets it almost immediately too. + let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); + assert!(acquired7); + + tokio::time::sleep(Duration::from_millis(1)).await; + + // But when we take a longer lease... + let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); + assert!(acquired8); + + // It blocks the other user. + let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); + assert!(!acquired9); + + // We can hold onto our lease. + let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); + assert!(acquired10); + } + } + }; +} diff --git a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs index 0cad147a664..6ea0608e1f4 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs @@ -185,4 +185,5 @@ mod tests { } event_cache_store_integration_tests!(); + event_cache_store_integration_tests_time!(); } diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 1fc66e31117..0caeda04046 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -276,7 +276,7 @@ mod tests { use matrix_sdk_base::{ event_cache_store::{EventCacheStore, EventCacheStoreError}, - event_cache_store_integration_tests, + event_cache_store_integration_tests, event_cache_store_integration_tests_time, media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, }; use matrix_sdk_test::async_test; @@ -300,6 +300,7 @@ mod tests { } event_cache_store_integration_tests!(); + event_cache_store_integration_tests_time!(); async fn get_event_cache_store_content_sorted_by_last_access( event_cache_store: &SqliteEventCacheStore, @@ -381,6 +382,7 @@ mod encrypted_tests { use matrix_sdk_base::{ event_cache_store::EventCacheStoreError, event_cache_store_integration_tests, + event_cache_store_integration_tests_time, }; use once_cell::sync::Lazy; use tempfile::{tempdir, TempDir}; @@ -405,4 +407,5 @@ mod encrypted_tests { } event_cache_store_integration_tests!(); + event_cache_store_integration_tests_time!(); } From 8b85ff24347a088a7d622b61aee05299d011079f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 30 Oct 2024 16:53:45 +0100 Subject: [PATCH 445/979] feat(base): Create `EventCacheStoreLock`. --- .../src/event_cache_store/mod.rs | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache_store/mod.rs b/crates/matrix-sdk-base/src/event_cache_store/mod.rs index 6709ec66420..cd57ad7804b 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/mod.rs @@ -19,7 +19,7 @@ //! into the event cache for the actual storage. By default this brings an //! in-memory store. -use std::{str::Utf8Error, sync::Arc}; +use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc}; #[cfg(any(test, feature = "testing"))] #[macro_use] @@ -27,7 +27,9 @@ pub mod integration_tests; mod memory_store; mod traits; -use matrix_sdk_common::store_locks::BackingStore; +use matrix_sdk_common::store_locks::{ + BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError, +}; pub use matrix_sdk_store_encryption::Error as StoreEncryptionError; #[cfg(any(test, feature = "testing"))] @@ -37,6 +39,75 @@ pub use self::{ traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore}, }; +/// The high-level public type to represent an `EventCacheStore` lock. +pub struct EventCacheStoreLock { + /// The inner cross process lock that is used to lock the `EventCacheStore`. + cross_process_lock: CrossProcessStoreLock, + + /// The store itself. + /// + /// That's the only place where the store exists. + store: Arc, +} + +impl fmt::Debug for EventCacheStoreLock { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.debug_struct("EventCacheStoreLock").finish_non_exhaustive() + } +} + +impl EventCacheStoreLock { + /// Create a new lock around the [`EventCacheStore`]. + pub fn new(store: S, key: String, holder: String) -> Self + where + S: IntoEventCacheStore, + { + let store = store.into_event_cache_store(); + + Self { + cross_process_lock: CrossProcessStoreLock::new( + LockableEventCacheStore(store.clone()), + key, + holder, + ), + store, + } + } + + /// Acquire a spin lock (see [`CrossProcessStoreLock::spin_lock`]). + pub async fn lock(&self) -> Result, LockStoreError> { + let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?; + + Ok(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() }) + } +} + +/// An RAII implementation of a “scoped lock” of an [`EventCacheStoreLock`]. +/// When this structure is dropped (falls out of scope), the lock will be +/// unlocked. +pub struct EventCacheStoreLockGuard<'a> { + /// The cross process lock guard. + #[allow(unused)] + cross_process_lock_guard: CrossProcessStoreLockGuard, + + /// A reference to the store. + store: &'a DynEventCacheStore, +} + +impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive() + } +} + +impl<'a> Deref for EventCacheStoreLockGuard<'a> { + type Target = DynEventCacheStore; + + fn deref(&self) -> &Self::Target { + self.store + } +} + /// Event cache store specific error type. #[derive(Debug, thiserror::Error)] pub enum EventCacheStoreError { From 7b3eb0b6f11662833cb7d6c41dd8b2aa30ade446 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 5 Nov 2024 10:40:50 +0100 Subject: [PATCH 446/979] feat(base,sdk): `Client` now uses `EventCacheStoreLock`. --- crates/matrix-sdk-base/src/client.rs | 15 +++++---- .../src/event_cache_store/mod.rs | 3 ++ crates/matrix-sdk-base/src/store/mod.rs | 19 ++++++++---- crates/matrix-sdk/src/client/builder/mod.rs | 31 ++++++++++++++++--- crates/matrix-sdk/src/client/mod.rs | 4 +-- crates/matrix-sdk/src/media.rs | 13 +++++--- crates/matrix-sdk/src/room/mod.rs | 7 +++-- 7 files changed, 65 insertions(+), 27 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 8dd67f83bde..20801ee022f 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -13,11 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "e2e-encryption")] +use std::sync::Arc; use std::{ collections::{BTreeMap, BTreeSet}, fmt, iter, ops::Deref, - sync::Arc, }; use eyeball::{SharedObservable, Subscriber}; @@ -71,7 +72,7 @@ use crate::RoomMemberships; use crate::{ deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent}, error::{Error, Result}, - event_cache_store::DynEventCacheStore, + event_cache_store::EventCacheStoreLock, response_processors::AccountDataProcessor, rooms::{ normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons}, @@ -95,7 +96,7 @@ pub struct BaseClient { pub(crate) store: Store, /// The store used by the event cache. - event_cache_store: Arc, + event_cache_store: EventCacheStoreLock, /// The store used for encryption. /// @@ -114,8 +115,7 @@ pub struct BaseClient { pub(crate) ignore_user_list_changes: SharedObservable>, /// A sender that is used to communicate changes to room information. Each - /// event contains the room and a boolean whether this event should - /// trigger a room list update. + /// tick contains the room ID and the reasons that have generated this tick. pub(crate) room_info_notable_update_sender: broadcast::Sender, /// The strategy to use for picking recipient devices, when sending an @@ -249,14 +249,13 @@ impl BaseClient { } /// Get a reference to the store. - #[allow(unknown_lints, clippy::explicit_auto_deref)] pub fn store(&self) -> &DynStateStore { self.store.deref() } /// Get a reference to the event cache store. - pub fn event_cache_store(&self) -> &DynEventCacheStore { - self.event_cache_store.as_ref() + pub fn event_cache_store(&self) -> &EventCacheStoreLock { + &self.event_cache_store } /// Is the client logged in. diff --git a/crates/matrix-sdk-base/src/event_cache_store/mod.rs b/crates/matrix-sdk-base/src/event_cache_store/mod.rs index cd57ad7804b..ab41d06327d 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/mod.rs @@ -40,6 +40,7 @@ pub use self::{ }; /// The high-level public type to represent an `EventCacheStore` lock. +#[derive(Clone)] pub struct EventCacheStoreLock { /// The inner cross process lock that is used to lock the `EventCacheStore`. cross_process_lock: CrossProcessStoreLock, @@ -50,6 +51,7 @@ pub struct EventCacheStoreLock { store: Arc, } +#[cfg(not(tarpaulin_include))] impl fmt::Debug for EventCacheStoreLock { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.debug_struct("EventCacheStoreLock").finish_non_exhaustive() @@ -94,6 +96,7 @@ pub struct EventCacheStoreLockGuard<'a> { store: &'a DynEventCacheStore, } +#[cfg(not(tarpaulin_include))] impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive() diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index cd4b7c67cd1..8e430ecdd30 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -60,7 +60,7 @@ use tokio::sync::{broadcast, Mutex, RwLock}; use tracing::warn; use crate::{ - event_cache_store::{DynEventCacheStore, IntoEventCacheStore}, + event_cache_store, rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState}, MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta, }; @@ -489,7 +489,7 @@ pub struct StoreConfig { #[cfg(feature = "e2e-encryption")] pub(crate) crypto_store: Arc, pub(crate) state_store: Arc, - pub(crate) event_cache_store: Arc, + pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock, } #[cfg(not(tarpaulin_include))] @@ -507,8 +507,11 @@ impl StoreConfig { #[cfg(feature = "e2e-encryption")] crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(), state_store: Arc::new(MemoryStore::new()), - event_cache_store: crate::event_cache_store::MemoryStore::new() - .into_event_cache_store(), + event_cache_store: event_cache_store::EventCacheStoreLock::new( + event_cache_store::MemoryStore::new(), + "default-key".to_owned(), + "matrix-sdk-base".to_owned(), + ), } } @@ -528,8 +531,12 @@ impl StoreConfig { } /// Set a custom implementation of an `EventCacheStore`. - pub fn event_cache_store(mut self, event_cache_store: impl IntoEventCacheStore) -> Self { - self.event_cache_store = event_cache_store.into_event_cache_store(); + pub fn event_cache_store(mut self, event_cache_store: S, key: String, holder: String) -> Self + where + S: event_cache_store::IntoEventCacheStore, + { + self.event_cache_store = + event_cache_store::EventCacheStoreLock::new(event_cache_store, key, holder); self } } diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 6c22d148019..078a08f0f2a 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -208,6 +208,7 @@ impl ClientBuilder { path: path.as_ref().to_owned(), cache_path: None, passphrase: passphrase.map(ToOwned::to_owned), + event_cache_store_lock_holder: "matrix-sdk".to_owned(), }; self } @@ -225,6 +226,7 @@ impl ClientBuilder { path: path.as_ref().to_owned(), cache_path: Some(cache_path.as_ref().to_owned()), passphrase: passphrase.map(ToOwned::to_owned), + event_cache_store_lock_holder: "matrix-sdk".to_owned(), }; self } @@ -235,6 +237,7 @@ impl ClientBuilder { self.store_config = BuilderStoreConfig::IndexedDb { name: name.to_owned(), passphrase: passphrase.map(ToOwned::to_owned), + event_cache_store_lock_holder: "matrix-sdk".to_owned(), }; self } @@ -551,7 +554,12 @@ async fn build_store_config( #[allow(clippy::infallible_destructuring_match)] let store_config = match builder_config { #[cfg(feature = "sqlite")] - BuilderStoreConfig::Sqlite { path, cache_path, passphrase } => { + BuilderStoreConfig::Sqlite { + path, + cache_path, + passphrase, + event_cache_store_lock_holder, + } => { let store_config = StoreConfig::new() .state_store( matrix_sdk_sqlite::SqliteStateStore::open(&path, passphrase.as_deref()).await?, @@ -562,6 +570,8 @@ async fn build_store_config( passphrase.as_deref(), ) .await?, + "default-key".to_owned(), + event_cache_store_lock_holder, ); #[cfg(feature = "e2e-encryption")] @@ -573,8 +583,13 @@ async fn build_store_config( } #[cfg(feature = "indexeddb")] - BuilderStoreConfig::IndexedDb { name, passphrase } => { - build_indexeddb_store_config(&name, passphrase.as_deref()).await? + BuilderStoreConfig::IndexedDb { name, passphrase, event_cache_store_lock_holder } => { + build_indexeddb_store_config( + &name, + passphrase.as_deref(), + event_cache_store_lock_holder, + ) + .await? } BuilderStoreConfig::Custom(config) => config, @@ -588,6 +603,7 @@ async fn build_store_config( async fn build_indexeddb_store_config( name: &str, passphrase: Option<&str>, + event_cache_store_lock_holder: String, ) -> Result { #[cfg(feature = "e2e-encryption")] let store_config = { @@ -604,7 +620,11 @@ async fn build_indexeddb_store_config( let store_config = { tracing::warn!("The IndexedDB backend does not implement an event cache store, falling back to the in-memory event cache store…"); - store_config.event_cache_store(matrix_sdk_base::event_cache_store::MemoryStore::new()) + store_config.event_cache_store( + matrix_sdk_base::event_cache_store::MemoryStore::new(), + "default-key".to_owned(), + event_cache_store_lock_holder, + ) }; Ok(store_config) @@ -614,6 +634,7 @@ async fn build_indexeddb_store_config( async fn build_indexeddb_store_config( _name: &str, _passphrase: Option<&str>, + _event_cache_store_lock_holder: String, ) -> Result { panic!("the IndexedDB is only available on the 'wasm32' arch") } @@ -658,11 +679,13 @@ enum BuilderStoreConfig { path: std::path::PathBuf, cache_path: Option, passphrase: Option, + event_cache_store_lock_holder: String, }, #[cfg(feature = "indexeddb")] IndexedDb { name: String, passphrase: Option, + event_cache_store_lock_holder: String, }, Custom(StoreConfig), } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 20268e75d39..e9f0eb763cb 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -33,7 +33,7 @@ use imbl::Vector; #[cfg(feature = "e2e-encryption")] use matrix_sdk_base::crypto::store::LockableCryptoStore; use matrix_sdk_base::{ - event_cache_store::DynEventCacheStore, + event_cache_store::EventCacheStoreLock, store::{DynStateStore, ServerCapabilities}, sync::{Notification, RoomUpdates}, BaseClient, RoomInfoNotableUpdate, RoomState, RoomStateFilter, SendOutsideWasm, SessionMeta, @@ -590,7 +590,7 @@ impl Client { } /// Get a reference to the event cache store. - pub(crate) fn event_cache_store(&self) -> &DynEventCacheStore { + pub(crate) fn event_cache_store(&self) -> &EventCacheStoreLock { self.base_client().event_cache_store() } diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index a25466e0a98..f27845fddb4 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -377,7 +377,7 @@ impl Media { // Read from the cache. if use_cache { if let Some(content) = - self.client.event_cache_store().get_media_content(request).await? + self.client.event_cache_store().lock().await?.get_media_content(request).await? { return Ok(content); } @@ -477,7 +477,12 @@ impl Media { }; if use_cache { - self.client.event_cache_store().add_media_content(request, content.clone()).await?; + self.client + .event_cache_store() + .lock() + .await? + .add_media_content(request, content.clone()) + .await?; } Ok(content) @@ -489,7 +494,7 @@ impl Media { /// /// * `request` - The `MediaRequest` of the content. pub async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { - Ok(self.client.event_cache_store().remove_media_content(request).await?) + Ok(self.client.event_cache_store().lock().await?.remove_media_content(request).await?) } /// Delete all the media content corresponding to the given @@ -499,7 +504,7 @@ impl Media { /// /// * `uri` - The `MxcUri` of the files. pub async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - Ok(self.client.event_cache_store().remove_media_content_for_uri(uri).await?) + Ok(self.client.event_cache_store().lock().await?.remove_media_content_for_uri(uri).await?) } /// Get the file of the given media event content. diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 667b77e6e40..217fd501b0e 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1988,14 +1988,15 @@ impl Room { .await?; if store_in_cache { - let cache_store = self.client.event_cache_store(); + let cache_store_lock_guard = self.client.event_cache_store().lock().await?; // A failure to cache shouldn't prevent the whole upload from finishing // properly, so only log errors during caching. debug!("caching the media"); let request = MediaRequest { source: media_source.clone(), format: MediaFormat::File }; - if let Err(err) = cache_store.add_media_content(&request, data).await { + + if let Err(err) = cache_store_lock_guard.add_media_content(&request, data).await { warn!("unable to cache the media after uploading it: {err}"); } @@ -2018,7 +2019,7 @@ impl Room { }), }; - if let Err(err) = cache_store.add_media_content(&request, data).await { + if let Err(err) = cache_store_lock_guard.add_media_content(&request, data).await { warn!("unable to cache the media after uploading it: {err}"); } } From 94bd421a8db199fe6aedd7e12b6a1788059bda32 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 6 Nov 2024 10:38:38 +0100 Subject: [PATCH 447/979] refactor: Use a common code for `try_take_leased_lock`. This code is shared by all `MemoryStore` implementations. --- .../src/event_cache_store/memory_store.rs | 50 +--------- crates/matrix-sdk-common/src/store_locks.rs | 95 ++++++++++++------- .../src/store/memorystore.rs | 36 +------ 3 files changed, 70 insertions(+), 111 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs index 6ea0608e1f4..d75c9f09776 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs @@ -12,15 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::{hash_map::Entry, HashMap}, - num::NonZeroUsize, - sync::RwLock as StdRwLock, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant}; use async_trait::async_trait; -use matrix_sdk_common::ring_buffer::RingBuffer; +use matrix_sdk_common::{ + ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, +}; use ruma::{MxcUri, OwnedMxcUri}; use super::{EventCacheStore, EventCacheStoreError, Result}; @@ -66,44 +63,7 @@ impl EventCacheStore for MemoryStore { key: &str, holder: &str, ) -> Result { - let now = Instant::now(); - let expiration = now + Duration::from_millis(lease_duration_ms.into()); - - match self.leases.write().unwrap().entry(key.to_owned()) { - // There is an existing holder. - Entry::Occupied(mut entry) => { - let (current_holder, current_expiration) = entry.get_mut(); - - if current_holder == holder { - // We had the lease before, extend it. - *current_expiration = expiration; - - Ok(true) - } else { - // We didn't have it. - if *current_expiration < now { - // Steal it! - *current_holder = holder.to_owned(); - *current_expiration = expiration; - - Ok(true) - } else { - // We tried our best. - Ok(false) - } - } - } - - // There is no holder, easy. - Entry::Vacant(entry) => { - entry.insert(( - holder.to_owned(), - Instant::now() + Duration::from_millis(lease_duration_ms.into()), - )); - - Ok(true) - } - } + Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) } async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { diff --git a/crates/matrix-sdk-common/src/store_locks.rs b/crates/matrix-sdk-common/src/store_locks.rs index 6ee4de48a0f..13350c1fa25 100644 --- a/crates/matrix-sdk-common/src/store_locks.rs +++ b/crates/matrix-sdk-common/src/store_locks.rs @@ -338,7 +338,7 @@ pub enum LockStoreError { mod tests { use std::{ collections::HashMap, - sync::{atomic, Arc, Mutex}, + sync::{atomic, Arc, RwLock}, time::Instant, }; @@ -350,47 +350,18 @@ mod tests { }; use super::{ - BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError, - EXTEND_LEASE_EVERY_MS, + memory_store_helper::try_take_leased_lock, BackingStore, CrossProcessStoreLock, + CrossProcessStoreLockGuard, LockStoreError, EXTEND_LEASE_EVERY_MS, }; #[derive(Clone, Default)] struct TestStore { - leases: Arc>>, + leases: Arc>>, } impl TestStore { fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool { - let now = Instant::now(); - let expiration = now + Duration::from_millis(lease_duration_ms.into()); - let mut leases = self.leases.lock().unwrap(); - if let Some(prev) = leases.get_mut(key) { - if prev.0 == holder { - // We had the lease before, extend it. - prev.1 = expiration; - true - } else { - // We didn't have it. - if prev.1 < now { - // Steal it! - prev.0 = holder.to_owned(); - prev.1 = expiration; - true - } else { - // We tried our best. - false - } - } - } else { - leases.insert( - key.to_owned(), - ( - holder.to_owned(), - Instant::now() + Duration::from_millis(lease_duration_ms.into()), - ), - ); - true - } + try_take_leased_lock(&self.leases, lease_duration_ms, key, holder) } } @@ -525,3 +496,59 @@ mod tests { Ok(()) } } + +/// Some code that is shared by almost all `MemoryStore` implementations out +/// there. +pub mod memory_store_helper { + use std::{ + collections::{hash_map::Entry, HashMap}, + sync::RwLock, + time::{Duration, Instant}, + }; + + pub fn try_take_leased_lock( + leases: &RwLock>, + lease_duration_ms: u32, + key: &str, + holder: &str, + ) -> bool { + let now = Instant::now(); + let expiration = now + Duration::from_millis(lease_duration_ms.into()); + + match leases.write().unwrap().entry(key.to_owned()) { + // There is an existing holder. + Entry::Occupied(mut entry) => { + let (current_holder, current_expiration) = entry.get_mut(); + + if current_holder == holder { + // We had the lease before, extend it. + *current_expiration = expiration; + + true + } else { + // We didn't have it. + if *current_expiration < now { + // Steal it! + *current_holder = holder.to_owned(); + *current_expiration = expiration; + + true + } else { + // We tried our best. + false + } + } + } + + // There is no holder, easy. + Entry::Vacant(entry) => { + entry.insert(( + holder.to_owned(), + Instant::now() + Duration::from_millis(lease_duration_ms.into()), + )); + + true + } + } + } +} diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index b773d819d94..6b177ea2641 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -13,13 +13,14 @@ // limitations under the License. use std::{ - collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, convert::Infallible, sync::RwLock as StdRwLock, - time::{Duration, Instant}, + time::Instant, }; use async_trait::async_trait; +use matrix_sdk_common::store_locks::memory_store_helper::try_take_leased_lock; use ruma::{ events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, @@ -631,36 +632,7 @@ impl CryptoStore for MemoryStore { key: &str, holder: &str, ) -> Result { - let now = Instant::now(); - let expiration = now + Duration::from_millis(lease_duration_ms.into()); - match self.leases.write().unwrap().entry(key.to_owned()) { - Entry::Occupied(mut o) => { - let prev = o.get_mut(); - if prev.0 == holder { - // We had the lease before, extend it. - prev.1 = expiration; - Ok(true) - } else { - // We didn't have it. - if prev.1 < now { - // Steal it! - prev.0 = holder.to_owned(); - prev.1 = expiration; - Ok(true) - } else { - // We tried our best. - Ok(false) - } - } - } - Entry::Vacant(v) => { - v.insert(( - holder.to_owned(), - Instant::now() + Duration::from_millis(lease_duration_ms.into()), - )); - Ok(true) - } - } + Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) } } From 0942dab2fdaaabfb2d3be05baf9641b1f6ab3cf1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 6 Nov 2024 13:09:37 +0100 Subject: [PATCH 448/979] doc(base): Document `Client::event_cache_store` a bit more. --- crates/matrix-sdk-base/src/store/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 8e430ecdd30..b68b668009c 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -531,6 +531,9 @@ impl StoreConfig { } /// Set a custom implementation of an `EventCacheStore`. + /// + /// The `key` and `holder` arguments represent the key and holder inside the + /// [`CrossProcessStoreLock::new`][matrix_sdk_common::store_locks::CrossProcessStoreLock::new]. pub fn event_cache_store(mut self, event_cache_store: S, key: String, holder: String) -> Self where S: event_cache_store::IntoEventCacheStore, From 9483703e357d65869924a7ba9eb4ce213b56ced2 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 30 Oct 2024 16:48:16 +0100 Subject: [PATCH 449/979] feat(send queue): allow sending attachments with the send queue --- bindings/matrix-sdk-ffi/src/client.rs | 2 + bindings/matrix-sdk-ffi/src/error.rs | 17 + crates/matrix-sdk-base/src/media.rs | 9 +- .../src/store/integration_tests.rs | 4 +- crates/matrix-sdk-base/src/store/mod.rs | 5 +- .../matrix-sdk-base/src/store/send_queue.rs | 102 ++- .../src/timeline/controller/mod.rs | 5 + crates/matrix-sdk/src/room/mod.rs | 4 +- crates/matrix-sdk/src/send_queue.rs | 759 ++++++++++++++++-- 9 files changed, 824 insertions(+), 83 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index ea37febad8f..dd9f6219d82 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -717,6 +717,7 @@ impl Client { ) -> Result, ClientError> { let source = (*media_source).clone(); + debug!(?source, "requesting media file"); Ok(self .inner .media() @@ -732,6 +733,7 @@ impl Client { ) -> Result, ClientError> { let source = (*media_source).clone(); + debug!(source = ?media_source, width, height, "requesting media thumbnail"); Ok(self .inner .media() diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index e644d7974d8..9d793b07020 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -183,6 +183,12 @@ pub enum QueueWedgeError { /// session before sending. CrossVerificationRequired, + /// Some media content to be sent has disappeared from the cache. + MissingMediaContent, + + /// Some mime type couldn't be parsed. + InvalidMimeType { mime_type: String }, + /// Other errors. GenericApiError { msg: String }, } @@ -201,10 +207,17 @@ impl Display for QueueWedgeError { QueueWedgeError::CrossVerificationRequired => { f.write_str("Own verification is required") } + QueueWedgeError::MissingMediaContent => { + f.write_str("Media to be sent disappeared from local storage") + } + QueueWedgeError::InvalidMimeType { mime_type } => { + write!(f, "Invalid mime type '{mime_type}' for media upload") + } QueueWedgeError::GenericApiError { msg } => f.write_str(msg), } } } + impl From for QueueWedgeError { fn from(value: SdkQueueWedgeError) -> Self { match value { @@ -223,6 +236,10 @@ impl From for QueueWedgeError { users: users.iter().map(ruma::OwnedUserId::to_string).collect(), }, SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired, + SdkQueueWedgeError::MissingMediaContent => Self::MissingMediaContent, + SdkQueueWedgeError::InvalidMimeType { mime_type } => { + Self::InvalidMimeType { mime_type } + } SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg }, } } diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index 6d02683a5f8..15950be168c 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -14,6 +14,7 @@ use ruma::{ }, MxcUri, UInt, }; +use serde::{Deserialize, Serialize}; const UNIQUE_SEPARATOR: &str = "_"; @@ -25,7 +26,7 @@ pub trait UniqueKey { } /// The requested format of a media file. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum MediaFormat { /// The file that was uploaded. File, @@ -44,7 +45,7 @@ impl UniqueKey for MediaFormat { } /// The requested size of a media thumbnail. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MediaThumbnailSize { /// The desired resizing method. pub method: Method, @@ -65,7 +66,7 @@ impl UniqueKey for MediaThumbnailSize { } /// The desired settings of a media thumbnail. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MediaThumbnailSettings { /// The desired size of the thumbnail. pub size: MediaThumbnailSize, @@ -110,7 +111,7 @@ impl UniqueKey for MediaSource { } /// A request for media data. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MediaRequest { /// The source of the media file. pub source: MediaSource, diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 131f919c2c4..e439965c416 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1406,7 +1406,9 @@ impl StateStoreIntegrationTests for DynStateStore { assert_eq!(dependents.len(), 1); assert_eq!(dependents[0].parent_transaction_id, txn0); assert_eq!(dependents[0].own_transaction_id, child_txn); - assert_eq!(dependents[0].parent_key.as_ref(), Some(&SentRequestKey::Event(event_id))); + assert_matches!(dependents[0].parent_key.as_ref(), Some(SentRequestKey::Event(eid)) => { + assert_eq!(*eid, event_id); + }); assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent); // Now remove it. diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index b68b668009c..a40973870b3 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -75,8 +75,9 @@ pub use self::integration_tests::StateStoreIntegrationTests; pub use self::{ memory_store::MemoryStore, send_queue::{ - ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, - QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent, + ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, + FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, + SentRequestKey, SerializableEventContent, }, traits::{ ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities, diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 7efe0f13633..c657881c930 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -18,12 +18,17 @@ use std::{collections::BTreeMap, fmt, ops::Deref}; use as_variant::as_variant; use ruma::{ - events::{AnyMessageLikeEventContent, EventContent as _, RawExt as _}, + events::{ + room::{message::RoomMessageEventContent, MediaSource}, + AnyMessageLikeEventContent, EventContent as _, RawExt as _, + }, serde::Raw, - OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, + OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, UInt, }; use serde::{Deserialize, Serialize}; +use crate::media::MediaRequest; + /// A thin wrapper to serialize a `AnyMessageLikeEventContent`. #[derive(Clone, Serialize, Deserialize)] pub struct SerializableEventContent { @@ -76,6 +81,28 @@ pub enum QueuedRequestKind { /// The content of the message-like event we'd like to send. content: SerializableEventContent, }, + + /// Content to upload on the media server. + /// + /// The bytes must be stored in the media cache, and are identified by the + /// cache key. + Upload { + /// Content type of the media to be uploaded. + /// + /// Stored as a `String` because `Mime` which we'd really want to use + /// here, is not serializable. Oh well. + content_type: String, + + /// The cache key used to retrieve the media's bytes in the event cache + /// store. + cache_key: MediaRequest, + + /// An optional media source for a thumbnail already uploaded. + thumbnail_source: Option, + + /// To which media event transaction does this upload relate? + related_to: OwnedTransactionId, + }, } impl From for QueuedRequestKind { @@ -143,6 +170,18 @@ pub enum QueueWedgeError { #[error("Own verification is required")] CrossVerificationRequired, + /// Media content was cached in the media store, but has disappeared before + /// we could upload it. + #[error("Media content disappeared")] + MissingMediaContent, + + /// We tried to upload some media content with an unknown mime type. + #[error("Invalid mime type '{mime_type}' for media")] + InvalidMimeType { + /// The observed mime type that's expected to be invalid. + mime_type: String, + }, + /// Other errors. #[error("Other unrecoverable error: {msg}")] GenericApiError { @@ -169,6 +208,43 @@ pub enum DependentQueuedRequestKind { /// Key used for the reaction. key: String, }, + + /// Upload a file that had a thumbnail. + UploadFileWithThumbnail { + /// Content type for the file itself (not the thumbnail). + content_type: String, + + /// Media request necessary to retrieve the file itself (not the + /// thumbnail). + cache_key: MediaRequest, + + /// To which media transaction id does this upload relate to? + related_to: OwnedTransactionId, + }, + + /// Finish an upload by updating references to the media cache and sending + /// the final media event with the remote MXC URIs. + FinishUpload { + /// Local echo for the event (containing the local MXC URIs). + local_echo: RoomMessageEventContent, + + /// Transaction id for the file upload. + file_upload: OwnedTransactionId, + + /// Information about the thumbnail, if present. + thumbnail_info: Option, + }, +} + +/// Detailed record about a thumbnail used when finishing a media upload. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FinishUploadThumbnailInfo { + /// Transaction id for the thumbnail upload. + pub txn: OwnedTransactionId, + /// Thumbnail's width. + pub width: UInt, + /// Thumbnail's height. + pub height: UInt, } /// A transaction id identifying a [`DependentQueuedRequest`] rather than its @@ -210,14 +286,34 @@ impl From for OwnedTransactionId { } } +impl From for ChildTransactionId { + fn from(val: OwnedTransactionId) -> Self { + Self(val) + } +} + /// A unique key (identifier) indicating that a transaction has been /// successfully sent to the server. /// /// The owning child transactions can now be resolved. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum SentRequestKey { /// The parent transaction returned an event when it succeeded. Event(OwnedEventId), + + /// The parent transaction returned an uploaded resource URL. + Media { + /// File that was uploaded by this request. + /// + /// If the request related to a thumbnail upload, this contains the + /// thumbnail media source. + file: MediaSource, + + /// Optional thumbnail previously uploaded, when uploading a file. + /// + /// When uploading a thumbnail, this is set to `None`. + thumbnail: Option, + }, } impl SentRequestKey { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index cd1c39b14d5..8f09d4f3095 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1359,6 +1359,11 @@ impl TimelineController

{ self.update_event_send_state(&transaction_id, EventSendState::Sent { event_id }) .await; } + + RoomSendQueueUpdate::UploadedMedia { related_to, .. } => { + // TODO(bnjbvr): Do something else? + info!(txn_id = %related_to, "some media for a media event has been uploaded"); + } } } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 217fd501b0e..8253449c17c 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2048,7 +2048,7 @@ impl Room { /// Creates the inner [`MessageType`] for an already-uploaded media file /// provided by its source. #[allow(clippy::too_many_arguments)] - fn make_attachment_type( + pub(crate) fn make_attachment_type( &self, content_type: &Mime, filename: &str, @@ -2134,7 +2134,7 @@ impl Room { /// Creates the [`RoomMessageEventContent`] based on the message type and /// mentions. - fn make_attachment_event( + pub(crate) fn make_attachment_event( msg_type: MessageType, mentions: Option, ) -> RoomMessageEventContent { diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 6a330844f0d..b23c4cb2aac 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -45,34 +45,45 @@ use std::{ collections::{BTreeMap, BTreeSet, HashMap}, + str::FromStr as _, sync::{ atomic::{AtomicBool, Ordering}, Arc, RwLock as SyncRwLock, }, }; +use as_variant::as_variant; use matrix_sdk_base::{ + event_cache_store::EventCacheStoreError, + media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, store::{ - ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, - QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent, + ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, + FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, + SentRequestKey, SerializableEventContent, }, RoomState, StoreError, }; use matrix_sdk_common::executor::{spawn, JoinHandle}; +use mime::Mime; use ruma::{ + assign, events::{ - reaction::ReactionEventContent, relation::Annotation, AnyMessageLikeEventContent, - EventContent as _, + reaction::ReactionEventContent, + relation::Annotation, + room::{message::MessageType, MediaSource, ThumbnailInfo}, + AnyMessageLikeEventContent, EventContent as _, }, + media::Method, serde::Raw, - OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, + uint, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, TransactionId, UInt, }; use tokio::sync::{broadcast, Notify, RwLock}; -use tracing::{debug, error, info, instrument, trace, warn}; +use tracing::{debug, error, info, instrument, trace, warn, Span}; #[cfg(feature = "e2e-encryption")] use crate::crypto::{OlmError, SessionRecipientCollectionError}; use crate::{ + attachment::AttachmentConfig, client::WeakClient, config::RequestConfig, error::RetryKind, @@ -350,19 +361,22 @@ impl RoomSendQueue { self.inner.notifier.notify_one(); - let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + let send_handle = SendHandle { + room: self.clone(), transaction_id: transaction_id.clone(), + is_upload: false, + }; + + let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id, content: LocalEchoContent::Event { serialized_event: content, - send_handle: SendHandle { - room: self.clone(), - transaction_id: transaction_id.clone(), - }, + send_handle: send_handle.clone(), send_error: None, }, })); - Ok(SendHandle { transaction_id, room: self.clone() }) + Ok(send_handle) } /// Queues an event for sending it to this room. @@ -390,6 +404,269 @@ impl RoomSendQueue { .await } + /// Queues an attachment to be sent to the room, using the send queue. + /// + /// This returns quickly (without sending or uploading anything), and will + /// push the event to be sent into a queue, handled in the background. + /// + /// Callers are expected to consume [`RoomSendQueueUpdate`] via calling + /// the [`Self::subscribe()`] method to get updates about the sending of + /// that event. + /// + /// By default, if sending failed on the first attempt, it will be retried a + /// few times. If sending failed after those retries, the entire + /// client's sending queue will be disabled, and it will need to be + /// manually re-enabled by the caller (e.g. after network is back, or when + /// something has been done about the faulty requests). + #[instrument(skip_all, fields(event_txn))] + pub async fn send_attachment( + &self, + filename: &str, + content_type: Mime, + data: Vec, + mut config: AttachmentConfig, + ) -> Result { + let Some(room) = self.inner.room.get() else { + return Err(RoomSendQueueError::RoomDisappeared); + }; + if room.state() != RoomState::Joined { + return Err(RoomSendQueueError::RoomNotJoined); + } + + let client = room.client(); + let store = client.store(); + + debug!(filename, %content_type, "sending an attachment"); + + // Push the dependent requests first, to make sure we're not sending the parent + // (depended upon) while dependencies aren't known yet. + + let upload_file_txn = TransactionId::new(); + let send_event_txn = config.txn_id.map_or_else(ChildTransactionId::new, Into::into); + + Span::current().record("event_txn", tracing::field::display(&*send_event_txn)); + + // Cache medias. + + // Prepare and cache the file. + let file_media_request = Self::make_local_file_media_request(&upload_file_txn); + + room.client() + .event_cache_store() + .add_media_content(&file_media_request, data.clone()) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + let (event_content, thumbnail_txn) = if let Some(thumbnail) = config.thumbnail.take() { + let info = thumbnail.info.as_ref(); + let height = info.and_then(|info| info.height).unwrap_or_else(|| { + trace!("thumbnail height is unknown, using 0 for the cache entry"); + uint!(0) + }); + let width = info.and_then(|info| info.width).unwrap_or_else(|| { + trace!("thumbnail width is unknown, using 0 for the cache entry"); + uint!(0) + }); + + let thumbnail_upload_txn = TransactionId::new(); + trace!( + %upload_file_txn, + %thumbnail_upload_txn, + thumbnail_size = ?(height, width), + "attachment has a thumbnail" + ); + + let media_request = + Self::make_local_thumbnail_media_request(&thumbnail_upload_txn, height, width); + + room.client() + .event_cache_store() + .add_media_content(&media_request, thumbnail.data.clone()) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + let thumbnail_info = + Box::new(assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { + mimetype: Some(thumbnail.content_type.as_ref().to_owned()) + })); + + // Save the event sending request as a dependent request on the file upload. + let content = Room::make_attachment_event( + room.make_attachment_type( + &content_type, + filename, + file_media_request.source.clone(), + config.caption, + config.formatted_caption, + config.info, + Some((media_request.source.clone(), thumbnail_info)), + ), + config.mentions, + ); + + store + .save_dependent_queued_request( + room.room_id(), + &upload_file_txn, + send_event_txn.clone(), + DependentQueuedRequestKind::FinishUpload { + local_echo: content.clone(), + file_upload: upload_file_txn.clone(), + thumbnail_info: Some(FinishUploadThumbnailInfo { + txn: thumbnail_upload_txn.clone(), + height, + width, + }), + }, + ) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + // Save the file upload request as a dependent request of the thumbnail upload. + store + .save_dependent_queued_request( + room.room_id(), + &thumbnail_upload_txn, + upload_file_txn.clone().into(), + DependentQueuedRequestKind::UploadFileWithThumbnail { + content_type: content_type.to_string(), + cache_key: file_media_request, + related_to: send_event_txn.clone().into(), + }, + ) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + // Save the thumbnail upload request. + store + .save_send_queue_request( + room.room_id(), + thumbnail_upload_txn.clone(), + QueuedRequestKind::Upload { + content_type: thumbnail.content_type.to_string(), + cache_key: media_request, + thumbnail_source: None, + related_to: send_event_txn.clone().into(), + }, + ) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + (content, Some(thumbnail_upload_txn)) + } else { + // No thumbnail: only save the file upload request and send the event as a + // dependency. + + trace!(%upload_file_txn, "attachment has no thumbnails"); + + let content = Room::make_attachment_event( + room.make_attachment_type( + &content_type, + filename, + file_media_request.source.clone(), + config.caption, + config.formatted_caption, + config.info, + None, + ), + config.mentions, + ); + + store + .save_dependent_queued_request( + room.room_id(), + &upload_file_txn, + send_event_txn.clone(), + DependentQueuedRequestKind::FinishUpload { + local_echo: content.clone(), + file_upload: upload_file_txn.clone(), + thumbnail_info: None, + }, + ) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + store + .save_send_queue_request( + room.room_id(), + upload_file_txn.clone(), + QueuedRequestKind::Upload { + content_type: content_type.to_string(), + cache_key: file_media_request, + thumbnail_source: None, + related_to: send_event_txn.clone().into(), + }, + ) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + // No thumbnail attachment. + (content, None) + }; + + let send_event_txn = OwnedTransactionId::from(send_event_txn); + trace!("manager sends a media to the background task"); + + self.inner.notifier.notify_one(); + + let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: send_event_txn.clone(), + content: LocalEchoContent::Event { + serialized_event: SerializableEventContent::new(&event_content.into()) + .map_err(RoomSendQueueStorageError::JsonSerialization)?, + // TODO: this should be a `SendAttachmentHandle`! + send_handle: SendHandle { + room: self.clone(), + transaction_id: send_event_txn.clone(), + is_upload: true, + }, + send_error: None, + }, + })); + + Ok(SendAttachmentHandle { + _room: self.clone(), + _transaction_id: send_event_txn, + _file_upload: upload_file_txn, + _thumbnail_transaction_id: thumbnail_txn, + }) + } + + /// Create a [`MediaRequest`] for a file we want to store locally before + /// sending it. + /// + /// This uses a MXC ID that is only locally valid. + fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequest { + // .local is guaranteed to be on the local network. It would be a shame that + // `send-queue.local` resolves to an actual Synapse media server, we don't + // expect this to be likely though. + MediaRequest { + source: MediaSource::Plain(OwnedMxcUri::from(format!( + "mxc://send-queue.local/{txn_id}" + ))), + format: MediaFormat::File, + } + } + + /// Create a [`MediaRequest`] for a file we want to store locally before + /// sending it. + /// + /// This uses a MXC ID that is only locally valid. + fn make_local_thumbnail_media_request( + txn_id: &TransactionId, + height: UInt, + width: UInt, + ) -> MediaRequest { + // See comment in [`Self::make_local_file_media_request`]. + let source = + MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.local/{}", txn_id))); + let format = MediaFormat::Thumbnail(MediaThumbnailSettings { + size: MediaThumbnailSize { method: Method::Scale, width, height }, + animated: false, + }); + MediaRequest { source, format } + } + /// Returns the current local requests as well as a receiver to listen to /// the send queue updates, as defined in [`RoomSendQueueUpdate`]. pub async fn subscribe( @@ -427,10 +704,15 @@ impl RoomSendQueue { // Try to apply dependent requests now; those applying to previously failed // attempts (local echoes) would succeed now. - if let Err(err) = queue.apply_dependent_requests().await { + let mut new_updates = Vec::new(); + if let Err(err) = queue.apply_dependent_requests(&mut new_updates).await { warn!("errors when applying dependent requests: {err}"); } + for up in new_updates { + let _ = updates.send(up); + } + if !locally_enabled.load(Ordering::SeqCst) { trace!("not enabled, sleeping"); // Wait for an explicit wakeup. @@ -454,7 +736,10 @@ impl RoomSendQueue { } }; - trace!(txn_id = %queued_request.transaction_id, "received a request to send!"); + let txn_id = queued_request.transaction_id.clone(); + trace!(txn_id = %txn_id, "received a request to send!"); + + let related_txn_id = as_variant!(&queued_request.kind, QueuedRequestKind::Upload { related_to, .. } => related_to.clone()); let Some(room) = room.get() else { if is_dropping.load(Ordering::SeqCst) { @@ -464,18 +749,22 @@ impl RoomSendQueue { continue; }; - match Self::handle_request(&room, &queued_request).await { - Ok(parent_key) => match queue - .mark_as_sent(&queued_request.transaction_id, parent_key.clone()) - .await - { + match Self::handle_request(&room, queued_request).await { + Ok(parent_key) => match queue.mark_as_sent(&txn_id, parent_key.clone()).await { Ok(()) => match parent_key { SentRequestKey::Event(event_id) => { let _ = updates.send(RoomSendQueueUpdate::SentEvent { - transaction_id: queued_request.transaction_id, + transaction_id: txn_id, event_id, }); } + + SentRequestKey::Media { file, .. } => { + let _ = updates.send(RoomSendQueueUpdate::UploadedMedia { + related_to: related_txn_id.as_ref().unwrap_or(&txn_id).clone(), + file, + }); + } }, Err(err) => { @@ -504,11 +793,11 @@ impl RoomSendQueue { }; if is_recoverable { - warn!(txn_id = %queued_request.transaction_id, error = ?err, "Recoverable error when sending request: {err}, disabling send queue"); + warn!(txn_id = %txn_id, error = ?err, "Recoverable error when sending request: {err}, disabling send queue"); // In this case, we intentionally keep the request in the queue, but mark it // as not being sent anymore. - queue.mark_as_not_being_sent(&queued_request.transaction_id).await; + queue.mark_as_not_being_sent(&txn_id).await; // Let observers know about a failure *after* we've marked the item as not // being sent anymore. Otherwise, there's a possible race where a caller @@ -520,16 +809,11 @@ impl RoomSendQueue { // disconnected, maybe the server had a hiccup). locally_enabled.store(false, Ordering::SeqCst); } else { - warn!(txn_id = %queued_request.transaction_id, error = ?err, "Unrecoverable error when sending request: {err}"); + warn!(txn_id = %txn_id, error = ?err, "Unrecoverable error when sending request: {err}"); // Mark the request as wedged, so it's not picked at any future point. - - if let Err(storage_error) = queue - .mark_as_wedged( - &queued_request.transaction_id, - QueueWedgeError::from(&err), - ) - .await + if let Err(storage_error) = + queue.mark_as_wedged(&txn_id, QueueWedgeError::from(&err)).await { warn!("unable to mark request as wedged: {storage_error}"); } @@ -544,7 +828,7 @@ impl RoomSendQueue { }); let _ = updates.send(RoomSendQueueUpdate::SendError { - transaction_id: queued_request.transaction_id, + transaction_id: related_txn_id.unwrap_or(txn_id), error, is_recoverable, }); @@ -558,9 +842,9 @@ impl RoomSendQueue { /// Handles a single request and returns the [`SentRequestKey`] on success. async fn handle_request( room: &Room, - request: &QueuedRequest, + request: QueuedRequest, ) -> Result { - match &request.kind { + match request.kind { QueuedRequestKind::Event { content } => { let (event, event_type) = content.raw(); @@ -573,6 +857,56 @@ impl RoomSendQueue { trace!(txn_id = %request.transaction_id, event_id = %res.event_id, "event successfully sent"); Ok(SentRequestKey::Event(res.event_id)) } + + QueuedRequestKind::Upload { + content_type, + cache_key, + thumbnail_source, + related_to: relates_to, + } => { + trace!(%relates_to, "uploading media related to event"); + + let mime = Mime::from_str(&content_type).map_err(|_| { + crate::Error::SendQueueWedgeError(QueueWedgeError::InvalidMimeType { + mime_type: content_type.clone(), + }) + })?; + + let Some(data) = + room.client().event_cache_store().get_media_content(&cache_key).await? + else { + return Err(crate::Error::SendQueueWedgeError( + QueueWedgeError::MissingMediaContent, + )); + }; + + #[cfg(feature = "e2e-encryption")] + let media_source = if room.is_encrypted().await? { + trace!("upload will be encrypted (encrypted room)"); + let mut cursor = std::io::Cursor::new(data); + let encrypted_file = + room.client().upload_encrypted_file(&mime, &mut cursor).await?; + MediaSource::Encrypted(Box::new(encrypted_file)) + } else { + trace!("upload will be in clear text (room without encryption)"); + let res = room.client().media().upload(&mime, data).await?; + MediaSource::Plain(res.content_uri) + }; + + #[cfg(not(feature = "e2e-encryption"))] + let media_source = { + let res = room.client().media().upload(&mime, data).await?; + MediaSource::Plain(res.content_uri) + }; + + let uri = match &media_source { + MediaSource::Plain(uri) => uri, + MediaSource::Encrypted(encrypted_file) => &encrypted_file.url, + }; + trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded"); + + Ok(SentRequestKey::Media { file: media_source, thumbnail: thumbnail_source }) + } } } @@ -633,6 +967,9 @@ impl From<&crate::Error> for QueueWedgeError { } }, + // Flatten errors of `Self` type. + crate::Error::SendQueueWedgeError(error) => error.clone(), + _ => QueueWedgeError::GenericApiError { msg: value.to_string() }, } } @@ -908,8 +1245,8 @@ impl QueueStorage { let store = client.store(); let local_requests = - store.load_send_queue_requests(&self.room_id).await?.into_iter().map(|queued| { - LocalEcho { + store.load_send_queue_requests(&self.room_id).await?.into_iter().filter_map(|queued| { + Some(LocalEcho { transaction_id: queued.transaction_id.clone(), content: match queued.kind { QueuedRequestKind::Event { content } => LocalEchoContent::Event { @@ -917,36 +1254,72 @@ impl QueueStorage { send_handle: SendHandle { room: room.clone(), transaction_id: queued.transaction_id, + is_upload: false, }, send_error: queued.error, }, + + QueuedRequestKind::Upload { .. } => { + // Don't return uploaded medias as their own things; the accompanying + // event represented as a dependent request should be sufficient. + return None; + } }, - } + }) }); - let local_reactions = - store.load_dependent_queued_requests(&self.room_id).await?.into_iter().filter_map( - |dep| match dep.kind { - DependentQueuedRequestKind::EditEvent { .. } - | DependentQueuedRequestKind::RedactEvent => { - // TODO: reflect local edits/redacts too? - None - } - DependentQueuedRequestKind::ReactEvent { key } => Some(LocalEcho { + let reactions_and_medias = store + .load_dependent_queued_requests(&self.room_id) + .await? + .into_iter() + .filter_map(|dep| match dep.kind { + DependentQueuedRequestKind::EditEvent { .. } + | DependentQueuedRequestKind::RedactEvent => { + // TODO: reflect local edits/redacts too? + None + } + + DependentQueuedRequestKind::ReactEvent { key } => Some(LocalEcho { + transaction_id: dep.own_transaction_id.clone().into(), + content: LocalEchoContent::React { + key, + send_handle: SendReactionHandle { + room: room.clone(), + transaction_id: dep.own_transaction_id, + }, + applies_to: dep.parent_transaction_id, + }, + }), + + DependentQueuedRequestKind::UploadFileWithThumbnail { .. } => { + // Don't reflect these: only the associated event is interesting to observers. + None + } + + DependentQueuedRequestKind::FinishUpload { + local_echo, + file_upload: _, + thumbnail_info: _, + } => { + // Materialize as an event local echo. + Some(LocalEcho { transaction_id: dep.own_transaction_id.clone().into(), - content: LocalEchoContent::React { - key, - send_handle: SendReactionHandle { + content: LocalEchoContent::Event { + serialized_event: SerializableEventContent::new(&local_echo.into()) + .ok()?, + // TODO this should be a `SendAttachmentHandle`! + send_handle: SendHandle { room: room.clone(), - transaction_id: dep.own_transaction_id, + transaction_id: dep.own_transaction_id.into(), + is_upload: true, }, - applies_to: dep.parent_transaction_id, + send_error: None, }, - }), - }, - ); + }) + } + }); - Ok(local_requests.chain(local_reactions).collect()) + Ok(local_requests.chain(reactions_and_medias).collect()) } /// Try to apply a single dependent request, whether it's local or remote. @@ -961,6 +1334,7 @@ impl QueueStorage { &self, client: &Client, dependent_request: DependentQueuedRequest, + new_updates: &mut Vec, ) -> Result { let store = client.store(); @@ -1030,7 +1404,7 @@ impl QueueStorage { serializable.into(), ) .await - .map_err(RoomSendQueueStorageError::StorageError)?; + .map_err(RoomSendQueueStorageError::StateStoreError)?; } else { // The parent event is still local; update the local echo. let edited = store @@ -1040,7 +1414,7 @@ impl QueueStorage { new_content.into(), ) .await - .map_err(RoomSendQueueStorageError::StorageError)?; + .map_err(RoomSendQueueStorageError::StateStoreError)?; if !edited { warn!("missing local echo upon dependent edit"); @@ -1084,7 +1458,7 @@ impl QueueStorage { &dependent_request.parent_transaction_id, ) .await - .map_err(RoomSendQueueStorageError::StorageError)?; + .map_err(RoomSendQueueStorageError::StateStoreError)?; if !removed { warn!("missing local echo upon dependent redact"); @@ -1116,19 +1490,214 @@ impl QueueStorage { serializable.into(), ) .await - .map_err(RoomSendQueueStorageError::StorageError)?; + .map_err(RoomSendQueueStorageError::StateStoreError)?; } else { // Not applied yet, we should retry later => false. return Ok(false); } } + + DependentQueuedRequestKind::UploadFileWithThumbnail { + content_type, + cache_key, + related_to, + } => { + let Some(parent_key) = parent_key else { + // Not finished yet. + return Ok(false); + }; + + // Both uploads are ready: enqueue sending the file's media now. + let (file, thumbnail) = match parent_key { + SentRequestKey::Media { file, thumbnail } => (file, thumbnail), + _ => { + return Err(RoomSendQueueError::StorageError( + RoomSendQueueStorageError::InvalidParentKey, + )); + } + }; + + // The media we just uploaded was a thumbnail, so the thumbnail shouldn't have + // a thumbnail itself. + debug_assert!(thumbnail.is_none()); + if thumbnail.is_some() { + warn!("unexpected thumbnail for a thumbnail!"); + } + + trace!( + %related_to, + "done uploading thumbnail, now queuing a request to send the media file itself" + ); + + let request = QueuedRequestKind::Upload { + content_type, + cache_key, + thumbnail_source: Some(file), + related_to, + }; + + store + .save_send_queue_request( + &self.room_id, + dependent_request.own_transaction_id.into(), + request, + ) + .await + .map_err(RoomSendQueueStorageError::StateStoreError)?; + } + + DependentQueuedRequestKind::FinishUpload { + mut local_echo, + file_upload, + thumbnail_info, + } => { + let Some(parent_key) = parent_key else { + // Not finished yet. + return Ok(false); + }; + + // Both uploads are ready: enqueue the event with its final data. + let (file_source, thumbnail_source) = match parent_key { + SentRequestKey::Media { file, thumbnail } => (file, thumbnail), + _ => { + return Err(RoomSendQueueError::StorageError( + RoomSendQueueStorageError::InvalidParentKey, + )); + } + }; + + { + // Update cache keys in the media stores, from the local ones to the remote + // ones. + + // Rename the original file. + let original_request = + RoomSendQueue::make_local_file_media_request(&file_upload); + + trace!( + ?original_request.source, + ?file_source, + "renaming media file key in cache store" + ); + + client + .event_cache_store() + .replace_media_key( + &original_request, + &MediaRequest { + source: file_source.clone(), + format: MediaFormat::File, + }, + ) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + + // Rename the thumbnail too, if needs be. + if let Some((info, new_source)) = + thumbnail_info.as_ref().zip(thumbnail_source.clone()) + { + let original_thumbnail_request = + RoomSendQueue::make_local_thumbnail_media_request( + &info.txn, + info.height, + info.width, + ); + + trace!( + ?original_thumbnail_request.source, + ?new_source, + "renaming thumbnail file key in cache store" + ); + + // Reuse the same format for the cached thumbnail with the final MXC ID. + let new_format = original_thumbnail_request.format.clone(); + + client + .event_cache_store() + .replace_media_key( + &original_thumbnail_request, + &MediaRequest { source: new_source, format: new_format }, + ) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + } else { + trace!("media had no thumbnail"); + } + } + + // Replace the source by the final ones in all the medias handled by + // `Room::make_attachment_type()`. + // + // Some variants look eerily similar below, but the `event` and `info` are all + // different types… + + match &mut local_echo.msgtype { + MessageType::Audio(event) => { + event.source = file_source; + } + MessageType::File(event) => { + event.source = file_source; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = thumbnail_source; + } + } + MessageType::Image(event) => { + event.source = file_source; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = thumbnail_source; + } + } + MessageType::Video(event) => { + event.source = file_source; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = thumbnail_source; + } + } + + _ => { + // All `MessageType` created by `Room::make_attachment_type` should be + // handled here. The only way to end up here is that a message type has + // been tampered with in the database. + error!("Invalid message type in database: {}", local_echo.msgtype()); + // Only crash debug builds. + debug_assert!(false, "invalid message type in database"); + } + } + + let new_content = SerializableEventContent::new(&local_echo.into()) + .map_err(RoomSendQueueStorageError::JsonSerialization)?; + + // Indicates observers that the upload finished, by editing the local echo for + // the event into its final form before sending. + new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent { + transaction_id: dependent_request.own_transaction_id.clone().into(), + new_content: new_content.clone(), + }); + + trace!( + event_txn = %&*dependent_request.own_transaction_id, + "queueing media event after successfully uploading the media (and maybe a thumbnail)" + ); + + store + .save_send_queue_request( + &self.room_id, + dependent_request.own_transaction_id.into(), + new_content.into(), + ) + .await + .map_err(RoomSendQueueStorageError::StateStoreError)?; + } } Ok(true) } #[instrument(skip(self))] - async fn apply_dependent_requests(&self) -> Result<(), RoomSendQueueError> { + async fn apply_dependent_requests( + &self, + new_updates: &mut Vec, + ) -> Result<(), RoomSendQueueError> { // Keep the lock until we're done touching the storage. let _being_sent = self.being_sent.read().await; @@ -1138,7 +1707,7 @@ impl QueueStorage { let dependent_requests = store .load_dependent_queued_requests(&self.room_id) .await - .map_err(RoomSendQueueStorageError::StorageError)?; + .map_err(RoomSendQueueStorageError::StateStoreError)?; let num_initial_dependent_requests = dependent_requests.len(); if num_initial_dependent_requests == 0 { @@ -1157,7 +1726,7 @@ impl QueueStorage { store .remove_dependent_queued_request(&self.room_id, &original.own_transaction_id) .await - .map_err(RoomSendQueueStorageError::StorageError)?; + .map_err(RoomSendQueueStorageError::StateStoreError)?; } } @@ -1171,14 +1740,14 @@ impl QueueStorage { for dependent in canonicalized_dependent_requests { let dependent_id = dependent.own_transaction_id.clone(); - match self.try_apply_single_dependent_request(&client, dependent).await { + match self.try_apply_single_dependent_request(&client, dependent, new_updates).await { Ok(should_remove) => { if should_remove { // The dependent request has been successfully applied, forget about it. store .remove_dependent_queued_request(&self.room_id, &dependent_id) .await - .map_err(RoomSendQueueStorageError::StorageError)?; + .map_err(RoomSendQueueStorageError::StateStoreError)?; num_dependent_requests -= 1; } @@ -1307,6 +1876,15 @@ pub enum RoomSendQueueUpdate { /// Received event id from the send response. event_id: OwnedEventId, }, + + /// A media has been successfully uploaded. + UploadedMedia { + /// The media event this uploaded media relates to. + related_to: OwnedTransactionId, + + /// The final media source for the file that was just uploaded. + file: MediaSource, + }, } /// An error triggered by the send queue module. @@ -1332,7 +1910,11 @@ pub enum RoomSendQueueError { pub enum RoomSendQueueStorageError { /// Error caused by the state store. #[error(transparent)] - StorageError(#[from] StoreError), + StateStoreError(#[from] StoreError), + + /// Error caused by the event cache store. + #[error(transparent)] + EventCacheStoreError(#[from] EventCacheStoreError), /// Error caused when (de)serializing into/from json. #[error(transparent)] @@ -1346,6 +1928,11 @@ pub enum RoomSendQueueStorageError { /// The client is shutting down. #[error("The client is shutting down.")] ClientShuttingDown, + + /// An operation not implemented yet on a send handle. + // TODO: remove this + #[error("This operation is not implemented yet for media uploads")] + OperationNotImplementedYet, } /// A handle to manipulate an event that was scheduled to be sent to a room. @@ -1354,9 +1941,18 @@ pub enum RoomSendQueueStorageError { pub struct SendHandle { room: RoomSendQueue, transaction_id: OwnedTransactionId, + is_upload: bool, } impl SendHandle { + fn nyi_for_uploads(&self) -> Result<(), RoomSendQueueStorageError> { + if self.is_upload { + Err(RoomSendQueueStorageError::OperationNotImplementedYet) + } else { + Ok(()) + } + } + /// Aborts the sending of the event, if it wasn't sent yet. /// /// Returns true if the sending could be aborted, false if not (i.e. the @@ -1364,6 +1960,7 @@ impl SendHandle { #[instrument(skip(self), fields(room_id = %self.room.inner.room.room_id(), txn_id = %self.transaction_id))] pub async fn abort(&self) -> Result { trace!("received an abort request"); + self.nyi_for_uploads()?; if self.room.inner.queue.cancel_event(&self.transaction_id).await? { trace!("successful abort"); @@ -1391,6 +1988,7 @@ impl SendHandle { event_type: String, ) -> Result { trace!("received an edit request"); + self.nyi_for_uploads()?; let serializable = SerializableEventContent::from_raw(new_content, event_type); @@ -1503,6 +2101,7 @@ impl SendReactionHandle { let handle = SendHandle { room: self.room.clone(), transaction_id: self.transaction_id.clone().into(), + is_upload: false, }; handle.abort().await @@ -1514,16 +2113,34 @@ impl SendReactionHandle { } } +/// A handle to execute actions while sending an attachment. +/// +/// In the future, this may support cancellation, subscribing to progress, etc. +#[derive(Clone, Debug)] +pub struct SendAttachmentHandle { + /// Reference to the send queue for the room where this attachment was sent. + _room: RoomSendQueue, + + /// Transaction id for the sending of the event itself. + _transaction_id: OwnedTransactionId, + + /// Transaction id for the file upload. + _file_upload: OwnedTransactionId, + + /// Transaction id for the thumbnail upload. + _thumbnail_transaction_id: Option, +} + /// From a given source of [`DependentQueuedRequest`], return only the most /// meaningful, i.e. the ones that wouldn't be overridden after applying the /// others. fn canonicalize_dependent_requests( dependent: &[DependentQueuedRequest], ) -> Vec { - let mut by_event_id = HashMap::>::new(); + let mut by_txn = HashMap::>::new(); for d in dependent { - let prevs = by_event_id.entry(d.parent_transaction_id.clone()).or_default(); + let prevs = by_txn.entry(d.parent_transaction_id.clone()).or_default(); if prevs.iter().any(|prev| matches!(prev.kind, DependentQueuedRequestKind::RedactEvent)) { // The parent event has already been flagged for redaction, don't consider the @@ -1544,7 +2161,10 @@ fn canonicalize_dependent_requests( } } - DependentQueuedRequestKind::ReactEvent { .. } => { + DependentQueuedRequestKind::UploadFileWithThumbnail { .. } + | DependentQueuedRequestKind::FinishUpload { .. } + | DependentQueuedRequestKind::ReactEvent { .. } => { + // These requests can't be canonicalized, push them as is. prevs.push(d); } @@ -1556,10 +2176,7 @@ fn canonicalize_dependent_requests( } } - by_event_id - .into_iter() - .flat_map(|(_parent_txn_id, entries)| entries.into_iter().cloned()) - .collect() + by_txn.into_iter().flat_map(|(_parent_txn_id, entries)| entries.into_iter().cloned()).collect() } #[cfg(all(test, not(target_arch = "wasm32")))] From a8992f37d7dedcbad1844d9eaf6ced389af05c18 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 31 Oct 2024 17:25:59 +0100 Subject: [PATCH 450/979] test(send queue): add a smoke test for sending an attachment with the send queue --- .../tests/integration/send_queue.rs | 331 +++++++++++++++++- 1 file changed, 316 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 55455dd0ff9..13ee7bb9cf5 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -9,8 +9,13 @@ use std::{ use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ + attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, Thumbnail}, config::{RequestConfig, StoreConfig}, - send_queue::{LocalEcho, LocalEchoContent, RoomSendQueueError, RoomSendQueueUpdate}, + media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, + send_queue::{ + LocalEcho, LocalEchoContent, RoomSendQueueError, RoomSendQueueStorageError, + RoomSendQueueUpdate, + }, test_utils::{ events::EventFactory, logged_in_client, logged_in_client_with_server, set_client_session, }, @@ -29,12 +34,16 @@ use ruma::{ NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers, UnstablePollStartContentBlock, UnstablePollStartEventContent, }, - room::message::RoomMessageEventContent, - AnyMessageLikeEventContent, EventContent as _, + room::{ + message::{MessageType, RoomMessageEventContent}, + MediaSource, + }, + AnyMessageLikeEventContent, EventContent as _, Mentions, }, - room_id, + media::Method, + mxc_uri, owned_user_id, room_id, serde::Raw, - EventId, OwnedEventId, + uint, EventId, MxcUri, OwnedEventId, TransactionId, }; use serde_json::json; use tokio::{ @@ -42,12 +51,35 @@ use tokio::{ time::{sleep, timeout}, }; use wiremock::{ - matchers::{header, method, path_regex}, + matchers::{header, method, path, path_regex}, Mock, Request, ResponseTemplate, }; use crate::mock_sync_with_new_room; +// TODO put into the MatrixMockServer +fn mock_jpeg_upload(mxc: &MxcUri, lock: Arc>) -> Mock { + let mxc = mxc.to_owned(); + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(move |_req: &Request| { + // Wait for the signal from the main task that we can process this query. + let mock_lock = lock.clone(); + std::thread::spawn(move || { + tokio::runtime::Runtime::new().unwrap().block_on(async { + drop(mock_lock.lock().await); + }); + }) + .join() + .unwrap(); + ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": mxc + })) + }) +} + fn mock_send_event(returned_event_id: &EventId) -> Mock { Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) @@ -68,9 +100,9 @@ fn mock_send_transient_failure() -> Mock { // A macro to assert on a stream of `RoomSendQueueUpdate`s. macro_rules! assert_update { - // Check the next stream event is a local echo for a message with the content $body. - // Returns a tuple of (transaction_id, send_handle). - ($watch:ident => local echo { body = $body:expr }) => {{ + // Check the next stream event is a local echo for an uploaded media. + // Returns a tuple of (transaction_id, send_handle, room_message). + ($watch:ident => local echo event) => {{ assert_let!( Ok(Ok(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { content: LocalEchoContent::Event { @@ -84,12 +116,34 @@ macro_rules! assert_update { ); let content = serialized_event.deserialize().unwrap(); - assert_let!(AnyMessageLikeEventContent::RoomMessage(_msg) = content); - assert_eq!(_msg.body(), $body); + assert_let!(AnyMessageLikeEventContent::RoomMessage(room_message) = content); + (txn, send_handle, room_message) + }}; + + // Check the next stream event is a local echo for a message with the content $body. + // Returns a tuple of (transaction_id, send_handle). + ($watch:ident => local echo { body = $body:expr }) => {{ + let (txn, send_handle, room_message) = assert_update!($watch => local echo event); + assert_eq!(room_message.body(), $body); (txn, send_handle) }}; + // Check the next stream event is a notification about an uploaded media. + // Returns a tuple of (transaction_id, send_handle). + ($watch:ident => uploaded { related_to = $related_to:expr, mxc = $mxc:expr }) => {{ + assert_let!( + Ok(Ok(RoomSendQueueUpdate::UploadedMedia { + related_to, + file, + })) = timeout(Duration::from_secs(1), $watch.recv()).await + ); + + assert_eq!(related_to, $related_to); + assert_let!(MediaSource::Plain(mxc) = file); + assert_eq!(mxc, $mxc); + }}; + // Check the next stream event is a local echo for a reaction with the content $key which // applies to the local echo with transaction id $parent. ($watch:ident => local reaction { key = $key:expr, parent = $parent_txn_id:expr }) => {{ @@ -110,9 +164,9 @@ macro_rules! assert_update { txn }}; - // Check the next stream event is an edit for a local echo with the content $body, and that the + // Check the next stream event is an edit event, and that the // transaction id is the one we expect. - ($watch:ident => edit { body = $body:expr, txn = $transaction_id:expr }) => {{ + ($watch:ident => edit local echo { txn = $transaction_id:expr }) => {{ assert_let!( Ok(Ok(RoomSendQueueUpdate::ReplacedLocalEvent { transaction_id: txn, @@ -120,11 +174,19 @@ macro_rules! assert_update { })) = timeout(Duration::from_secs(1), $watch.recv()).await ); + assert_eq!(txn, $transaction_id); + let content = serialized_event.deserialize().unwrap(); assert_let!(AnyMessageLikeEventContent::RoomMessage(_msg) = content); - assert_eq!(_msg.body(), $body); - assert_eq!(txn, $transaction_id); + _msg + }}; + + // Check the next stream event is an edit for a local echo with the content $body, and that the + // transaction id is the one we expect. + ($watch:ident => edit { body = $body:expr, txn = $transaction_id:expr }) => {{ + let msg = assert_update!($watch => edit local echo { txn = $transaction_id }); + assert_eq!(msg.body(), $body); }}; // Check the next stream event is a retry event, with optional checks on txn=$txn @@ -1946,3 +2008,242 @@ async fn test_reactions() { assert!(watch.is_empty()); } + +#[async_test] +async fn test_media_uploads() { + let (client, server) = logged_in_client_with_server().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + + let room = mock_sync_with_new_room( + |builder| { + builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + }, + &client, + &server, + room_id, + ) + .await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // ---------------------- + // Create the media to send, with a thumbnail. + let filename = "surprise.jpeg.exe"; + let content_type = mime::IMAGE_JPEG; + let data = b"hello world".to_vec(); + + let thumbnail = Thumbnail { + data: b"thumbnail".to_vec(), + content_type: content_type.clone(), + info: Some(BaseThumbnailInfo { + height: Some(uint!(13)), + width: Some(uint!(37)), + size: Some(uint!(42)), + }), + }; + + let attachment_info = AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(14)), + width: Some(uint!(38)), + size: Some(uint!(43)), + blurhash: None, + }); + + let transaction_id = TransactionId::new(); + let mentions = Mentions::with_user_ids([owned_user_id!("@ivan:sdk.rs")]); + let config = AttachmentConfig::with_thumbnail(thumbnail) + .txn_id(&transaction_id) + .caption(Some("caption".to_owned())) + .mentions(Some(mentions.clone())) + .info(attachment_info); + + // ---------------------- + // Prepare endpoints. + mock_encryption_state(&server, false).await; + mock_send_event(event_id!("$1")).expect(1).mount(&server).await; + + let allow_upload_lock = Arc::new(Mutex::new(())); + let block_upload = allow_upload_lock.lock().await; + + mock_jpeg_upload(mxc_uri!("mxc://sdk.rs/thumbnail"), allow_upload_lock.clone()) + .up_to_n_times(1) + .expect(1) + .mount(&server) + .await; + mock_jpeg_upload(mxc_uri!("mxc://sdk.rs/media"), allow_upload_lock.clone()) + .up_to_n_times(1) + .expect(1) + .mount(&server) + .await; + + // ---------------------- + // Send the media. + assert!(watch.is_empty()); + q.send_attachment(filename, content_type, data, config) + .await + .expect("queuing the attachment works"); + + // ---------------------- + // Observe the local echo + let (txn, send_handle, content) = assert_update!(watch => local echo event); + assert_eq!(txn, transaction_id); + + // Check mentions. + let mentions = content.mentions.unwrap(); + assert!(!mentions.room); + assert_eq!( + mentions.user_ids.into_iter().collect::>(), + vec![owned_user_id!("@ivan:sdk.rs")] + ); + + // Check metadata. + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.body, "caption"); + assert!(img_content.formatted_caption().is_none()); + assert_eq!(img_content.filename.as_deref().unwrap(), filename); + + let info = img_content.info.unwrap(); + assert_eq!(info.height, Some(uint!(14))); + assert_eq!(info.width, Some(uint!(38))); + assert_eq!(info.size, Some(uint!(43))); + assert_eq!(info.mimetype.as_deref(), Some("image/jpeg")); + assert!(info.blurhash.is_none()); + + // Check the data source: it should reference the send queue local storage. + let local_source = img_content.source; + assert_let!(MediaSource::Plain(mxc) = &local_source); + assert!(mxc.to_string().starts_with("mxc://send-queue.local/"), "{mxc}"); + + // The media is immediately available from the cache. + let file_media = client + .media() + .get_media_content(&MediaRequest { source: local_source, format: MediaFormat::File }, true) + .await + .expect("media should be found"); + assert_eq!(file_media, b"hello world"); + + // ---------------------- + // Thumbnail. + + // Check metadata. + let tinfo = info.thumbnail_info.unwrap(); + assert_eq!(tinfo.height, Some(uint!(13))); + assert_eq!(tinfo.width, Some(uint!(37))); + assert_eq!(tinfo.size, Some(uint!(42))); + assert_eq!(tinfo.mimetype.as_deref(), Some("image/jpeg")); + + // Check the thumbnail source: it should reference the send queue local storage. + let local_thumbnail_source = info.thumbnail_source.unwrap(); + assert_let!(MediaSource::Plain(mxc) = &local_thumbnail_source); + assert!(mxc.to_string().starts_with("mxc://send-queue.local/"), "{mxc}"); + + let thumbnail_media = client + .media() + .get_media_content( + &MediaRequest { + source: local_thumbnail_source, + // TODO: extract this reasonable query into a helper function shared across the + // codebase + format: MediaFormat::Thumbnail(MediaThumbnailSettings { + size: MediaThumbnailSize { + height: tinfo.height.unwrap(), + width: tinfo.width.unwrap(), + method: Method::Scale, + }, + animated: false, + }), + }, + true, + ) + .await + .expect("media should be found"); + assert_eq!(thumbnail_media, b"thumbnail"); + + // ---------------------- + // Send handle operations. + + // Operations on the send handle haven't been implemented yet. + assert_matches!( + send_handle.abort().await, + Err(RoomSendQueueStorageError::OperationNotImplementedYet) + ); + // (and this operation would be invalid, we shouldn't turn a media into a + // message). + assert_matches!( + send_handle.edit(RoomMessageEventContent::text_plain("hi").into()).await, + Err(RoomSendQueueStorageError::OperationNotImplementedYet) + ); + + // ---------------------- + // Let the upload progress. + assert!(watch.is_empty()); + drop(block_upload); + + assert_update!(watch => uploaded { + related_to = transaction_id, + mxc = mxc_uri!("mxc://sdk.rs/thumbnail") + }); + + assert_update!(watch => uploaded { + related_to = transaction_id, + mxc = mxc_uri!("mxc://sdk.rs/media") + }); + + let edit_msg = assert_update!(watch => edit local echo { + txn = transaction_id + }); + assert_let!(MessageType::Image(new_content) = edit_msg.msgtype); + + assert_let!(MediaSource::Plain(new_uri) = &new_content.source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/media")); + + let file_media = client + .media() + .get_media_content( + &MediaRequest { source: new_content.source, format: MediaFormat::File }, + true, + ) + .await + .expect("media should be found with its final MXC uri in the cache"); + assert_eq!(file_media, b"hello world"); + + let new_thumbnail_source = new_content.info.unwrap().thumbnail_source.unwrap(); + assert_let!(MediaSource::Plain(new_uri) = &new_thumbnail_source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/thumbnail")); + + let thumbnail_media = client + .media() + .get_media_content( + &MediaRequest { + source: new_thumbnail_source, + // TODO: extract this reasonable query into a helper function shared across the + // codebase + format: MediaFormat::Thumbnail(MediaThumbnailSettings { + size: MediaThumbnailSize { + height: tinfo.height.unwrap(), + width: tinfo.width.unwrap(), + method: Method::Scale, + }, + animated: false, + }), + }, + true, + ) + .await + .expect("media should be found"); + assert_eq!(thumbnail_media, b"thumbnail"); + + // The event is sent, at some point. + assert_update!(watch => sent { + txn = transaction_id, + event_id = event_id!("$1") + }); + + // That's all, folks! + assert!(watch.is_empty()); +} From c196a9754b211beccb7d50a6e5c6cceb9b2a73dc Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 31 Oct 2024 17:31:02 +0100 Subject: [PATCH 451/979] feat(timeline): send medias via the send queue --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 63 ++++---------------- crates/matrix-sdk-ui/src/timeline/futures.rs | 24 +------- 2 files changed, 13 insertions(+), 74 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index cbdd8ab21c9..002c33e1791 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -105,18 +105,13 @@ impl Timeline { filename: String, mime_type: Option, attachment_config: AttachmentConfig, - store_in_cache: bool, progress_watcher: Option>, ) -> Result<(), RoomError> { let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; let mime_type = mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let mut request = self.inner.send_attachment(filename, mime_type, attachment_config); - - if store_in_cache { - request.store_in_cache(); - } + let request = self.inner.send_attachment(filename, mime_type, attachment_config); if let Some(progress_watcher) = progress_watcher { let mut subscriber = request.subscribe_to_send_progress(); @@ -278,7 +273,6 @@ impl Timeline { } } - #[allow(clippy::too_many_arguments)] pub fn send_image( self: Arc, url: String, @@ -286,7 +280,6 @@ impl Timeline { image_info: ImageInfo, caption: Option, formatted_caption: Option, - store_in_cache: bool, progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { @@ -299,18 +292,11 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - image_info.mimetype, - attachment_config, - store_in_cache, - progress_watcher, - ) - .await + self.send_attachment(url, image_info.mimetype, attachment_config, progress_watcher) + .await })) } - #[allow(clippy::too_many_arguments)] pub fn send_video( self: Arc, url: String, @@ -318,7 +304,6 @@ impl Timeline { video_info: VideoInfo, caption: Option, formatted_caption: Option, - store_in_cache: bool, progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { @@ -331,14 +316,8 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - video_info.mimetype, - attachment_config, - store_in_cache, - progress_watcher, - ) - .await + self.send_attachment(url, video_info.mimetype, attachment_config, progress_watcher) + .await })) } @@ -348,7 +327,6 @@ impl Timeline { audio_info: AudioInfo, caption: Option, formatted_caption: Option, - store_in_cache: bool, progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { @@ -361,14 +339,8 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - audio_info.mimetype, - attachment_config, - store_in_cache, - progress_watcher, - ) - .await + self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) + .await })) } @@ -380,7 +352,6 @@ impl Timeline { waveform: Vec, caption: Option, formatted_caption: Option, - store_in_cache: bool, progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { @@ -394,14 +365,8 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment( - url, - audio_info.mimetype, - attachment_config, - store_in_cache, - progress_watcher, - ) - .await + self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) + .await })) } @@ -409,7 +374,6 @@ impl Timeline { self: Arc, url: String, file_info: FileInfo, - store_in_cache: bool, progress_watcher: Option>, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { @@ -419,14 +383,7 @@ impl Timeline { let attachment_config = AttachmentConfig::new().info(attachment_info); - self.send_attachment( - url, - file_info.mimetype, - attachment_config, - store_in_cache, - progress_watcher, - ) - .await + self.send_attachment(url, file_info.mimetype, attachment_config, progress_watcher).await })) } diff --git a/crates/matrix-sdk-ui/src/timeline/futures.rs b/crates/matrix-sdk-ui/src/timeline/futures.rs index 91d20e708d9..45cebb4668b 100644 --- a/crates/matrix-sdk-ui/src/timeline/futures.rs +++ b/crates/matrix-sdk-ui/src/timeline/futures.rs @@ -15,7 +15,6 @@ pub struct SendAttachment<'a> { config: AttachmentConfig, tracing_span: Span, pub(crate) send_progress: SharedObservable, - store_in_cache: bool, } impl<'a> SendAttachment<'a> { @@ -32,7 +31,6 @@ impl<'a> SendAttachment<'a> { config, tracing_span: Span::current(), send_progress: Default::default(), - store_in_cache: false, } } @@ -42,14 +40,6 @@ impl<'a> SendAttachment<'a> { pub fn subscribe_to_send_progress(&self) -> Subscriber { self.send_progress.subscribe() } - - /// Whether the sent attachment should be stored in the cache or not. - /// - /// If set to true, then retrieving the data for the attachment will result - /// in a cache hit immediately after upload. - pub fn store_in_cache(&mut self) { - self.store_in_cache = true; - } } impl<'a> IntoFuture for SendAttachment<'a> { @@ -57,8 +47,7 @@ impl<'a> IntoFuture for SendAttachment<'a> { boxed_into_future!(extra_bounds: 'a); fn into_future(self) -> Self::IntoFuture { - let Self { timeline, path, mime_type, config, tracing_span, send_progress, store_in_cache } = - self; + let Self { timeline, path, mime_type, config, tracing_span, send_progress: _ } = self; let fut = async move { let filename = path @@ -68,15 +57,8 @@ impl<'a> IntoFuture for SendAttachment<'a> { .ok_or(Error::InvalidAttachmentFileName)?; let data = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?; - let mut fut = timeline - .room() - .send_attachment(filename, &mime_type, data, config) - .with_send_progress_observable(send_progress); - - if store_in_cache { - fut = fut.store_in_cache(); - } - + let send_queue = timeline.room().send_queue(); + let fut = send_queue.send_attachment(filename, mime_type, data, config); fut.await.map_err(|_| Error::FailedSendingAttachment)?; Ok(()) From 57ad256fe1061f609d1915544d5c09c7b913392d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 5 Nov 2024 15:17:14 +0100 Subject: [PATCH 452/979] doc(send queue): beef up the send queue module comment and describe uploads --- crates/matrix-sdk/src/send_queue.rs | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index b23c4cb2aac..d838ab5ea33 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -42,6 +42,90 @@ //! recommended to call this method during initialization of a client, //! otherwise persisted unsent events will only be re-sent after the send //! queue for the given room has been reopened for the first time. +//! +//! # Send handle +//! +//! Just after queuing a request to send something, a [`SendHandle`] is +//! returned, allowing manipulating the inflight request. +//! +//! For a send handle for an event, it's possible to edit the event / abort +//! sending it. If it was still in the queue (i.e. not sent yet, or not being +//! sent), then such an action would happen locally (i.e. in the database). +//! Otherwise, it is "too late": the background task may be sending +//! the event already, or has sent it; in that case, the edit/aborting must +//! happen as an actual event materializing this, on the server. To accomplish +//! this, the send queue may send such an event, using the dependency system +//! described below. +//! +//! # Dependency system +//! +//! The send queue includes a simple dependency system, where a +//! [`QueuedRequest`] can have zero or more dependents in the form of +//! [`DependentQueuedRequest`]. A dependent queued request can have at most one +//! depended-upon (parent) queued request. +//! +//! This allows implementing deferred edits/redacts, as hinted to in the +//! previous section. +//! +//! ## Media upload +//! +//! This dependency system also allows uploading medias, since the media's +//! *content* must be uploaded before we send the media *event* that describes +//! it. +//! +//! In the simplest case, that is, a media file and its event must be sent (i.e. +//! no thumbnails): +//! +//! - The file's content is immediately cached in the +//! [`matrix_sdk_base::event_cache_store::EventCacheStore`], using an MXC ID +//! that is temporary and designates a local URI without any possible doubt. +//! - An initial media event is created and uses this temporary MXC ID, and +//! propagated as a local echo for an event. +//! - A [`QueuedRequest`] is pushed to upload the file's media +//! ([`QueuedRequestKind::Upload`]). +//! - A [`DependentQueuedRequest`] is pushed to finish the upload +//! ([`DependentQueuedRequestKind::FinishUpload`]). +//! +//! What is expected to happen, if all goes well, is the following: +//! +//! - the media is uploaded to the media homeserver, which returns the final MXC +//! ID. +//! - when marking the upload request as sent, the MXC ID is injected (as a +//! [`matrix_sdk_base::store::SentRequestKey`]) into the dependent request +//! [`DependentQueuedRequestKind::FinishUpload`] created in the last step +//! above. +//! - next time the send queue handles dependent queries, it'll see this one is +//! ready to be sent, and it will transform it into an event queued request +//! ([`QueuedRequestKind::Event`]), with the event created in the local echo +//! before, updated with the MXC ID returned from the server. +//! - this updated local echo is also propagated as an edit of the local echo to +//! observers, who get the final version with the final MXC IDs at this point +//! too. +//! - then the event is sent normally, as any event sent with the send queue. +//! +//! When there is a thumbnail, things behave similarly, with some tweaks: +//! +//! - the thumbnail's content is also stored into the cache store immediately, +//! - the thumbnail is sent first as an [`QueuedRequestKind::Upload`] request, +//! - the file upload is pushed as a dependent request of kind +//! [`DependentQueuedRequestKind::UploadFileWithThumbnail`] (this variant +//! keeps the file's key used to look it up in the cache store). +//! - the media event is then sent as a dependent request as described in the +//! previous section. +//! +//! What's expected to happen is thus the following: +//! +//! - After the thumbnail has been uploaded, the dependent query will retrieve +//! the final MXC ID returned by the homeserver for the thumbnail, and store +//! it into the [`QueuedRequestKind::Upload`]'s `thumbnail_source` field, +//! allowing to remember the thumbnail MXC ID when it's time to finish the +//! upload later. +//! - The dependent request is morphed into another +//! [`QueuedRequestKind::Upload`], for the file itself. +//! +//! The rest of the process is then similar to that of uploading a file without +//! a thumbnail. The only difference is that there's a thumbnail source (MXC ID) +//! remembered and fixed up into the media event, just before sending it. use std::{ collections::{BTreeMap, BTreeSet, HashMap}, From e9d5aa1221cc7e810471a1c07a029a2ac1f87809 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 5 Nov 2024 16:22:44 +0100 Subject: [PATCH 453/979] chore(send queue): move code around to avoid an enormous `send_attachment` method --- crates/matrix-sdk/src/send_queue.rs | 291 +++++++++++++++------------- 1 file changed, 151 insertions(+), 140 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index d838ab5ea33..fe0038b1151 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -154,7 +154,10 @@ use ruma::{ events::{ reaction::ReactionEventContent, relation::Annotation, - room::{message::MessageType, MediaSource, ThumbnailInfo}, + room::{ + message::{MessageType, RoomMessageEventContent}, + MediaSource, ThumbnailInfo, + }, AnyMessageLikeEventContent, EventContent as _, }, media::Method, @@ -517,31 +520,27 @@ impl RoomSendQueue { return Err(RoomSendQueueError::RoomNotJoined); } - let client = room.client(); - let store = client.store(); - - debug!(filename, %content_type, "sending an attachment"); - - // Push the dependent requests first, to make sure we're not sending the parent - // (depended upon) while dependencies aren't known yet. - let upload_file_txn = TransactionId::new(); let send_event_txn = config.txn_id.map_or_else(ChildTransactionId::new, Into::into); Span::current().record("event_txn", tracing::field::display(&*send_event_txn)); + debug!(filename, %content_type, %upload_file_txn, "sending an attachment"); - // Cache medias. - - // Prepare and cache the file. + // Cache the file itself in the cache store. let file_media_request = Self::make_local_file_media_request(&upload_file_txn); - room.client() .event_cache_store() .add_media_content(&file_media_request, data.clone()) .await .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - let (event_content, thumbnail_txn) = if let Some(thumbnail) = config.thumbnail.take() { + // Process the thumbnail, if it's been provided. + let (upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info) = if let Some( + thumbnail, + ) = + config.thumbnail.take() + { + // Normalize information to retrieve the thumbnail in the cache store. let info = thumbnail.info.as_ref(); let height = info.and_then(|info| info.height).unwrap_or_else(|| { trace!("thumbnail height is unknown, using 0 for the cache entry"); @@ -552,156 +551,78 @@ impl RoomSendQueue { uint!(0) }); - let thumbnail_upload_txn = TransactionId::new(); - trace!( - %upload_file_txn, - %thumbnail_upload_txn, - thumbnail_size = ?(height, width), - "attachment has a thumbnail" - ); - - let media_request = - Self::make_local_thumbnail_media_request(&thumbnail_upload_txn, height, width); + let txn = TransactionId::new(); + trace!(upload_thumbnail_txn = %txn, thumbnail_size = ?(height, width), "attachment has a thumbnail"); + // Cache thumbnail in the cache store. + let thumbnail_media_request = + Self::make_local_thumbnail_media_request(&txn, height, width); room.client() .event_cache_store() - .add_media_content(&media_request, thumbnail.data.clone()) + .add_media_content(&thumbnail_media_request, thumbnail.data.clone()) .await .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + // Create the information required for filling the thumbnail section of the + // media event. let thumbnail_info = Box::new(assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) })); - // Save the event sending request as a dependent request on the file upload. - let content = Room::make_attachment_event( - room.make_attachment_type( - &content_type, - filename, - file_media_request.source.clone(), - config.caption, - config.formatted_caption, - config.info, - Some((media_request.source.clone(), thumbnail_info)), - ), - config.mentions, - ); - - store - .save_dependent_queued_request( - room.room_id(), - &upload_file_txn, - send_event_txn.clone(), - DependentQueuedRequestKind::FinishUpload { - local_echo: content.clone(), - file_upload: upload_file_txn.clone(), - thumbnail_info: Some(FinishUploadThumbnailInfo { - txn: thumbnail_upload_txn.clone(), - height, - width, - }), - }, - ) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - - // Save the file upload request as a dependent request of the thumbnail upload. - store - .save_dependent_queued_request( - room.room_id(), - &thumbnail_upload_txn, - upload_file_txn.clone().into(), - DependentQueuedRequestKind::UploadFileWithThumbnail { - content_type: content_type.to_string(), - cache_key: file_media_request, - related_to: send_event_txn.clone().into(), - }, - ) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - - // Save the thumbnail upload request. - store - .save_send_queue_request( - room.room_id(), - thumbnail_upload_txn.clone(), - QueuedRequestKind::Upload { - content_type: thumbnail.content_type.to_string(), - cache_key: media_request, - thumbnail_source: None, - related_to: send_event_txn.clone().into(), - }, - ) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - - (content, Some(thumbnail_upload_txn)) + ( + Some(txn.clone()), + Some((thumbnail_media_request.source.clone(), thumbnail_info)), + Some(( + FinishUploadThumbnailInfo { txn, width, height }, + thumbnail_media_request, + thumbnail.content_type, + )), + ) } else { - // No thumbnail: only save the file upload request and send the event as a - // dependency. - - trace!(%upload_file_txn, "attachment has no thumbnails"); - - let content = Room::make_attachment_event( - room.make_attachment_type( - &content_type, - filename, - file_media_request.source.clone(), - config.caption, - config.formatted_caption, - config.info, - None, - ), - config.mentions, - ); - - store - .save_dependent_queued_request( - room.room_id(), - &upload_file_txn, - send_event_txn.clone(), - DependentQueuedRequestKind::FinishUpload { - local_echo: content.clone(), - file_upload: upload_file_txn.clone(), - thumbnail_info: None, - }, - ) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + Default::default() + }; - store - .save_send_queue_request( - room.room_id(), - upload_file_txn.clone(), - QueuedRequestKind::Upload { - content_type: content_type.to_string(), - cache_key: file_media_request, - thumbnail_source: None, - related_to: send_event_txn.clone().into(), - }, - ) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + // Create the content for the media event. + let event_content = Room::make_attachment_event( + room.make_attachment_type( + &content_type, + filename, + file_media_request.source.clone(), + config.caption, + config.formatted_caption, + config.info, + event_thumbnail_info, + ), + config.mentions, + ); - // No thumbnail attachment. - (content, None) - }; + // Save requests in the queue storage. + self.inner + .queue + .push_media( + event_content.clone(), + content_type, + send_event_txn.clone().into(), + upload_file_txn.clone(), + file_media_request, + queue_thumbnail_info, + ) + .await?; - let send_event_txn = OwnedTransactionId::from(send_event_txn); trace!("manager sends a media to the background task"); self.inner.notifier.notify_one(); let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { - transaction_id: send_event_txn.clone(), + transaction_id: send_event_txn.clone().into(), content: LocalEchoContent::Event { serialized_event: SerializableEventContent::new(&event_content.into()) .map_err(RoomSendQueueStorageError::JsonSerialization)?, // TODO: this should be a `SendAttachmentHandle`! send_handle: SendHandle { room: self.clone(), - transaction_id: send_event_txn.clone(), + transaction_id: send_event_txn.clone().into(), is_upload: true, }, send_error: None, @@ -710,9 +631,9 @@ impl RoomSendQueue { Ok(SendAttachmentHandle { _room: self.clone(), - _transaction_id: send_event_txn, + _transaction_id: send_event_txn.into(), _file_upload: upload_file_txn, - _thumbnail_transaction_id: thumbnail_txn, + _thumbnail_transaction_id: upload_thumbnail_txn, }) } @@ -1288,6 +1209,96 @@ impl QueueStorage { Ok(edited) } + /// Push requests (and dependents) to upload a media. + /// + /// See the module-level description for details of the whole processus. + async fn push_media( + &self, + event: RoomMessageEventContent, + content_type: Mime, + send_event_txn: OwnedTransactionId, + upload_file_txn: OwnedTransactionId, + file_media_request: MediaRequest, + thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequest, Mime)>, + ) -> Result<(), RoomSendQueueStorageError> { + // Keep the lock until we're done touching the storage. + // TODO refactor to make the relationship between being_sent and the store more + // obvious. + let _guard = self.being_sent.read().await; + + let client = self.client()?; + let store = client.store(); + + let thumbnail_info = + if let Some((thumbnail_info, thumbnail_media_request, thumbnail_content_type)) = + thumbnail + { + let upload_thumbnail_txn = thumbnail_info.txn.clone(); + + // Save the thumbnail upload request. + store + .save_send_queue_request( + &self.room_id, + upload_thumbnail_txn.clone(), + QueuedRequestKind::Upload { + content_type: thumbnail_content_type.to_string(), + cache_key: thumbnail_media_request, + thumbnail_source: None, + related_to: send_event_txn.clone(), + }, + ) + .await?; + + // Save the file upload request as a dependent request of the thumbnail upload. + store + .save_dependent_queued_request( + &self.room_id, + &upload_thumbnail_txn, + upload_file_txn.clone().into(), + DependentQueuedRequestKind::UploadFileWithThumbnail { + content_type: content_type.to_string(), + cache_key: file_media_request, + related_to: send_event_txn.clone(), + }, + ) + .await?; + + Some(thumbnail_info) + } else { + // Save the file upload as its own request, not a dependent one. + store + .save_send_queue_request( + &self.room_id, + upload_file_txn.clone(), + QueuedRequestKind::Upload { + content_type: content_type.to_string(), + cache_key: file_media_request, + thumbnail_source: None, + related_to: send_event_txn.clone(), + }, + ) + .await?; + + None + }; + + // Push the dependent request for the event itself. + store + .save_dependent_queued_request( + &self.room_id, + &upload_file_txn, + send_event_txn.into(), + DependentQueuedRequestKind::FinishUpload { + local_echo: event, + file_upload: upload_file_txn.clone(), + thumbnail_info, + }, + ) + .await?; + + Ok(()) + } + /// Reacts to the given local echo of an event. #[instrument(skip(self))] async fn react( From 13244d808bdb84384c32ce367bd51fcac40af475 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 5 Nov 2024 16:38:59 +0100 Subject: [PATCH 454/979] chore(send queue): move more code around to split work into smaller functions --- crates/matrix-sdk-base/src/store/mod.rs | 2 +- .../matrix-sdk-base/src/store/send_queue.rs | 35 ++-- crates/matrix-sdk/src/send_queue.rs | 171 ++++++++---------- 3 files changed, 100 insertions(+), 108 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index a40973870b3..1bc7ec70609 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -77,7 +77,7 @@ pub use self::{ send_queue::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, - SentRequestKey, SerializableEventContent, + SentMediaInfo, SentRequestKey, SerializableEventContent, }, traits::{ ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities, diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index c657881c930..fb832706931 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -292,6 +292,22 @@ impl From for ChildTransactionId { } } +/// Information about a media (and its thumbnail) that have been sent to an +/// homeserver. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SentMediaInfo { + /// File that was uploaded by this request. + /// + /// If the request related to a thumbnail upload, this contains the + /// thumbnail media source. + pub file: MediaSource, + + /// Optional thumbnail previously uploaded, when uploading a file. + /// + /// When uploading a thumbnail, this is set to `None`. + pub thumbnail: Option, +} + /// A unique key (identifier) indicating that a transaction has been /// successfully sent to the server. /// @@ -302,18 +318,7 @@ pub enum SentRequestKey { Event(OwnedEventId), /// The parent transaction returned an uploaded resource URL. - Media { - /// File that was uploaded by this request. - /// - /// If the request related to a thumbnail upload, this contains the - /// thumbnail media source. - file: MediaSource, - - /// Optional thumbnail previously uploaded, when uploading a file. - /// - /// When uploading a thumbnail, this is set to `None`. - thumbnail: Option, - }, + Media(SentMediaInfo), } impl SentRequestKey { @@ -321,6 +326,12 @@ impl SentRequestKey { pub fn into_event_id(self) -> Option { as_variant!(self, Self::Event) } + + /// Converts the current parent key into information about a sent media, if + /// possible. + pub fn into_media(self) -> Option { + as_variant!(self, Self::Media) + } } /// A request to be sent, depending on a [`QueuedRequest`] to be sent first. diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index fe0038b1151..1fe73fe174d 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -143,7 +143,7 @@ use matrix_sdk_base::{ store::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, - SentRequestKey, SerializableEventContent, + SentMediaInfo, SentRequestKey, SerializableEventContent, }, RoomState, StoreError, }; @@ -672,6 +672,45 @@ impl RoomSendQueue { MediaRequest { source, format } } + /// Replace the source by the final ones in all the media types handled by + /// [`Room::make_attachment_type()`]. + fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) { + // Some variants look eerily similar below, but the `event` and `info` are all + // different types… + match &mut echo.msgtype { + MessageType::Audio(event) => { + event.source = sent.file; + } + MessageType::File(event) => { + event.source = sent.file; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = sent.thumbnail; + } + } + MessageType::Image(event) => { + event.source = sent.file; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = sent.thumbnail; + } + } + MessageType::Video(event) => { + event.source = sent.file; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = sent.thumbnail; + } + } + + _ => { + // All `MessageType` created by `Room::make_attachment_type` should be + // handled here. The only way to end up here is that a message type has + // been tampered with in the database. + error!("Invalid message type in database: {}", echo.msgtype()); + // Only crash debug builds. + debug_assert!(false, "invalid message type in database"); + } + } + } + /// Returns the current local requests as well as a receiver to listen to /// the send queue updates, as defined in [`RoomSendQueueUpdate`]. pub async fn subscribe( @@ -764,10 +803,10 @@ impl RoomSendQueue { }); } - SentRequestKey::Media { file, .. } => { + SentRequestKey::Media(media_info) => { let _ = updates.send(RoomSendQueueUpdate::UploadedMedia { related_to: related_txn_id.as_ref().unwrap_or(&txn_id).clone(), - file, + file: media_info.file, }); } }, @@ -910,7 +949,10 @@ impl RoomSendQueue { }; trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded"); - Ok(SentRequestKey::Media { file: media_source, thumbnail: thumbnail_source }) + Ok(SentRequestKey::Media(SentMediaInfo { + file: media_source, + thumbnail: thumbnail_source, + })) } } } @@ -1598,24 +1640,20 @@ impl QueueStorage { related_to, } => { let Some(parent_key) = parent_key else { - // Not finished yet. + // Not finished yet, we should retry later => false. return Ok(false); }; - // Both uploads are ready: enqueue sending the file's media now. - let (file, thumbnail) = match parent_key { - SentRequestKey::Media { file, thumbnail } => (file, thumbnail), - _ => { - return Err(RoomSendQueueError::StorageError( - RoomSendQueueStorageError::InvalidParentKey, - )); - } - }; + // The thumbnail has been sent, now transform the dependent file upload request + // into a ready one. + let sent_media = parent_key.into_media().ok_or( + RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey), + )?; // The media we just uploaded was a thumbnail, so the thumbnail shouldn't have // a thumbnail itself. - debug_assert!(thumbnail.is_none()); - if thumbnail.is_some() { + debug_assert!(sent_media.thumbnail.is_none()); + if sent_media.thumbnail.is_some() { warn!("unexpected thumbnail for a thumbnail!"); } @@ -1627,7 +1665,8 @@ impl QueueStorage { let request = QueuedRequestKind::Upload { content_type, cache_key, - thumbnail_source: Some(file), + // The thumbnail for the next upload is the file we just uploaded here. + thumbnail_source: Some(sent_media.file), related_to, }; @@ -1647,40 +1686,26 @@ impl QueueStorage { thumbnail_info, } => { let Some(parent_key) = parent_key else { - // Not finished yet. + // Not finished yet, we should retry later => false. return Ok(false); }; // Both uploads are ready: enqueue the event with its final data. - let (file_source, thumbnail_source) = match parent_key { - SentRequestKey::Media { file, thumbnail } => (file, thumbnail), - _ => { - return Err(RoomSendQueueError::StorageError( - RoomSendQueueStorageError::InvalidParentKey, - )); - } - }; + let sent_media = parent_key.into_media().ok_or( + RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey), + )?; + // Update cache keys in the cache store. { - // Update cache keys in the media stores, from the local ones to the remote - // ones. - - // Rename the original file. - let original_request = - RoomSendQueue::make_local_file_media_request(&file_upload); - - trace!( - ?original_request.source, - ?file_source, - "renaming media file key in cache store" - ); - + // Do it for the file itself. + let from_req = RoomSendQueue::make_local_file_media_request(&file_upload); + trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store"); client .event_cache_store() .replace_media_key( - &original_request, + &from_req, &MediaRequest { - source: file_source.clone(), + source: sent_media.file.clone(), format: MediaFormat::File, }, ) @@ -1689,75 +1714,31 @@ impl QueueStorage { // Rename the thumbnail too, if needs be. if let Some((info, new_source)) = - thumbnail_info.as_ref().zip(thumbnail_source.clone()) + thumbnail_info.as_ref().zip(sent_media.thumbnail.clone()) { - let original_thumbnail_request = - RoomSendQueue::make_local_thumbnail_media_request( - &info.txn, - info.height, - info.width, - ); - - trace!( - ?original_thumbnail_request.source, - ?new_source, - "renaming thumbnail file key in cache store" + let from_req = RoomSendQueue::make_local_thumbnail_media_request( + &info.txn, + info.height, + info.width, ); + trace!( from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); + // Reuse the same format for the cached thumbnail with the final MXC ID. - let new_format = original_thumbnail_request.format.clone(); + let new_format = from_req.format.clone(); client .event_cache_store() .replace_media_key( - &original_thumbnail_request, + &from_req, &MediaRequest { source: new_source, format: new_format }, ) .await .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; - } else { - trace!("media had no thumbnail"); } } - // Replace the source by the final ones in all the medias handled by - // `Room::make_attachment_type()`. - // - // Some variants look eerily similar below, but the `event` and `info` are all - // different types… - - match &mut local_echo.msgtype { - MessageType::Audio(event) => { - event.source = file_source; - } - MessageType::File(event) => { - event.source = file_source; - if let Some(info) = event.info.as_mut() { - info.thumbnail_source = thumbnail_source; - } - } - MessageType::Image(event) => { - event.source = file_source; - if let Some(info) = event.info.as_mut() { - info.thumbnail_source = thumbnail_source; - } - } - MessageType::Video(event) => { - event.source = file_source; - if let Some(info) = event.info.as_mut() { - info.thumbnail_source = thumbnail_source; - } - } - - _ => { - // All `MessageType` created by `Room::make_attachment_type` should be - // handled here. The only way to end up here is that a message type has - // been tampered with in the database. - error!("Invalid message type in database: {}", local_echo.msgtype()); - // Only crash debug builds. - debug_assert!(false, "invalid message type in database"); - } - } + RoomSendQueue::update_media_event_after_upload(&mut local_echo, sent_media); let new_content = SerializableEventContent::new(&local_echo.into()) .map_err(RoomSendQueueStorageError::JsonSerialization)?; From c04a73c28dac02944fe9debbeeb5a46b200a4932 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 5 Nov 2024 17:01:54 +0100 Subject: [PATCH 455/979] chore(send queue): move code for media upload to its own file --- crates/matrix-sdk/src/send_queue.rs | 371 ++------------------ crates/matrix-sdk/src/send_queue/upload.rs | 386 +++++++++++++++++++++ 2 files changed, 413 insertions(+), 344 deletions(-) create mode 100644 crates/matrix-sdk/src/send_queue/upload.rs diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 1fe73fe174d..a4ae6891e4f 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -139,7 +139,7 @@ use std::{ use as_variant::as_variant; use matrix_sdk_base::{ event_cache_store::EventCacheStoreError, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, + media::MediaRequest, store::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, @@ -150,27 +150,21 @@ use matrix_sdk_base::{ use matrix_sdk_common::executor::{spawn, JoinHandle}; use mime::Mime; use ruma::{ - assign, events::{ reaction::ReactionEventContent, relation::Annotation, - room::{ - message::{MessageType, RoomMessageEventContent}, - MediaSource, ThumbnailInfo, - }, + room::{message::RoomMessageEventContent, MediaSource}, AnyMessageLikeEventContent, EventContent as _, }, - media::Method, serde::Raw, - uint, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, TransactionId, UInt, + OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, }; use tokio::sync::{broadcast, Notify, RwLock}; -use tracing::{debug, error, info, instrument, trace, warn, Span}; +use tracing::{debug, error, info, instrument, trace, warn}; #[cfg(feature = "e2e-encryption")] use crate::crypto::{OlmError, SessionRecipientCollectionError}; use crate::{ - attachment::AttachmentConfig, client::WeakClient, config::RequestConfig, error::RetryKind, @@ -178,6 +172,8 @@ use crate::{ Client, Room, }; +mod upload; + /// A client-wide send queue, for all the rooms known by a client. pub struct SendQueue { client: Client, @@ -491,226 +487,6 @@ impl RoomSendQueue { .await } - /// Queues an attachment to be sent to the room, using the send queue. - /// - /// This returns quickly (without sending or uploading anything), and will - /// push the event to be sent into a queue, handled in the background. - /// - /// Callers are expected to consume [`RoomSendQueueUpdate`] via calling - /// the [`Self::subscribe()`] method to get updates about the sending of - /// that event. - /// - /// By default, if sending failed on the first attempt, it will be retried a - /// few times. If sending failed after those retries, the entire - /// client's sending queue will be disabled, and it will need to be - /// manually re-enabled by the caller (e.g. after network is back, or when - /// something has been done about the faulty requests). - #[instrument(skip_all, fields(event_txn))] - pub async fn send_attachment( - &self, - filename: &str, - content_type: Mime, - data: Vec, - mut config: AttachmentConfig, - ) -> Result { - let Some(room) = self.inner.room.get() else { - return Err(RoomSendQueueError::RoomDisappeared); - }; - if room.state() != RoomState::Joined { - return Err(RoomSendQueueError::RoomNotJoined); - } - - let upload_file_txn = TransactionId::new(); - let send_event_txn = config.txn_id.map_or_else(ChildTransactionId::new, Into::into); - - Span::current().record("event_txn", tracing::field::display(&*send_event_txn)); - debug!(filename, %content_type, %upload_file_txn, "sending an attachment"); - - // Cache the file itself in the cache store. - let file_media_request = Self::make_local_file_media_request(&upload_file_txn); - room.client() - .event_cache_store() - .add_media_content(&file_media_request, data.clone()) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - - // Process the thumbnail, if it's been provided. - let (upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info) = if let Some( - thumbnail, - ) = - config.thumbnail.take() - { - // Normalize information to retrieve the thumbnail in the cache store. - let info = thumbnail.info.as_ref(); - let height = info.and_then(|info| info.height).unwrap_or_else(|| { - trace!("thumbnail height is unknown, using 0 for the cache entry"); - uint!(0) - }); - let width = info.and_then(|info| info.width).unwrap_or_else(|| { - trace!("thumbnail width is unknown, using 0 for the cache entry"); - uint!(0) - }); - - let txn = TransactionId::new(); - trace!(upload_thumbnail_txn = %txn, thumbnail_size = ?(height, width), "attachment has a thumbnail"); - - // Cache thumbnail in the cache store. - let thumbnail_media_request = - Self::make_local_thumbnail_media_request(&txn, height, width); - room.client() - .event_cache_store() - .add_media_content(&thumbnail_media_request, thumbnail.data.clone()) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - - // Create the information required for filling the thumbnail section of the - // media event. - let thumbnail_info = - Box::new(assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { - mimetype: Some(thumbnail.content_type.as_ref().to_owned()) - })); - - ( - Some(txn.clone()), - Some((thumbnail_media_request.source.clone(), thumbnail_info)), - Some(( - FinishUploadThumbnailInfo { txn, width, height }, - thumbnail_media_request, - thumbnail.content_type, - )), - ) - } else { - Default::default() - }; - - // Create the content for the media event. - let event_content = Room::make_attachment_event( - room.make_attachment_type( - &content_type, - filename, - file_media_request.source.clone(), - config.caption, - config.formatted_caption, - config.info, - event_thumbnail_info, - ), - config.mentions, - ); - - // Save requests in the queue storage. - self.inner - .queue - .push_media( - event_content.clone(), - content_type, - send_event_txn.clone().into(), - upload_file_txn.clone(), - file_media_request, - queue_thumbnail_info, - ) - .await?; - - trace!("manager sends a media to the background task"); - - self.inner.notifier.notify_one(); - - let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { - transaction_id: send_event_txn.clone().into(), - content: LocalEchoContent::Event { - serialized_event: SerializableEventContent::new(&event_content.into()) - .map_err(RoomSendQueueStorageError::JsonSerialization)?, - // TODO: this should be a `SendAttachmentHandle`! - send_handle: SendHandle { - room: self.clone(), - transaction_id: send_event_txn.clone().into(), - is_upload: true, - }, - send_error: None, - }, - })); - - Ok(SendAttachmentHandle { - _room: self.clone(), - _transaction_id: send_event_txn.into(), - _file_upload: upload_file_txn, - _thumbnail_transaction_id: upload_thumbnail_txn, - }) - } - - /// Create a [`MediaRequest`] for a file we want to store locally before - /// sending it. - /// - /// This uses a MXC ID that is only locally valid. - fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequest { - // .local is guaranteed to be on the local network. It would be a shame that - // `send-queue.local` resolves to an actual Synapse media server, we don't - // expect this to be likely though. - MediaRequest { - source: MediaSource::Plain(OwnedMxcUri::from(format!( - "mxc://send-queue.local/{txn_id}" - ))), - format: MediaFormat::File, - } - } - - /// Create a [`MediaRequest`] for a file we want to store locally before - /// sending it. - /// - /// This uses a MXC ID that is only locally valid. - fn make_local_thumbnail_media_request( - txn_id: &TransactionId, - height: UInt, - width: UInt, - ) -> MediaRequest { - // See comment in [`Self::make_local_file_media_request`]. - let source = - MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.local/{}", txn_id))); - let format = MediaFormat::Thumbnail(MediaThumbnailSettings { - size: MediaThumbnailSize { method: Method::Scale, width, height }, - animated: false, - }); - MediaRequest { source, format } - } - - /// Replace the source by the final ones in all the media types handled by - /// [`Room::make_attachment_type()`]. - fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) { - // Some variants look eerily similar below, but the `event` and `info` are all - // different types… - match &mut echo.msgtype { - MessageType::Audio(event) => { - event.source = sent.file; - } - MessageType::File(event) => { - event.source = sent.file; - if let Some(info) = event.info.as_mut() { - info.thumbnail_source = sent.thumbnail; - } - } - MessageType::Image(event) => { - event.source = sent.file; - if let Some(info) = event.info.as_mut() { - info.thumbnail_source = sent.thumbnail; - } - } - MessageType::Video(event) => { - event.source = sent.file; - if let Some(info) = event.info.as_mut() { - info.thumbnail_source = sent.thumbnail; - } - } - - _ => { - // All `MessageType` created by `Room::make_attachment_type` should be - // handled here. The only way to end up here is that a message type has - // been tampered with in the database. - error!("Invalid message type in database: {}", echo.msgtype()); - // Only crash debug builds. - debug_assert!(false, "invalid message type in database"); - } - } - } - /// Returns the current local requests as well as a receiver to listen to /// the send queue updates, as defined in [`RoomSendQueueUpdate`]. pub async fn subscribe( @@ -916,13 +692,10 @@ impl RoomSendQueue { }) })?; - let Some(data) = - room.client().event_cache_store().get_media_content(&cache_key).await? - else { - return Err(crate::Error::SendQueueWedgeError( - QueueWedgeError::MissingMediaContent, - )); - }; + let data = + room.client().event_cache_store().get_media_content(&cache_key).await?.ok_or( + crate::Error::SendQueueWedgeError(QueueWedgeError::MissingMediaContent), + )?; #[cfg(feature = "e2e-encryption")] let media_source = if room.is_encrypted().await? { @@ -1643,45 +1416,19 @@ impl QueueStorage { // Not finished yet, we should retry later => false. return Ok(false); }; - - // The thumbnail has been sent, now transform the dependent file upload request - // into a ready one. - let sent_media = parent_key.into_media().ok_or( - RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey), - )?; - - // The media we just uploaded was a thumbnail, so the thumbnail shouldn't have - // a thumbnail itself. - debug_assert!(sent_media.thumbnail.is_none()); - if sent_media.thumbnail.is_some() { - warn!("unexpected thumbnail for a thumbnail!"); - } - - trace!( - %related_to, - "done uploading thumbnail, now queuing a request to send the media file itself" - ); - - let request = QueuedRequestKind::Upload { + self.handle_dependent_file_upload_with_thumbnail( + client, + dependent_request.own_transaction_id.into(), + parent_key, content_type, cache_key, - // The thumbnail for the next upload is the file we just uploaded here. - thumbnail_source: Some(sent_media.file), related_to, - }; - - store - .save_send_queue_request( - &self.room_id, - dependent_request.own_transaction_id.into(), - request, - ) - .await - .map_err(RoomSendQueueStorageError::StateStoreError)?; + ) + .await?; } DependentQueuedRequestKind::FinishUpload { - mut local_echo, + local_echo, file_upload, thumbnail_info, } => { @@ -1689,80 +1436,16 @@ impl QueueStorage { // Not finished yet, we should retry later => false. return Ok(false); }; - - // Both uploads are ready: enqueue the event with its final data. - let sent_media = parent_key.into_media().ok_or( - RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey), - )?; - - // Update cache keys in the cache store. - { - // Do it for the file itself. - let from_req = RoomSendQueue::make_local_file_media_request(&file_upload); - trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store"); - client - .event_cache_store() - .replace_media_key( - &from_req, - &MediaRequest { - source: sent_media.file.clone(), - format: MediaFormat::File, - }, - ) - .await - .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; - - // Rename the thumbnail too, if needs be. - if let Some((info, new_source)) = - thumbnail_info.as_ref().zip(sent_media.thumbnail.clone()) - { - let from_req = RoomSendQueue::make_local_thumbnail_media_request( - &info.txn, - info.height, - info.width, - ); - - trace!( from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); - - // Reuse the same format for the cached thumbnail with the final MXC ID. - let new_format = from_req.format.clone(); - - client - .event_cache_store() - .replace_media_key( - &from_req, - &MediaRequest { source: new_source, format: new_format }, - ) - .await - .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; - } - } - - RoomSendQueue::update_media_event_after_upload(&mut local_echo, sent_media); - - let new_content = SerializableEventContent::new(&local_echo.into()) - .map_err(RoomSendQueueStorageError::JsonSerialization)?; - - // Indicates observers that the upload finished, by editing the local echo for - // the event into its final form before sending. - new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent { - transaction_id: dependent_request.own_transaction_id.clone().into(), - new_content: new_content.clone(), - }); - - trace!( - event_txn = %&*dependent_request.own_transaction_id, - "queueing media event after successfully uploading the media (and maybe a thumbnail)" - ); - - store - .save_send_queue_request( - &self.room_id, - dependent_request.own_transaction_id.into(), - new_content.into(), - ) - .await - .map_err(RoomSendQueueStorageError::StateStoreError)?; + self.handle_dependent_finish_upload( + client, + dependent_request.own_transaction_id.into(), + parent_key, + local_echo, + file_upload, + thumbnail_info, + new_updates, + ) + .await?; } } diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs new file mode 100644 index 00000000000..e31515c9fff --- /dev/null +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -0,0 +1,386 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Private implementations of the media upload mechanism. + +use matrix_sdk_base::{ + media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, + store::{ + ChildTransactionId, FinishUploadThumbnailInfo, QueuedRequestKind, SentMediaInfo, + SentRequestKey, SerializableEventContent, + }, + RoomState, +}; +use mime::Mime; +use ruma::{ + assign, + events::room::{ + message::{MessageType, RoomMessageEventContent}, + MediaSource, ThumbnailInfo, + }, + media::Method, + uint, OwnedMxcUri, OwnedTransactionId, TransactionId, UInt, +}; +use tracing::{debug, error, instrument, trace, warn, Span}; + +use super::{QueueStorage, RoomSendQueue, RoomSendQueueError, SendAttachmentHandle}; +use crate::{ + attachment::AttachmentConfig, + send_queue::{ + LocalEcho, LocalEchoContent, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle, + }, + Client, Room, +}; + +/// Create a [`MediaRequest`] for a file we want to store locally before +/// sending it. +/// +/// This uses a MXC ID that is only locally valid. +fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequest { + // .local is guaranteed to be on the local network. It would be a shame that + // `send-queue.local` resolves to an actual Synapse media server, we don't + // expect this to be likely though. + MediaRequest { + source: MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.local/{txn_id}"))), + format: MediaFormat::File, + } +} + +/// Create a [`MediaRequest`] for a file we want to store locally before +/// sending it. +/// +/// This uses a MXC ID that is only locally valid. +fn make_local_thumbnail_media_request( + txn_id: &TransactionId, + height: UInt, + width: UInt, +) -> MediaRequest { + // See comment in [`Self::make_local_file_media_request`]. + let source = + MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.local/{}", txn_id))); + let format = MediaFormat::Thumbnail(MediaThumbnailSettings { + size: MediaThumbnailSize { method: Method::Scale, width, height }, + animated: false, + }); + MediaRequest { source, format } +} + +/// Replace the source by the final ones in all the media types handled by +/// [`Room::make_attachment_type()`]. +fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) { + // Some variants look eerily similar below, but the `event` and `info` are all + // different types… + match &mut echo.msgtype { + MessageType::Audio(event) => { + event.source = sent.file; + } + MessageType::File(event) => { + event.source = sent.file; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = sent.thumbnail; + } + } + MessageType::Image(event) => { + event.source = sent.file; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = sent.thumbnail; + } + } + MessageType::Video(event) => { + event.source = sent.file; + if let Some(info) = event.info.as_mut() { + info.thumbnail_source = sent.thumbnail; + } + } + + _ => { + // All `MessageType` created by `Room::make_attachment_type` should be + // handled here. The only way to end up here is that a message type has + // been tampered with in the database. + error!("Invalid message type in database: {}", echo.msgtype()); + // Only crash debug builds. + debug_assert!(false, "invalid message type in database"); + } + } +} + +impl RoomSendQueue { + /// Queues an attachment to be sent to the room, using the send queue. + /// + /// This returns quickly (without sending or uploading anything), and will + /// push the event to be sent into a queue, handled in the background. + /// + /// Callers are expected to consume [`RoomSendQueueUpdate`] via calling + /// the [`Self::subscribe()`] method to get updates about the sending of + /// that event. + /// + /// By default, if sending failed on the first attempt, it will be retried a + /// few times. If sending failed after those retries, the entire + /// client's sending queue will be disabled, and it will need to be + /// manually re-enabled by the caller (e.g. after network is back, or when + /// something has been done about the faulty requests). + #[instrument(skip_all, fields(event_txn))] + pub async fn send_attachment( + &self, + filename: &str, + content_type: Mime, + data: Vec, + mut config: AttachmentConfig, + ) -> Result { + let Some(room) = self.inner.room.get() else { + return Err(RoomSendQueueError::RoomDisappeared); + }; + if room.state() != RoomState::Joined { + return Err(RoomSendQueueError::RoomNotJoined); + } + + let upload_file_txn = TransactionId::new(); + let send_event_txn = config.txn_id.map_or_else(ChildTransactionId::new, Into::into); + + Span::current().record("event_txn", tracing::field::display(&*send_event_txn)); + debug!(filename, %content_type, %upload_file_txn, "sending an attachment"); + + // Cache the file itself in the cache store. + let file_media_request = make_local_file_media_request(&upload_file_txn); + room.client() + .event_cache_store() + .add_media_content(&file_media_request, data.clone()) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + // Process the thumbnail, if it's been provided. + let (upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info) = if let Some( + thumbnail, + ) = + config.thumbnail.take() + { + // Normalize information to retrieve the thumbnail in the cache store. + let info = thumbnail.info.as_ref(); + let height = info.and_then(|info| info.height).unwrap_or_else(|| { + trace!("thumbnail height is unknown, using 0 for the cache entry"); + uint!(0) + }); + let width = info.and_then(|info| info.width).unwrap_or_else(|| { + trace!("thumbnail width is unknown, using 0 for the cache entry"); + uint!(0) + }); + + let txn = TransactionId::new(); + trace!(upload_thumbnail_txn = %txn, thumbnail_size = ?(height, width), "attachment has a thumbnail"); + + // Cache thumbnail in the cache store. + let thumbnail_media_request = make_local_thumbnail_media_request(&txn, height, width); + room.client() + .event_cache_store() + .add_media_content(&thumbnail_media_request, thumbnail.data.clone()) + .await + .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; + + // Create the information required for filling the thumbnail section of the + // media event. + let thumbnail_info = + Box::new(assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { + mimetype: Some(thumbnail.content_type.as_ref().to_owned()) + })); + + ( + Some(txn.clone()), + Some((thumbnail_media_request.source.clone(), thumbnail_info)), + Some(( + FinishUploadThumbnailInfo { txn, width, height }, + thumbnail_media_request, + thumbnail.content_type, + )), + ) + } else { + Default::default() + }; + + // Create the content for the media event. + let event_content = Room::make_attachment_event( + room.make_attachment_type( + &content_type, + filename, + file_media_request.source.clone(), + config.caption, + config.formatted_caption, + config.info, + event_thumbnail_info, + ), + config.mentions, + ); + + // Save requests in the queue storage. + self.inner + .queue + .push_media( + event_content.clone(), + content_type, + send_event_txn.clone().into(), + upload_file_txn.clone(), + file_media_request, + queue_thumbnail_info, + ) + .await?; + + trace!("manager sends a media to the background task"); + + self.inner.notifier.notify_one(); + + let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: send_event_txn.clone().into(), + content: LocalEchoContent::Event { + serialized_event: SerializableEventContent::new(&event_content.into()) + .map_err(RoomSendQueueStorageError::JsonSerialization)?, + // TODO: this should be a `SendAttachmentHandle`! + send_handle: SendHandle { + room: self.clone(), + transaction_id: send_event_txn.clone().into(), + is_upload: true, + }, + send_error: None, + }, + })); + + Ok(SendAttachmentHandle { + _room: self.clone(), + _transaction_id: send_event_txn.into(), + _file_upload: upload_file_txn, + _thumbnail_transaction_id: upload_thumbnail_txn, + }) + } +} + +impl QueueStorage { + /// Consumes a finished upload and queues sending of the final media event. + #[allow(clippy::too_many_arguments)] + pub(super) async fn handle_dependent_finish_upload( + &self, + client: &Client, + event_txn: OwnedTransactionId, + parent_key: SentRequestKey, + mut local_echo: RoomMessageEventContent, + file_upload_txn: OwnedTransactionId, + thumbnail_info: Option, + new_updates: &mut Vec, + ) -> Result<(), RoomSendQueueError> { + // Both uploads are ready: enqueue the event with its final data. + let sent_media = parent_key + .into_media() + .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?; + + // Update cache keys in the cache store. + { + // Do it for the file itself. + let from_req = make_local_file_media_request(&file_upload_txn); + + trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store"); + + client + .event_cache_store() + .replace_media_key( + &from_req, + &MediaRequest { source: sent_media.file.clone(), format: MediaFormat::File }, + ) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + + // Rename the thumbnail too, if needs be. + if let Some((info, new_source)) = + thumbnail_info.as_ref().zip(sent_media.thumbnail.clone()) + { + let from_req = + make_local_thumbnail_media_request(&info.txn, info.height, info.width); + + trace!( from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); + + // Reuse the same format for the cached thumbnail with the final MXC ID. + let new_format = from_req.format.clone(); + + client + .event_cache_store() + .replace_media_key( + &from_req, + &MediaRequest { source: new_source, format: new_format }, + ) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + } + } + + update_media_event_after_upload(&mut local_echo, sent_media); + + let new_content = SerializableEventContent::new(&local_echo.into()) + .map_err(RoomSendQueueStorageError::JsonSerialization)?; + + // Indicates observers that the upload finished, by editing the local echo for + // the event into its final form before sending. + new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent { + transaction_id: event_txn.clone(), + new_content: new_content.clone(), + }); + + trace!(%event_txn, "queueing media event after successfully uploading media(s)"); + + client + .store() + .save_send_queue_request(&self.room_id, event_txn, new_content.into()) + .await + .map_err(RoomSendQueueStorageError::StateStoreError)?; + + Ok(()) + } + + /// Consumes a finished upload of a thumbnail and queues the file upload. + pub(super) async fn handle_dependent_file_upload_with_thumbnail( + &self, + client: &Client, + next_upload_txn: OwnedTransactionId, + parent_key: SentRequestKey, + content_type: String, + cache_key: MediaRequest, + event_txn: OwnedTransactionId, + ) -> Result<(), RoomSendQueueError> { + // The thumbnail has been sent, now transform the dependent file upload request + // into a ready one. + let sent_media = parent_key + .into_media() + .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?; + + // The media we just uploaded was a thumbnail, so the thumbnail shouldn't have + // a thumbnail itself. + debug_assert!(sent_media.thumbnail.is_none()); + if sent_media.thumbnail.is_some() { + warn!("unexpected thumbnail for a thumbnail!"); + } + + trace!(related_to = %event_txn, "done uploading thumbnail, now queuing a request to send the media file itself"); + + let request = QueuedRequestKind::Upload { + content_type, + cache_key, + // The thumbnail for the next upload is the file we just uploaded here. + thumbnail_source: Some(sent_media.file), + related_to: event_txn, + }; + + client + .store() + .save_send_queue_request(&self.room_id, next_upload_txn, request) + .await + .map_err(RoomSendQueueStorageError::StateStoreError)?; + + Ok(()) + } +} From 9178e4ce333a779ac771bc60e73bb3561e2f7b86 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 10:54:18 +0100 Subject: [PATCH 456/979] chore(send queue): review --- .../src/event_cache_store/traits.rs | 3 +++ .../matrix-sdk-base/src/store/send_queue.rs | 2 +- crates/matrix-sdk/src/send_queue.rs | 21 +++++++++-------- crates/matrix-sdk/src/send_queue/upload.rs | 23 ++++++++++++------- .../tests/integration/send_queue.rs | 4 ++-- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache_store/traits.rs b/crates/matrix-sdk-base/src/event_cache_store/traits.rs index a288750f6b1..eaa1ada194c 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/traits.rs @@ -61,6 +61,9 @@ pub trait EventCacheStore: AsyncTraitDeps { /// keyed as a file before. The caller is responsible of ensuring that /// the replacement makes sense, according to their use case. /// + /// This should not raise an error when the `from` parameter points to an + /// unknown media, and it should silently continue in this case. + /// /// # Arguments /// /// * `from` - The previous `MediaRequest` of the file. diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index fb832706931..51bc1c7f1c6 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -86,7 +86,7 @@ pub enum QueuedRequestKind { /// /// The bytes must be stored in the media cache, and are identified by the /// cache key. - Upload { + MediaUpload { /// Content type of the media to be uploaded. /// /// Stored as a `String` because `Mime` which we'd really want to use diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index a4ae6891e4f..1ec961f1eaf 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -82,7 +82,7 @@ //! - An initial media event is created and uses this temporary MXC ID, and //! propagated as a local echo for an event. //! - A [`QueuedRequest`] is pushed to upload the file's media -//! ([`QueuedRequestKind::Upload`]). +//! ([`QueuedRequestKind::MediaUpload`]). //! - A [`DependentQueuedRequest`] is pushed to finish the upload //! ([`DependentQueuedRequestKind::FinishUpload`]). //! @@ -106,7 +106,8 @@ //! When there is a thumbnail, things behave similarly, with some tweaks: //! //! - the thumbnail's content is also stored into the cache store immediately, -//! - the thumbnail is sent first as an [`QueuedRequestKind::Upload`] request, +//! - the thumbnail is sent first as an [`QueuedRequestKind::MediaUpload`] +//! request, //! - the file upload is pushed as a dependent request of kind //! [`DependentQueuedRequestKind::UploadFileWithThumbnail`] (this variant //! keeps the file's key used to look it up in the cache store). @@ -117,11 +118,11 @@ //! //! - After the thumbnail has been uploaded, the dependent query will retrieve //! the final MXC ID returned by the homeserver for the thumbnail, and store -//! it into the [`QueuedRequestKind::Upload`]'s `thumbnail_source` field, +//! it into the [`QueuedRequestKind::MediaUpload`]'s `thumbnail_source` field, //! allowing to remember the thumbnail MXC ID when it's time to finish the //! upload later. //! - The dependent request is morphed into another -//! [`QueuedRequestKind::Upload`], for the file itself. +//! [`QueuedRequestKind::MediaUpload`], for the file itself. //! //! The rest of the process is then similar to that of uploading a file without //! a thumbnail. The only difference is that there's a thumbnail source (MXC ID) @@ -559,7 +560,7 @@ impl RoomSendQueue { let txn_id = queued_request.transaction_id.clone(); trace!(txn_id = %txn_id, "received a request to send!"); - let related_txn_id = as_variant!(&queued_request.kind, QueuedRequestKind::Upload { related_to, .. } => related_to.clone()); + let related_txn_id = as_variant!(&queued_request.kind, QueuedRequestKind::MediaUpload { related_to, .. } => related_to.clone()); let Some(room) = room.get() else { if is_dropping.load(Ordering::SeqCst) { @@ -678,7 +679,7 @@ impl RoomSendQueue { Ok(SentRequestKey::Event(res.event_id)) } - QueuedRequestKind::Upload { + QueuedRequestKind::MediaUpload { content_type, cache_key, thumbnail_source, @@ -1055,10 +1056,10 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, upload_thumbnail_txn.clone(), - QueuedRequestKind::Upload { + QueuedRequestKind::MediaUpload { content_type: thumbnail_content_type.to_string(), cache_key: thumbnail_media_request, - thumbnail_source: None, + thumbnail_source: None, // the thumbnail has no thumbnails :) related_to: send_event_txn.clone(), }, ) @@ -1085,7 +1086,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, upload_file_txn.clone(), - QueuedRequestKind::Upload { + QueuedRequestKind::MediaUpload { content_type: content_type.to_string(), cache_key: file_media_request, thumbnail_source: None, @@ -1169,7 +1170,7 @@ impl QueueStorage { send_error: queued.error, }, - QueuedRequestKind::Upload { .. } => { + QueuedRequestKind::MediaUpload { .. } => { // Don't return uploaded medias as their own things; the accompanying // event represented as a dependent request should be sufficient. return None; diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index e31515c9fff..2bd6a8c4780 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -48,11 +48,17 @@ use crate::{ /// /// This uses a MXC ID that is only locally valid. fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequest { - // .local is guaranteed to be on the local network. It would be a shame that - // `send-queue.local` resolves to an actual Synapse media server, we don't - // expect this to be likely though. + // This mustn't represent a potentially valid media server, otherwise it'd be + // possible for an attacker to return malicious content under some + // preconditions (e.g. the cache store has been cleared before the upload + // took place). To mitigate against this, we use the .localhost TLD, + // which is guaranteed to be on the local machine. As a result, the only attack + // possible would be coming from the user themselves, which we consider a + // non-threat. MediaRequest { - source: MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.local/{txn_id}"))), + source: MediaSource::Plain(OwnedMxcUri::from(format!( + "mxc://send-queue.localhost/{txn_id}" + ))), format: MediaFormat::File, } } @@ -66,9 +72,9 @@ fn make_local_thumbnail_media_request( height: UInt, width: UInt, ) -> MediaRequest { - // See comment in [`Self::make_local_file_media_request`]. + // See comment in [`make_local_file_media_request`]. let source = - MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.local/{}", txn_id))); + MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.localhost/{}", txn_id))); let format = MediaFormat::Thumbnail(MediaThumbnailSettings { size: MediaThumbnailSize { method: Method::Scale, width, height }, animated: false, @@ -79,7 +85,7 @@ fn make_local_thumbnail_media_request( /// Replace the source by the final ones in all the media types handled by /// [`Room::make_attachment_type()`]. fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) { - // Some variants look eerily similar below, but the `event` and `info` are all + // Some variants look really similar below, but the `event` and `info` are all // different types… match &mut echo.msgtype { MessageType::Audio(event) => { @@ -141,6 +147,7 @@ impl RoomSendQueue { let Some(room) = self.inner.room.get() else { return Err(RoomSendQueueError::RoomDisappeared); }; + if room.state() != RoomState::Joined { return Err(RoomSendQueueError::RoomNotJoined); } @@ -367,7 +374,7 @@ impl QueueStorage { trace!(related_to = %event_txn, "done uploading thumbnail, now queuing a request to send the media file itself"); - let request = QueuedRequestKind::Upload { + let request = QueuedRequestKind::MediaUpload { content_type, cache_key, // The thumbnail for the next upload is the file we just uploaded here. diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 13ee7bb9cf5..cf6ec31b014 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -2117,7 +2117,7 @@ async fn test_media_uploads() { // Check the data source: it should reference the send queue local storage. let local_source = img_content.source; assert_let!(MediaSource::Plain(mxc) = &local_source); - assert!(mxc.to_string().starts_with("mxc://send-queue.local/"), "{mxc}"); + assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); // The media is immediately available from the cache. let file_media = client @@ -2140,7 +2140,7 @@ async fn test_media_uploads() { // Check the thumbnail source: it should reference the send queue local storage. let local_thumbnail_source = info.thumbnail_source.unwrap(); assert_let!(MediaSource::Plain(mxc) = &local_thumbnail_source); - assert!(mxc.to_string().starts_with("mxc://send-queue.local/"), "{mxc}"); + assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); let thumbnail_media = client .media() From 4bbe620d0fc18012272fb9015d63be7805afce23 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 14:23:27 +0100 Subject: [PATCH 457/979] feat(timeline): use the send queue for media uploads behind a feature toggle --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 63 ++++++++++++++++---- crates/matrix-sdk-ui/src/timeline/futures.rs | 32 ++++++++-- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 002c33e1791..d82f719ed7d 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -106,12 +106,17 @@ impl Timeline { mime_type: Option, attachment_config: AttachmentConfig, progress_watcher: Option>, + use_send_queue: bool, ) -> Result<(), RoomError> { let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; let mime_type = mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let request = self.inner.send_attachment(filename, mime_type, attachment_config); + let mut request = self.inner.send_attachment(filename, mime_type, attachment_config); + + if use_send_queue { + request = request.use_send_queue(); + } if let Some(progress_watcher) = progress_watcher { let mut subscriber = request.subscribe_to_send_progress(); @@ -273,6 +278,7 @@ impl Timeline { } } + #[allow(clippy::too_many_arguments)] pub fn send_image( self: Arc, url: String, @@ -281,6 +287,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + use_send_queue: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_image_info = BaseImageInfo::try_from(&image_info) @@ -292,11 +299,18 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, image_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + image_info.mimetype, + attachment_config, + progress_watcher, + use_send_queue, + ) + .await })) } + #[allow(clippy::too_many_arguments)] pub fn send_video( self: Arc, url: String, @@ -305,6 +319,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + use_send_queue: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) @@ -316,8 +331,14 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, video_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + video_info.mimetype, + attachment_config, + progress_watcher, + use_send_queue, + ) + .await })) } @@ -328,6 +349,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + use_send_queue: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) @@ -339,8 +361,14 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + audio_info.mimetype, + attachment_config, + progress_watcher, + use_send_queue, + ) + .await })) } @@ -353,6 +381,7 @@ impl Timeline { caption: Option, formatted_caption: Option, progress_watcher: Option>, + use_send_queue: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) @@ -365,8 +394,14 @@ impl Timeline { .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); - self.send_attachment(url, audio_info.mimetype, attachment_config, progress_watcher) - .await + self.send_attachment( + url, + audio_info.mimetype, + attachment_config, + progress_watcher, + use_send_queue, + ) + .await })) } @@ -375,6 +410,7 @@ impl Timeline { url: String, file_info: FileInfo, progress_watcher: Option>, + use_send_queue: bool, ) -> Arc { SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_file_info: BaseFileInfo = @@ -383,7 +419,14 @@ impl Timeline { let attachment_config = AttachmentConfig::new().info(attachment_info); - self.send_attachment(url, file_info.mimetype, attachment_config, progress_watcher).await + self.send_attachment( + url, + file_info.mimetype, + attachment_config, + progress_watcher, + use_send_queue, + ) + .await })) } diff --git a/crates/matrix-sdk-ui/src/timeline/futures.rs b/crates/matrix-sdk-ui/src/timeline/futures.rs index 45cebb4668b..546b8ef5407 100644 --- a/crates/matrix-sdk-ui/src/timeline/futures.rs +++ b/crates/matrix-sdk-ui/src/timeline/futures.rs @@ -15,6 +15,7 @@ pub struct SendAttachment<'a> { config: AttachmentConfig, tracing_span: Span, pub(crate) send_progress: SharedObservable, + use_send_queue: bool, } impl<'a> SendAttachment<'a> { @@ -31,9 +32,22 @@ impl<'a> SendAttachment<'a> { config, tracing_span: Span::current(), send_progress: Default::default(), + use_send_queue: false, } } + /// (Experimental) Uses the send queue to upload this media. + /// + /// This uses the send queue to upload the medias, and as such it provides + /// local echoes for the uploaded media too, not blocking the sending + /// request. + /// + /// This will be the default in future versions, when the feature work will + /// be done there. + pub fn use_send_queue(self) -> Self { + Self { use_send_queue: true, ..self } + } + /// Get a subscriber to observe the progress of sending the request /// body. #[cfg(not(target_arch = "wasm32"))] @@ -47,7 +61,8 @@ impl<'a> IntoFuture for SendAttachment<'a> { boxed_into_future!(extra_bounds: 'a); fn into_future(self) -> Self::IntoFuture { - let Self { timeline, path, mime_type, config, tracing_span, send_progress: _ } = self; + let Self { timeline, path, mime_type, config, tracing_span, use_send_queue, send_progress } = + self; let fut = async move { let filename = path @@ -57,9 +72,18 @@ impl<'a> IntoFuture for SendAttachment<'a> { .ok_or(Error::InvalidAttachmentFileName)?; let data = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?; - let send_queue = timeline.room().send_queue(); - let fut = send_queue.send_attachment(filename, mime_type, data, config); - fut.await.map_err(|_| Error::FailedSendingAttachment)?; + if use_send_queue { + let send_queue = timeline.room().send_queue(); + let fut = send_queue.send_attachment(filename, mime_type, data, config); + fut.await.map_err(|_| Error::FailedSendingAttachment)?; + } else { + let fut = timeline + .room() + .send_attachment(filename, &mime_type, data, config) + .with_send_progress_observable(send_progress) + .store_in_cache(); + fut.await.map_err(|_| Error::FailedSendingAttachment)?; + } Ok(()) }; From 566a13b16e146832b456f550c95c5bdf8b45c0e9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 14:44:55 +0100 Subject: [PATCH 458/979] refactor!(media): inline `MediaThumbnailSize` into `MediaThumbnailSettings` Changelog: all the fields of `MediaThumbnailSize` have been inlined into `MediaThumbnailSettings`, and the former type has been removed. --- crates/matrix-sdk-base/src/media.rs | 21 ++++--------------- crates/matrix-sdk/src/media.rs | 13 ++++++------ crates/matrix-sdk/src/room/mod.rs | 10 ++++----- crates/matrix-sdk/src/send_queue/upload.rs | 6 ++++-- crates/matrix-sdk/tests/integration/media.rs | 14 +++++++++---- .../tests/integration/room/attachment/mod.rs | 18 +++++++--------- .../tests/integration/send_queue.rs | 18 +++++++--------- 7 files changed, 43 insertions(+), 57 deletions(-) diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index 15950be168c..ad4852751eb 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -44,9 +44,9 @@ impl UniqueKey for MediaFormat { } } -/// The requested size of a media thumbnail. +/// The desired settings of a media thumbnail. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MediaThumbnailSize { +pub struct MediaThumbnailSettings { /// The desired resizing method. pub method: Method, @@ -57,19 +57,6 @@ pub struct MediaThumbnailSize { /// The desired height of the thumbnail. The actual thumbnail may not match /// the size specified. pub height: UInt, -} - -impl UniqueKey for MediaThumbnailSize { - fn unique_key(&self) -> String { - format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height) - } -} - -/// The desired settings of a media thumbnail. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MediaThumbnailSettings { - /// The desired size of the thumbnail. - pub size: MediaThumbnailSize, /// If we want to request an animated thumbnail from the homeserver. /// @@ -84,13 +71,13 @@ impl MediaThumbnailSettings { /// Constructs a new `MediaThumbnailSettings` with the given method, width /// and height. pub fn new(method: Method, width: UInt, height: UInt) -> Self { - Self { size: MediaThumbnailSize { method, width, height }, animated: false } + Self { method, width, height, animated: false } } } impl UniqueKey for MediaThumbnailSettings { fn unique_key(&self) -> String { - let mut key = self.size.unique_key(); + let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height); if self.animated { key.push_str(UNIQUE_SEPARATOR); diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index f27845fddb4..ffb3f92da6f 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -437,16 +437,17 @@ impl Media { content } + MediaSource::Plain(uri) => { if let MediaFormat::Thumbnail(settings) = &request.format { if use_auth { let mut request = authenticated_media::get_content_thumbnail::v1::Request::from_uri( uri, - settings.size.width, - settings.size.height, + settings.width, + settings.height, )?; - request.method = Some(settings.size.method.clone()); + request.method = Some(settings.method.clone()); request.animated = Some(settings.animated); self.client.send(request, request_config).await?.file @@ -455,10 +456,10 @@ impl Media { let request = { let mut request = media::get_content_thumbnail::v3::Request::from_url( uri, - settings.size.width, - settings.size.height, + settings.width, + settings.height, )?; - request.method = Some(settings.size.method.clone()); + request.method = Some(settings.method.clone()); request.animated = Some(settings.animated); request }; diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 8253449c17c..7ec2f77e431 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -41,7 +41,7 @@ use matrix_sdk_base::{ deserialized_responses::{ RawAnySyncOrStrippedState, RawSyncOrStrippedState, SyncOrStrippedState, TimelineEvent, }, - media::{MediaThumbnailSettings, MediaThumbnailSize}, + media::MediaThumbnailSettings, store::StateStoreExt, ComposerDraft, RoomInfoNotableUpdateReasons, RoomMemberships, StateChanges, StateStoreDataKey, StateStoreDataValue, @@ -2010,11 +2010,9 @@ impl Room { let request = MediaRequest { source: source.clone(), format: MediaFormat::Thumbnail(MediaThumbnailSettings { - size: MediaThumbnailSize { - method: ruma::media::Method::Scale, - width, - height, - }, + method: ruma::media::Method::Scale, + width, + height, animated: false, }), }; diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 2bd6a8c4780..192c5dcfa1d 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -15,7 +15,7 @@ //! Private implementations of the media upload mechanism. use matrix_sdk_base::{ - media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, + media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, store::{ ChildTransactionId, FinishUploadThumbnailInfo, QueuedRequestKind, SentMediaInfo, SentRequestKey, SerializableEventContent, @@ -76,7 +76,9 @@ fn make_local_thumbnail_media_request( let source = MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.localhost/{}", txn_id))); let format = MediaFormat::Thumbnail(MediaThumbnailSettings { - size: MediaThumbnailSize { method: Method::Scale, width, height }, + method: Method::Scale, + width, + height, animated: false, }); MediaRequest { source, format } diff --git a/crates/matrix-sdk/tests/integration/media.rs b/crates/matrix-sdk/tests/integration/media.rs index 70ac2eac68d..a44d65627b3 100644 --- a/crates/matrix-sdk/tests/integration/media.rs +++ b/crates/matrix-sdk/tests/integration/media.rs @@ -1,7 +1,7 @@ use matrix_sdk::{ config::RequestConfig, matrix_auth::{MatrixSession, MatrixSessionTokens}, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, + media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, test_utils::logged_in_client_with_server, Client, SessionMeta, }; @@ -183,7 +183,9 @@ async fn test_get_media_file_no_auth() { .await; let settings = MediaThumbnailSettings { - size: MediaThumbnailSize { method: Method::Crop, width: uint!(100), height: uint!(100) }, + method: Method::Crop, + width: uint!(100), + height: uint!(100), animated: true, }; client.media().get_thumbnail(&event_content, settings, true).await.unwrap(); @@ -293,7 +295,9 @@ async fn test_get_media_file_with_auth_matrix_1_11() { .await; let settings = MediaThumbnailSettings { - size: MediaThumbnailSize { method: Method::Crop, width: uint!(100), height: uint!(100) }, + method: Method::Crop, + width: uint!(100), + height: uint!(100), animated: true, }; client.media().get_thumbnail(&event_content, settings, true).await.unwrap(); @@ -406,7 +410,9 @@ async fn test_get_media_file_with_auth_matrix_stable_feature() { .await; let settings = MediaThumbnailSettings { - size: MediaThumbnailSize { method: Method::Crop, width: uint!(100), height: uint!(100) }, + method: Method::Crop, + width: uint!(100), + height: uint!(100), animated: true, }; client.media().get_thumbnail(&event_content, settings, true).await.unwrap(); diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 252721f6d5d..4e0a7fb6e5f 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -6,7 +6,7 @@ use matrix_sdk::{ Thumbnail, }, config::SyncSettings, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, + media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{async_test, mocks::mock_encryption_state, test_json, DEFAULT_TEST_ROOM_ID}; @@ -249,11 +249,9 @@ async fn test_room_attachment_send_info_thumbnail() { let thumbnail_request = MediaRequest { source: MediaSource::Plain(thumbnail_mxc.clone()), format: MediaFormat::Thumbnail(MediaThumbnailSettings { - size: MediaThumbnailSize { - method: ruma::media::Method::Scale, - width: uint!(480), - height: uint!(360), - }, + method: ruma::media::Method::Scale, + width: uint!(480), + height: uint!(360), animated: false, }), }; @@ -312,11 +310,9 @@ async fn test_room_attachment_send_info_thumbnail() { let thumbnail_request = MediaRequest { source: MediaSource::Plain(thumbnail_mxc), format: MediaFormat::Thumbnail(MediaThumbnailSettings { - size: MediaThumbnailSize { - method: ruma::media::Method::Scale, - width: uint!(42), - height: uint!(1337), - }, + method: ruma::media::Method::Scale, + width: uint!(42), + height: uint!(1337), animated: false, }), }; diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index cf6ec31b014..6573e0d0243 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -11,7 +11,7 @@ use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, Thumbnail}, config::{RequestConfig, StoreConfig}, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings, MediaThumbnailSize}, + media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, send_queue::{ LocalEcho, LocalEchoContent, RoomSendQueueError, RoomSendQueueStorageError, RoomSendQueueUpdate, @@ -2150,11 +2150,9 @@ async fn test_media_uploads() { // TODO: extract this reasonable query into a helper function shared across the // codebase format: MediaFormat::Thumbnail(MediaThumbnailSettings { - size: MediaThumbnailSize { - height: tinfo.height.unwrap(), - width: tinfo.width.unwrap(), - method: Method::Scale, - }, + height: tinfo.height.unwrap(), + width: tinfo.width.unwrap(), + method: Method::Scale, animated: false, }), }, @@ -2224,11 +2222,9 @@ async fn test_media_uploads() { // TODO: extract this reasonable query into a helper function shared across the // codebase format: MediaFormat::Thumbnail(MediaThumbnailSettings { - size: MediaThumbnailSize { - height: tinfo.height.unwrap(), - width: tinfo.width.unwrap(), - method: Method::Scale, - }, + height: tinfo.height.unwrap(), + width: tinfo.width.unwrap(), + method: Method::Scale, animated: false, }), }, From 77ee02f5291d1751289b8e9d06832bb71e6cfeaa Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 14:48:53 +0100 Subject: [PATCH 459/979] refactor!(media): rename `MediaRequest` to `MediaRequestParameters` Because it's not a request we send to the server; it's some of the request parameters. --- bindings/matrix-sdk-ffi/src/client.rs | 9 +++--- .../event_cache_store/integration_tests.rs | 20 +++++++------ .../src/event_cache_store/memory_store.rs | 16 +++++++---- .../src/event_cache_store/traits.rs | 28 +++++++++++-------- crates/matrix-sdk-base/src/media.rs | 14 ++++++---- .../matrix-sdk-base/src/store/send_queue.rs | 6 ++-- .../src/event_cache_store.rs | 26 ++++++++++------- crates/matrix-sdk/src/account.rs | 4 +-- crates/matrix-sdk/src/media.rs | 21 +++++++++----- crates/matrix-sdk/src/room/member.rs | 4 +-- crates/matrix-sdk/src/room/mod.rs | 9 +++--- crates/matrix-sdk/src/send_queue.rs | 6 ++-- crates/matrix-sdk/src/send_queue/upload.rs | 19 +++++++------ crates/matrix-sdk/tests/integration/media.rs | 4 +-- .../tests/integration/room/attachment/mod.rs | 10 +++---- .../tests/integration/send_queue.rs | 13 +++++---- 16 files changed, 123 insertions(+), 86 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index dd9f6219d82..0b5208c510f 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -8,7 +8,8 @@ use std::{ use anyhow::{anyhow, Context as _}; use matrix_sdk::{ media::{ - MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSettings, + MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters, + MediaThumbnailSettings, }, oidc::{ registrations::{ClientId, OidcRegistrations}, @@ -442,7 +443,7 @@ impl Client { .inner .media() .get_media_file( - &MediaRequest { source, format: MediaFormat::File }, + &MediaRequestParameters { source, format: MediaFormat::File }, filename, &mime_type, use_cache, @@ -721,7 +722,7 @@ impl Client { Ok(self .inner .media() - .get_media_content(&MediaRequest { source, format: MediaFormat::File }, true) + .get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true) .await?) } @@ -738,7 +739,7 @@ impl Client { .inner .media() .get_media_content( - &MediaRequest { + &MediaRequestParameters { source, format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( Method::Scale, diff --git a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs index b9960489980..788aa4a0cae 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs @@ -20,7 +20,7 @@ use ruma::{ }; use super::DynEventCacheStore; -use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSettings}; +use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}; /// `EventCacheStore` integration tests. /// @@ -41,9 +41,11 @@ pub trait EventCacheStoreIntegrationTests { impl EventCacheStoreIntegrationTests for DynEventCacheStore { async fn test_media_content(&self) { let uri = mxc_uri!("mxc://localhost/media"); - let request_file = - MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File }; - let request_thumbnail = MediaRequest { + let request_file = MediaRequestParameters { + source: MediaSource::Plain(uri.to_owned()), + format: MediaFormat::File, + }; + let request_thumbnail = MediaRequestParameters { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( Method::Crop, @@ -53,7 +55,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { }; let other_uri = mxc_uri!("mxc://localhost/media-other"); - let request_other_file = MediaRequest { + let request_other_file = MediaRequestParameters { source: MediaSource::Plain(other_uri.to_owned()), format: MediaFormat::File, }; @@ -145,8 +147,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { async fn test_replace_media_key(&self) { let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d"); - let req = - MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File }; + let req = MediaRequestParameters { + source: MediaSource::Plain(uri.to_owned()), + format: MediaFormat::File, + }; let content = "hello".as_bytes().to_owned(); @@ -161,7 +165,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { // Replacing a media request works. let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d"); - let new_req = MediaRequest { + let new_req = MediaRequestParameters { source: MediaSource::Plain(new_uri.to_owned()), format: MediaFormat::File, }; diff --git a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs index d75c9f09776..1b5debbccee 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs @@ -21,7 +21,7 @@ use matrix_sdk_common::{ use ruma::{MxcUri, OwnedMxcUri}; use super::{EventCacheStore, EventCacheStoreError, Result}; -use crate::media::{MediaRequest, UniqueKey as _}; +use crate::media::{MediaRequestParameters, UniqueKey as _}; /// In-memory, non-persistent implementation of the `EventCacheStore`. /// @@ -66,7 +66,11 @@ impl EventCacheStore for MemoryStore { Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) } - async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { + async fn add_media_content( + &self, + request: &MediaRequestParameters, + data: Vec, + ) -> Result<()> { // Avoid duplication. Let's try to remove it first. self.remove_media_content(request).await?; // Now, let's add it. @@ -77,8 +81,8 @@ impl EventCacheStore for MemoryStore { async fn replace_media_key( &self, - from: &MediaRequest, - to: &MediaRequest, + from: &MediaRequestParameters, + to: &MediaRequestParameters, ) -> Result<(), Self::Error> { let expected_key = from.unique_key(); @@ -91,7 +95,7 @@ impl EventCacheStore for MemoryStore { Ok(()) } - async fn get_media_content(&self, request: &MediaRequest) -> Result>> { + async fn get_media_content(&self, request: &MediaRequestParameters) -> Result>> { let expected_key = request.unique_key(); let media = self.media.read().unwrap(); @@ -100,7 +104,7 @@ impl EventCacheStore for MemoryStore { })) } - async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { + async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> { let expected_key = request.unique_key(); let mut media = self.media.write().unwrap(); diff --git a/crates/matrix-sdk-base/src/event_cache_store/traits.rs b/crates/matrix-sdk-base/src/event_cache_store/traits.rs index eaa1ada194c..6742851870d 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/traits.rs @@ -19,7 +19,7 @@ use matrix_sdk_common::AsyncTraitDeps; use ruma::MxcUri; use super::EventCacheStoreError; -use crate::media::MediaRequest; +use crate::media::MediaRequestParameters; /// An abstract trait that can be used to implement different store backends /// for the event cache of the SDK. @@ -46,7 +46,7 @@ pub trait EventCacheStore: AsyncTraitDeps { /// * `content` - The content of the file. async fn add_media_content( &self, - request: &MediaRequest, + request: &MediaRequestParameters, content: Vec, ) -> Result<(), Self::Error>; @@ -71,8 +71,8 @@ pub trait EventCacheStore: AsyncTraitDeps { /// * `to` - The new `MediaRequest` of the file. async fn replace_media_key( &self, - from: &MediaRequest, - to: &MediaRequest, + from: &MediaRequestParameters, + to: &MediaRequestParameters, ) -> Result<(), Self::Error>; /// Get a media file's content out of the media store. @@ -82,7 +82,7 @@ pub trait EventCacheStore: AsyncTraitDeps { /// * `request` - The `MediaRequest` of the file. async fn get_media_content( &self, - request: &MediaRequest, + request: &MediaRequestParameters, ) -> Result>, Self::Error>; /// Remove a media file's content from the media store. @@ -90,7 +90,10 @@ pub trait EventCacheStore: AsyncTraitDeps { /// # Arguments /// /// * `request` - The `MediaRequest` of the file. - async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error>; + async fn remove_media_content( + &self, + request: &MediaRequestParameters, + ) -> Result<(), Self::Error>; /// Remove all the media files' content associated to an `MxcUri` from the /// media store. @@ -127,7 +130,7 @@ impl EventCacheStore for EraseEventCacheStoreError { async fn add_media_content( &self, - request: &MediaRequest, + request: &MediaRequestParameters, content: Vec, ) -> Result<(), Self::Error> { self.0.add_media_content(request, content).await.map_err(Into::into) @@ -135,20 +138,23 @@ impl EventCacheStore for EraseEventCacheStoreError { async fn replace_media_key( &self, - from: &MediaRequest, - to: &MediaRequest, + from: &MediaRequestParameters, + to: &MediaRequestParameters, ) -> Result<(), Self::Error> { self.0.replace_media_key(from, to).await.map_err(Into::into) } async fn get_media_content( &self, - request: &MediaRequest, + request: &MediaRequestParameters, ) -> Result>, Self::Error> { self.0.get_media_content(request).await.map_err(Into::into) } - async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error> { + async fn remove_media_content( + &self, + request: &MediaRequestParameters, + ) -> Result<(), Self::Error> { self.0.remove_media_content(request).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index ad4852751eb..b9ea760f644 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -97,9 +97,11 @@ impl UniqueKey for MediaSource { } } -/// A request for media data. +/// Parameters for a request for retrieve media data. +/// +/// This is used as a key in the media cache too. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MediaRequest { +pub struct MediaRequestParameters { /// The source of the media file. pub source: MediaSource, @@ -107,7 +109,7 @@ pub struct MediaRequest { pub format: MediaFormat, } -impl MediaRequest { +impl MediaRequestParameters { /// Get the [`MxcUri`] from `Self`. pub fn uri(&self) -> &MxcUri { match &self.source { @@ -117,7 +119,7 @@ impl MediaRequest { } } -impl UniqueKey for MediaRequest { +impl UniqueKey for MediaRequestParameters { fn unique_key(&self) -> String { format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key()) } @@ -213,14 +215,14 @@ mod tests { fn test_media_request_url() { let mxc_uri = mxc_uri!("mxc://homeserver/media"); - let plain = MediaRequest { + let plain = MediaRequestParameters { source: MediaSource::Plain(mxc_uri.to_owned()), format: MediaFormat::File, }; assert_eq!(plain.uri(), mxc_uri); - let file = MediaRequest { + let file = MediaRequestParameters { source: MediaSource::Encrypted(Box::new( serde_json::from_value(json!({ "url": mxc_uri, diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 51bc1c7f1c6..f5f0ccaa7e0 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -27,7 +27,7 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; -use crate::media::MediaRequest; +use crate::media::MediaRequestParameters; /// A thin wrapper to serialize a `AnyMessageLikeEventContent`. #[derive(Clone, Serialize, Deserialize)] @@ -95,7 +95,7 @@ pub enum QueuedRequestKind { /// The cache key used to retrieve the media's bytes in the event cache /// store. - cache_key: MediaRequest, + cache_key: MediaRequestParameters, /// An optional media source for a thumbnail already uploaded. thumbnail_source: Option, @@ -216,7 +216,7 @@ pub enum DependentQueuedRequestKind { /// Media request necessary to retrieve the file itself (not the /// thumbnail). - cache_key: MediaRequest, + cache_key: MediaRequestParameters, /// To which media transaction id does this upload relate to? related_to: OwnedTransactionId, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 0caeda04046..77387de9b6d 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ event_cache_store::EventCacheStore, - media::{MediaRequest, UniqueKey}, + media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; use ruma::MilliSecondsSinceUnixEpoch; @@ -182,7 +182,11 @@ impl EventCacheStore for SqliteEventCacheStore { Ok(num_touched == 1) } - async fn add_media_content(&self, request: &MediaRequest, content: Vec) -> Result<()> { + async fn add_media_content( + &self, + request: &MediaRequestParameters, + content: Vec, + ) -> Result<()> { let uri = self.encode_key(keys::MEDIA, request.source.unique_key()); let format = self.encode_key(keys::MEDIA, request.format.unique_key()); let data = self.encode_value(content)?; @@ -199,8 +203,8 @@ impl EventCacheStore for SqliteEventCacheStore { async fn replace_media_key( &self, - from: &MediaRequest, - to: &MediaRequest, + from: &MediaRequestParameters, + to: &MediaRequestParameters, ) -> Result<(), Self::Error> { let prev_uri = self.encode_key(keys::MEDIA, from.source.unique_key()); let prev_format = self.encode_key(keys::MEDIA, from.format.unique_key()); @@ -219,7 +223,7 @@ impl EventCacheStore for SqliteEventCacheStore { Ok(()) } - async fn get_media_content(&self, request: &MediaRequest) -> Result>> { + async fn get_media_content(&self, request: &MediaRequestParameters) -> Result>> { let uri = self.encode_key(keys::MEDIA, request.source.unique_key()); let format = self.encode_key(keys::MEDIA, request.format.unique_key()); @@ -247,7 +251,7 @@ impl EventCacheStore for SqliteEventCacheStore { data.map(|v| self.decode_value(&v).map(Into::into)).transpose() } - async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { + async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> { let uri = self.encode_key(keys::MEDIA, request.source.unique_key()); let format = self.encode_key(keys::MEDIA, request.format.unique_key()); @@ -277,7 +281,7 @@ mod tests { use matrix_sdk_base::{ event_cache_store::{EventCacheStore, EventCacheStoreError}, event_cache_store_integration_tests, event_cache_store_integration_tests_time, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, + media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, }; use matrix_sdk_test::async_test; use once_cell::sync::Lazy; @@ -318,9 +322,11 @@ mod tests { async fn test_last_access() { let event_cache_store = get_event_cache_store().await.expect("creating media cache failed"); let uri = mxc_uri!("mxc://localhost/media"); - let file_request = - MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File }; - let thumbnail_request = MediaRequest { + let file_request = MediaRequestParameters { + source: MediaSource::Plain(uri.to_owned()), + format: MediaFormat::File, + }; + let thumbnail_request = MediaRequestParameters { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( Method::Crop, diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index d46ed6c8058..249bfa3129e 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -15,7 +15,7 @@ // limitations under the License. use matrix_sdk_base::{ - media::{MediaFormat, MediaRequest}, + media::{MediaFormat, MediaRequestParameters}, store::StateStoreExt, StateStoreDataKey, StateStoreDataValue, }; @@ -217,7 +217,7 @@ impl Account { /// ``` pub async fn get_avatar(&self, format: MediaFormat) -> Result>> { if let Some(url) = self.get_avatar_url().await? { - let request = MediaRequest { source: MediaSource::Plain(url), format }; + let request = MediaRequestParameters { source: MediaSource::Plain(url), format }; Ok(Some(self.client.media().get_media_content(&request, true).await?)) } else { Ok(None) diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index ffb3f92da6f..5111812ebbf 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -296,7 +296,7 @@ impl Media { #[cfg(not(target_arch = "wasm32"))] pub async fn get_media_file( &self, - request: &MediaRequest, + request: &MediaRequestParameters, filename: Option, content_type: &Mime, use_cache: bool, @@ -371,7 +371,7 @@ impl Media { /// * `use_cache` - If we should use the media cache for this request. pub async fn get_media_content( &self, - request: &MediaRequest, + request: &MediaRequestParameters, use_cache: bool, ) -> Result> { // Read from the cache. @@ -494,7 +494,7 @@ impl Media { /// # Arguments /// /// * `request` - The `MediaRequest` of the content. - pub async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { + pub async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> { Ok(self.client.event_cache_store().lock().await?.remove_media_content(request).await?) } @@ -530,7 +530,10 @@ impl Media { ) -> Result>> { let Some(source) = event_content.source() else { return Ok(None) }; let file = self - .get_media_content(&MediaRequest { source, format: MediaFormat::File }, use_cache) + .get_media_content( + &MediaRequestParameters { source, format: MediaFormat::File }, + use_cache, + ) .await?; Ok(Some(file)) } @@ -545,7 +548,11 @@ impl Media { /// * `event_content` - The media event content. pub async fn remove_file(&self, event_content: &impl MediaEventContent) -> Result<()> { if let Some(source) = event_content.source() { - self.remove_media_content(&MediaRequest { source, format: MediaFormat::File }).await?; + self.remove_media_content(&MediaRequestParameters { + source, + format: MediaFormat::File, + }) + .await?; } Ok(()) @@ -578,7 +585,7 @@ impl Media { let Some(source) = event_content.thumbnail_source() else { return Ok(None) }; let thumbnail = self .get_media_content( - &MediaRequest { source, format: MediaFormat::Thumbnail(settings) }, + &MediaRequestParameters { source, format: MediaFormat::Thumbnail(settings) }, use_cache, ) .await?; @@ -602,7 +609,7 @@ impl Media { settings: MediaThumbnailSettings, ) -> Result<()> { if let Some(source) = event_content.source() { - self.remove_media_content(&MediaRequest { + self.remove_media_content(&MediaRequestParameters { source, format: MediaFormat::Thumbnail(settings), }) diff --git a/crates/matrix-sdk/src/room/member.rs b/crates/matrix-sdk/src/room/member.rs index 59c774af616..6b5ae84dad3 100644 --- a/crates/matrix-sdk/src/room/member.rs +++ b/crates/matrix-sdk/src/room/member.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use ruma::events::room::MediaSource; use crate::{ - media::{MediaFormat, MediaRequest}, + media::{MediaFormat, MediaRequestParameters}, BaseRoomMember, Client, Result, }; @@ -61,7 +61,7 @@ impl RoomMember { /// ``` pub async fn avatar(&self, format: MediaFormat) -> Result>> { let Some(url) = self.avatar_url() else { return Ok(None) }; - let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format }; + let request = MediaRequestParameters { source: MediaSource::Plain(url.to_owned()), format }; Ok(Some(self.client.media().get_media_content(&request, true).await?)) } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 7ec2f77e431..2aba1c9b852 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -132,7 +132,7 @@ use crate::{ error::{BeaconError, WrongRoomState}, event_cache::{self, EventCacheDropHandles, RoomEventCache}, event_handler::{EventHandler, EventHandlerDropGuard, EventHandlerHandle, SyncEvent}, - media::{MediaFormat, MediaRequest}, + media::{MediaFormat, MediaRequestParameters}, notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode}, room::power_levels::{RoomPowerLevelChanges, RoomPowerLevelsExt}, sync::RoomUpdate, @@ -264,7 +264,7 @@ impl Room { /// ``` pub async fn avatar(&self, format: MediaFormat) -> Result>> { let Some(url) = self.avatar_url() else { return Ok(None) }; - let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format }; + let request = MediaRequestParameters { source: MediaSource::Plain(url.to_owned()), format }; Ok(Some(self.client.media().get_media_content(&request, true).await?)) } @@ -1994,7 +1994,8 @@ impl Room { // properly, so only log errors during caching. debug!("caching the media"); - let request = MediaRequest { source: media_source.clone(), format: MediaFormat::File }; + let request = + MediaRequestParameters { source: media_source.clone(), format: MediaFormat::File }; if let Err(err) = cache_store_lock_guard.add_media_content(&request, data).await { warn!("unable to cache the media after uploading it: {err}"); @@ -2007,7 +2008,7 @@ impl Room { // Do a best guess at figuring the media request: not animated, cropped // thumbnail of the original size. - let request = MediaRequest { + let request = MediaRequestParameters { source: source.clone(), format: MediaFormat::Thumbnail(MediaThumbnailSettings { method: ruma::media::Method::Scale, diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 1ec961f1eaf..0b96416d049 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -140,7 +140,7 @@ use std::{ use as_variant::as_variant; use matrix_sdk_base::{ event_cache_store::EventCacheStoreError, - media::MediaRequest, + media::MediaRequestParameters, store::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, @@ -1034,8 +1034,8 @@ impl QueueStorage { content_type: Mime, send_event_txn: OwnedTransactionId, upload_file_txn: OwnedTransactionId, - file_media_request: MediaRequest, - thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequest, Mime)>, + file_media_request: MediaRequestParameters, + thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequestParameters, Mime)>, ) -> Result<(), RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. // TODO refactor to make the relationship between being_sent and the store more diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 192c5dcfa1d..e3142dda028 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -15,7 +15,7 @@ //! Private implementations of the media upload mechanism. use matrix_sdk_base::{ - media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, + media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, store::{ ChildTransactionId, FinishUploadThumbnailInfo, QueuedRequestKind, SentMediaInfo, SentRequestKey, SerializableEventContent, @@ -47,7 +47,7 @@ use crate::{ /// sending it. /// /// This uses a MXC ID that is only locally valid. -fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequest { +fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters { // This mustn't represent a potentially valid media server, otherwise it'd be // possible for an attacker to return malicious content under some // preconditions (e.g. the cache store has been cleared before the upload @@ -55,7 +55,7 @@ fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequest { // which is guaranteed to be on the local machine. As a result, the only attack // possible would be coming from the user themselves, which we consider a // non-threat. - MediaRequest { + MediaRequestParameters { source: MediaSource::Plain(OwnedMxcUri::from(format!( "mxc://send-queue.localhost/{txn_id}" ))), @@ -71,7 +71,7 @@ fn make_local_thumbnail_media_request( txn_id: &TransactionId, height: UInt, width: UInt, -) -> MediaRequest { +) -> MediaRequestParameters { // See comment in [`make_local_file_media_request`]. let source = MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.localhost/{}", txn_id))); @@ -81,7 +81,7 @@ fn make_local_thumbnail_media_request( height, animated: false, }); - MediaRequest { source, format } + MediaRequestParameters { source, format } } /// Replace the source by the final ones in all the media types handled by @@ -300,7 +300,10 @@ impl QueueStorage { .event_cache_store() .replace_media_key( &from_req, - &MediaRequest { source: sent_media.file.clone(), format: MediaFormat::File }, + &MediaRequestParameters { + source: sent_media.file.clone(), + format: MediaFormat::File, + }, ) .await .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; @@ -321,7 +324,7 @@ impl QueueStorage { .event_cache_store() .replace_media_key( &from_req, - &MediaRequest { source: new_source, format: new_format }, + &MediaRequestParameters { source: new_source, format: new_format }, ) .await .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; @@ -358,7 +361,7 @@ impl QueueStorage { next_upload_txn: OwnedTransactionId, parent_key: SentRequestKey, content_type: String, - cache_key: MediaRequest, + cache_key: MediaRequestParameters, event_txn: OwnedTransactionId, ) -> Result<(), RoomSendQueueError> { // The thumbnail has been sent, now transform the dependent file upload request diff --git a/crates/matrix-sdk/tests/integration/media.rs b/crates/matrix-sdk/tests/integration/media.rs index a44d65627b3..dc108b56a39 100644 --- a/crates/matrix-sdk/tests/integration/media.rs +++ b/crates/matrix-sdk/tests/integration/media.rs @@ -1,7 +1,7 @@ use matrix_sdk::{ config::RequestConfig, matrix_auth::{MatrixSession, MatrixSessionTokens}, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, + media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, test_utils::logged_in_client_with_server, Client, SessionMeta, }; @@ -35,7 +35,7 @@ async fn test_get_media_content_no_auth() { let media = client.media(); - let request = MediaRequest { + let request = MediaRequestParameters { source: MediaSource::Plain(mxc_uri!("mxc://localhost/textfile").to_owned()), format: MediaFormat::File, }; diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 4e0a7fb6e5f..012a0832168 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -6,7 +6,7 @@ use matrix_sdk::{ Thumbnail, }, config::SyncSettings, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, + media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{async_test, mocks::mock_encryption_state, test_json, DEFAULT_TEST_ROOM_ID}; @@ -245,8 +245,8 @@ async fn test_room_attachment_send_info_thumbnail() { // Preconditions: nothing is found in the cache. let media_request = - MediaRequest { source: MediaSource::Plain(media_mxc), format: MediaFormat::File }; - let thumbnail_request = MediaRequest { + MediaRequestParameters { source: MediaSource::Plain(media_mxc), format: MediaFormat::File }; + let thumbnail_request = MediaRequestParameters { source: MediaSource::Plain(thumbnail_mxc.clone()), format: MediaFormat::Thumbnail(MediaThumbnailSettings { method: ruma::media::Method::Scale, @@ -297,7 +297,7 @@ async fn test_room_attachment_send_info_thumbnail() { let _ = client .media() .get_media_content( - &MediaRequest { + &MediaRequestParameters { source: MediaSource::Plain(thumbnail_mxc.clone()), format: MediaFormat::File, }, @@ -307,7 +307,7 @@ async fn test_room_attachment_send_info_thumbnail() { .unwrap_err(); // But it is not found when requesting it as a thumbnail with a different size. - let thumbnail_request = MediaRequest { + let thumbnail_request = MediaRequestParameters { source: MediaSource::Plain(thumbnail_mxc), format: MediaFormat::Thumbnail(MediaThumbnailSettings { method: ruma::media::Method::Scale, diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 6573e0d0243..7ec87d6023e 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -11,7 +11,7 @@ use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, Thumbnail}, config::{RequestConfig, StoreConfig}, - media::{MediaFormat, MediaRequest, MediaThumbnailSettings}, + media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, send_queue::{ LocalEcho, LocalEchoContent, RoomSendQueueError, RoomSendQueueStorageError, RoomSendQueueUpdate, @@ -2122,7 +2122,10 @@ async fn test_media_uploads() { // The media is immediately available from the cache. let file_media = client .media() - .get_media_content(&MediaRequest { source: local_source, format: MediaFormat::File }, true) + .get_media_content( + &MediaRequestParameters { source: local_source, format: MediaFormat::File }, + true, + ) .await .expect("media should be found"); assert_eq!(file_media, b"hello world"); @@ -2145,7 +2148,7 @@ async fn test_media_uploads() { let thumbnail_media = client .media() .get_media_content( - &MediaRequest { + &MediaRequestParameters { source: local_thumbnail_source, // TODO: extract this reasonable query into a helper function shared across the // codebase @@ -2203,7 +2206,7 @@ async fn test_media_uploads() { let file_media = client .media() .get_media_content( - &MediaRequest { source: new_content.source, format: MediaFormat::File }, + &MediaRequestParameters { source: new_content.source, format: MediaFormat::File }, true, ) .await @@ -2217,7 +2220,7 @@ async fn test_media_uploads() { let thumbnail_media = client .media() .get_media_content( - &MediaRequest { + &MediaRequestParameters { source: new_thumbnail_source, // TODO: extract this reasonable query into a helper function shared across the // codebase From 8d07f36247e92c5cfae4c8d0b60f6ea56743bcdf Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 15:18:51 +0100 Subject: [PATCH 460/979] =?UTF-8?q?chore(send=20queue):=20adapt=20to=20new?= =?UTF-8?q?=20locks=20around=20the=20event=20cache=20store=20=F0=9F=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/matrix-sdk/src/send_queue.rs | 19 +++- crates/matrix-sdk/src/send_queue/upload.rs | 116 +++++++++++---------- 2 files changed, 78 insertions(+), 57 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 0b96416d049..a842a9e895e 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -146,6 +146,7 @@ use matrix_sdk_base::{ FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, SentMediaInfo, SentRequestKey, SerializableEventContent, }, + store_locks::LockStoreError, RoomState, StoreError, }; use matrix_sdk_common::executor::{spawn, JoinHandle}; @@ -693,10 +694,16 @@ impl RoomSendQueue { }) })?; - let data = - room.client().event_cache_store().get_media_content(&cache_key).await?.ok_or( - crate::Error::SendQueueWedgeError(QueueWedgeError::MissingMediaContent), - )?; + let data = room + .client() + .event_cache_store() + .lock() + .await? + .get_media_content(&cache_key) + .await? + .ok_or(crate::Error::SendQueueWedgeError( + QueueWedgeError::MissingMediaContent, + ))?; #[cfg(feature = "e2e-encryption")] let media_source = if room.is_encrypted().await? { @@ -1676,6 +1683,10 @@ pub enum RoomSendQueueStorageError { #[error(transparent)] EventCacheStoreError(#[from] EventCacheStoreError), + /// Error caused when attempting to get a handle on the event cache store. + #[error(transparent)] + LockError(#[from] LockStoreError), + /// Error caused when (de)serializing into/from json. #[error(transparent)] JsonSerialization(#[from] serde_json::Error), diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index e3142dda028..2c38977387c 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -160,60 +160,66 @@ impl RoomSendQueue { Span::current().record("event_txn", tracing::field::display(&*send_event_txn)); debug!(filename, %content_type, %upload_file_txn, "sending an attachment"); - // Cache the file itself in the cache store. let file_media_request = make_local_file_media_request(&upload_file_txn); - room.client() - .event_cache_store() - .add_media_content(&file_media_request, data.clone()) - .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - // Process the thumbnail, if it's been provided. - let (upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info) = if let Some( - thumbnail, - ) = - config.thumbnail.take() - { - // Normalize information to retrieve the thumbnail in the cache store. - let info = thumbnail.info.as_ref(); - let height = info.and_then(|info| info.height).unwrap_or_else(|| { - trace!("thumbnail height is unknown, using 0 for the cache entry"); - uint!(0) - }); - let width = info.and_then(|info| info.width).unwrap_or_else(|| { - trace!("thumbnail width is unknown, using 0 for the cache entry"); - uint!(0) - }); - - let txn = TransactionId::new(); - trace!(upload_thumbnail_txn = %txn, thumbnail_size = ?(height, width), "attachment has a thumbnail"); - - // Cache thumbnail in the cache store. - let thumbnail_media_request = make_local_thumbnail_media_request(&txn, height, width); - room.client() + let (upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info) = { + let client = room.client(); + let cache_store = client .event_cache_store() - .add_media_content(&thumbnail_media_request, thumbnail.data.clone()) + .lock() .await - .map_err(|err| RoomSendQueueError::StorageError(err.into()))?; - - // Create the information required for filling the thumbnail section of the - // media event. - let thumbnail_info = - Box::new(assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { - mimetype: Some(thumbnail.content_type.as_ref().to_owned()) - })); - - ( - Some(txn.clone()), - Some((thumbnail_media_request.source.clone(), thumbnail_info)), - Some(( - FinishUploadThumbnailInfo { txn, width, height }, - thumbnail_media_request, - thumbnail.content_type, - )), - ) - } else { - Default::default() + .map_err(RoomSendQueueStorageError::LockError)?; + + // Cache the file itself in the cache store. + cache_store + .add_media_content(&file_media_request, data.clone()) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + + // Process the thumbnail, if it's been provided. + if let Some(thumbnail) = config.thumbnail.take() { + // Normalize information to retrieve the thumbnail in the cache store. + let info = thumbnail.info.as_ref(); + let height = info.and_then(|info| info.height).unwrap_or_else(|| { + trace!("thumbnail height is unknown, using 0 for the cache entry"); + uint!(0) + }); + let width = info.and_then(|info| info.width).unwrap_or_else(|| { + trace!("thumbnail width is unknown, using 0 for the cache entry"); + uint!(0) + }); + + let txn = TransactionId::new(); + trace!(upload_thumbnail_txn = %txn, thumbnail_size = ?(height, width), "attachment has a thumbnail"); + + // Cache thumbnail in the cache store. + let thumbnail_media_request = + make_local_thumbnail_media_request(&txn, height, width); + cache_store + .add_media_content(&thumbnail_media_request, thumbnail.data.clone()) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + + // Create the information required for filling the thumbnail section of the + // media event. + let thumbnail_info = Box::new( + assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { + mimetype: Some(thumbnail.content_type.as_ref().to_owned()) + }), + ); + + ( + Some(txn.clone()), + Some((thumbnail_media_request.source.clone(), thumbnail_info)), + Some(( + FinishUploadThumbnailInfo { txn, width, height }, + thumbnail_media_request, + thumbnail.content_type, + )), + ) + } else { + Default::default() + } }; // Create the content for the media event. @@ -296,8 +302,13 @@ impl QueueStorage { trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store"); - client + let cache_store = client .event_cache_store() + .lock() + .await + .map_err(RoomSendQueueStorageError::LockError)?; + + cache_store .replace_media_key( &from_req, &MediaRequestParameters { @@ -320,8 +331,7 @@ impl QueueStorage { // Reuse the same format for the cached thumbnail with the final MXC ID. let new_format = from_req.format.clone(); - client - .event_cache_store() + cache_store .replace_media_key( &from_req, &MediaRequestParameters { source: new_source, format: new_format }, From f256fe4b24d9d180b6dcf3dc7ead4b06d2ef87ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 7 Nov 2024 10:54:11 +0100 Subject: [PATCH 461/979] chore: Remove Ruma from the cargo-deny git dep allow list --- .deny.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.deny.toml b/.deny.toml index 88b53b451a2..d179083df96 100644 --- a/.deny.toml +++ b/.deny.toml @@ -54,8 +54,6 @@ allow-git = [ "https://github.com/element-hq/tracing.git", # Sam as for the tracing dependency. "https://github.com/element-hq/paranoid-android.git", - # Well, it's Ruma. - "https://github.com/ruma/ruma", # A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10 "https://github.com/jplatte/const_panic", # A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22 From 90b8015d7170439a32f3d8c779f5c0e035ce4327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 7 Nov 2024 10:54:35 +0100 Subject: [PATCH 462/979] chore: Don't ignore the aquamarine RUST-SEC issue, we bumped aquamarine --- .deny.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/.deny.toml b/.deny.toml index d179083df96..7c9a53f6320 100644 --- a/.deny.toml +++ b/.deny.toml @@ -10,7 +10,6 @@ exclude = [ version = 2 ignore = [ { id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" }, - { id = "RUSTSEC-2024-0370", reason = "Waiting for a Aquamarine release" }, ] [licenses] From 65287178d180db0fe19176b4d84ee845c961fb4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 7 Nov 2024 10:55:04 +0100 Subject: [PATCH 463/979] chore: Bump futures-util in the lock file We were locked onto a yanked version of futures-util. --- Cargo.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98c2e7b3d74..be3ee36dc0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1817,9 +1817,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1827,9 +1827,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -1844,15 +1844,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1861,21 +1861,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", From 00c4071fe1f6d00630931b0219382cb8b751029e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 5 Nov 2024 18:52:51 +0100 Subject: [PATCH 464/979] feat(ffi): allow `VerificationStateListener` to emit the current state With this, we get notified of the current verification state almost immediately. Without it, you may either call it too soon and receive an `Unknown` state or you might have to call `Encryption::wait_for_e2ee_initialization_tasks()` and wait until it's finished to request a valid state value. --- bindings/matrix-sdk-ffi/src/encryption.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index e6ab1253b41..785e87112b5 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -397,8 +397,15 @@ impl Encryption { pub fn verification_state_listener( self: Arc, listener: Box, + emit_current_value: bool, ) -> Arc { let mut subscriber = self.inner.verification_state(); + + if emit_current_value { + // Emit current value first + listener.on_update(subscriber.get().into()); + } + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { while let Some(verification_state) = subscriber.next().await { listener.on_update(verification_state.into()); From d54f2a8b045888299478c528c14aa4931796739d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 5 Nov 2024 18:53:50 +0100 Subject: [PATCH 465/979] fix(encryption): emit an updated current verification state before any network request happens This way we don't get stuck with an outdated value if there is no network connection. --- crates/matrix-sdk/src/encryption/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index a6ecc1f7754..74f4f2abcc7 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -1604,6 +1604,10 @@ impl Encryption { let this = self.clone(); tasks.setup_e2ee = Some(spawn(async move { + // Update the current state first, so we don't have to wait for the result of + // network requests + this.update_verification_state().await; + if this.settings().auto_enable_cross_signing { if let Err(e) = this.bootstrap_cross_signing_if_needed(auth_data).await { error!("Couldn't bootstrap cross signing {e:?}"); @@ -1616,8 +1620,6 @@ impl Encryption { if let Err(e) = this.recovery().setup().await { error!("Couldn't setup and resume recovery {e:?}"); } - - this.update_verification_state().await; })); } From 0f9bc20bb810313cb80730a304bbe62217a91cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 7 Nov 2024 10:15:16 +0100 Subject: [PATCH 466/979] fix(ffi): use `subscribe_reset` for `verification_state` instead, add a regression test --- bindings/matrix-sdk-ffi/src/encryption.rs | 6 --- crates/matrix-sdk/src/encryption/mod.rs | 53 +++++++++++++++++++++-- crates/matrix-sdk/src/test_utils.rs | 10 +++-- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 785e87112b5..ab54911b6c4 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -397,15 +397,9 @@ impl Encryption { pub fn verification_state_listener( self: Arc, listener: Box, - emit_current_value: bool, ) -> Arc { let mut subscriber = self.inner.verification_state(); - if emit_current_value { - // Emit current value first - listener.on_update(subscriber.get().into()); - } - Arc::new(TaskHandle::new(RUNTIME.spawn(async move { while let Some(verification_state) = subscriber.next().await { listener.on_update(verification_state.into()); diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 74f4f2abcc7..0ac2d38dc53 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -774,7 +774,7 @@ impl Encryption { /// # anyhow::Ok(()) }; /// ``` pub fn verification_state(&self) -> Subscriber { - self.client.inner.verification_state.subscribe() + self.client.inner.verification_state.subscribe_reset() } /// Get a verification object with the given flow id. @@ -1698,7 +1698,14 @@ impl Encryption { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use std::time::Duration; + use std::{ + ops::Not, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, + }; use matrix_sdk_base::SessionMeta; use matrix_sdk_test::{ @@ -1713,13 +1720,15 @@ mod tests { use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, - Mock, MockServer, ResponseTemplate, + Mock, MockServer, Request, ResponseTemplate, }; use crate::{ + assert_next_matches_with_timeout, config::RequestConfig, + encryption::VerificationState, matrix_auth::{MatrixSession, MatrixSessionTokens}, - test_utils::logged_in_client, + test_utils::{logged_in_client, no_retry_test_client, set_client_session}, Client, }; @@ -2019,4 +2028,40 @@ mod tests { let after_taking_lock_second_time = client.olm_machine().await.as_ref().unwrap().clone(); assert!(after_taking_lock_first_time.same_as(&after_taking_lock_second_time)); } + + #[async_test] + async fn test_update_verification_state_is_updated_before_any_requests_happen() { + // Given a client and a server + let client = no_retry_test_client(None).await; + let server = MockServer::start().await; + + // When we subscribe to its verification state + let mut verification_state = client.encryption().verification_state(); + + // We can get its initial value, and it's Unknown + assert_next_matches_with_timeout!(verification_state, VerificationState::Unknown); + + // We set up a mocked request to check this endpoint is not called before + // reading the new state + let keys_requested = Arc::new(AtomicBool::new(false)); + let inner_bool = keys_requested.clone(); + + Mock::given(method("GET")) + .and(path_regex( + r"/_matrix/client/r0/user/.*/account_data/m.secret_storage.default_key", + )) + .respond_with(move |_req: &Request| { + inner_bool.fetch_or(true, Ordering::SeqCst); + ResponseTemplate::new(200).set_body_json(json!({})) + }) + .mount(&server) + .await; + + // When the session is initialised and the encryption tasks spawn + set_client_session(&client).await; + + // Then we can get an updated value without waiting for any network requests + assert!(keys_requested.load(Ordering::SeqCst).not()); + assert_next_matches_with_timeout!(verification_state, VerificationState::Unverified); + } } diff --git a/crates/matrix-sdk/src/test_utils.rs b/crates/matrix-sdk/src/test_utils.rs index f016503e85a..f4aee4b6e14 100644 --- a/crates/matrix-sdk/src/test_utils.rs +++ b/crates/matrix-sdk/src/test_utils.rs @@ -100,11 +100,13 @@ pub async fn logged_in_client_with_server() -> (Client, wiremock::MockServer) { (client, server) } -/// Asserts the next item in a [`Stream`] can be loaded in the given timeout in -/// the given timeout in milliseconds. +/// Asserts the next item in a `Stream` or `Subscriber` can be loaded in the +/// given timeout in the given timeout in milliseconds. #[macro_export] macro_rules! assert_next_with_timeout { ($stream:expr, $timeout_ms:expr) => {{ + // Needed for subscribers, as they won't use the StreamExt features + #[allow(unused_imports)] use futures_util::StreamExt as _; tokio::time::timeout(std::time::Duration::from_millis($timeout_ms), $stream.next()) .await @@ -113,8 +115,8 @@ macro_rules! assert_next_with_timeout { }}; } -/// Assert the next item in a [`Stream`] matches the provided pattern in the -/// given timeout in milliseconds. +/// Assert the next item in a `Stream` or `Subscriber` matches the provided +/// pattern in the given timeout in milliseconds. /// /// If no timeout is provided, a default `100ms` value will be used. #[macro_export] From 5193c2033f66935d56422c4dea63c37472494f69 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 6 Nov 2024 19:03:11 +0100 Subject: [PATCH 467/979] feat(ffi): Auto approve the required widget capabilities for element call raise hand and reaction feature. --- bindings/matrix-sdk-ffi/src/widget.rs | 55 +++++++++++++++++---------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/widget.rs b/bindings/matrix-sdk-ffi/src/widget.rs index 956670f4bb0..44aa4dbffae 100644 --- a/bindings/matrix-sdk-ffi/src/widget.rs +++ b/bindings/matrix-sdk-ffi/src/widget.rs @@ -5,6 +5,7 @@ use matrix_sdk::{ async_trait, widget::{MessageLikeEventFilter, StateEventFilter}, }; +use ruma::events::MessageLikeEventType; use tracing::error; use crate::{room::Room, RUNTIME}; @@ -268,6 +269,31 @@ pub fn get_element_call_required_permissions( ) -> WidgetCapabilities { use ruma::events::StateEventType; + let read_send = vec![ + // To read and send rageshake requests from other room members + WidgetEventFilter::MessageLikeWithType { + event_type: "org.matrix.rageshake_request".to_owned(), + }, + // To read and send encryption keys + // TODO change this to the appropriate to-device version once ready + WidgetEventFilter::MessageLikeWithType { + event_type: "io.element.call.encryption_keys".to_owned(), + }, + // To read and send custom EC reactions. They are different to normal `m.reaction` + // because they can be send multiple times to the same event. + WidgetEventFilter::MessageLikeWithType { + event_type: "io.element.call.reaction".to_owned(), + }, + // This allows send raise hand reactions. + WidgetEventFilter::MessageLikeWithType { + event_type: MessageLikeEventType::Reaction.to_string(), + }, + // This allows to detect if someone does not raise their hand anymore. + WidgetEventFilter::MessageLikeWithType { + event_type: MessageLikeEventType::RoomRedaction.to_string(), + }, + ]; + WidgetCapabilities { read: vec![ // To compute the current state of the matrixRTC session. @@ -278,19 +304,13 @@ pub fn get_element_call_required_permissions( WidgetEventFilter::StateWithType { event_type: StateEventType::RoomEncryption.to_string(), }, - // To read rageshake requests from other room members - WidgetEventFilter::MessageLikeWithType { - event_type: "org.matrix.rageshake_request".to_owned(), - }, - // To read encryption keys - // TODO change this to the appropriate to-device version once ready - WidgetEventFilter::MessageLikeWithType { - event_type: "io.element.call.encryption_keys".to_owned(), - }, // This allows the widget to check the room version, so it can know about // version-specific auth rules (namely MSC3779). WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() }, - ], + ] + .into_iter() + .chain(read_send.clone()) + .collect(), send: vec![ // To send the call participation state event (main MatrixRTC event). // This is required for legacy state events (using only one event for all devices with @@ -313,15 +333,10 @@ pub fn get_element_call_required_permissions( event_type: StateEventType::CallMember.to_string(), state_key: format!("_{own_user_id}_{own_device_id}"), }, - // To request other room members to send rageshakes - WidgetEventFilter::MessageLikeWithType { - event_type: "org.matrix.rageshake_request".to_owned(), - }, - // To send this user's encryption keys - WidgetEventFilter::MessageLikeWithType { - event_type: "io.element.call.encryption_keys".to_owned(), - }, - ], + ] + .into_iter() + .chain(read_send) + .collect(), requires_client: true, update_delayed_event: true, send_delayed_event: true, @@ -417,7 +432,7 @@ impl From for WidgetCapabilities { } /// Different kinds of filters that could be applied to the timeline events. -#[derive(uniffi::Enum)] +#[derive(uniffi::Enum, Clone)] pub enum WidgetEventFilter { /// Matches message-like events with the given `type`. MessageLikeWithType { event_type: String }, From bab676138830b4cef3bd3b2ae189f1d9922d05f2 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 7 Nov 2024 12:21:28 +0100 Subject: [PATCH 468/979] fix(event cache): give looser parameters for the deduplicator's bloom filters The previous values would lead to super large memory allocations, as observed with `valgrind --tool=massive` on the tiny test added in this commit: - for 400 rooms each having 100 events, this led to 540MB of allocations. - for 1000 rooms each having 100 events, this led to 1.5GB of allocations. This is not acceptable for any kind of devices, especially for mobile devices which may be more constrained on memory. The bloom filter is an optimisation to avoid going through events in the room's event list, so it shouldn't cause a big toll like that; instead, we can reduce the parameters values given when creating the filters. With the given parameters, 1000 rooms each having 100 events leads to 1.2MB of allocations. --- .../src/event_cache/deduplicator.rs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs index b87dbd01d42..57d4126afef 100644 --- a/crates/matrix-sdk/src/event_cache/deduplicator.rs +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -41,8 +41,10 @@ impl fmt::Debug for Deduplicator { } impl Deduplicator { - const APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS: usize = 800_000; - const DESIRED_FALSE_POSITIVE_RATE: f64 = 0.001; + // Note: don't use too high numbers here, or the amount of allocated memory will + // explode. See https://github.com/matrix-org/matrix-rust-sdk/pull/4231 for details. + const APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS: usize = 1_000; + const DESIRED_FALSE_POSITIVE_RATE: f64 = 0.01; /// Create a new `Deduplicator`. pub fn new() -> Self { @@ -138,7 +140,7 @@ pub enum Decoration { #[cfg(test)] mod tests { - use assert_matches2::assert_let; + use assert_matches2::{assert_let, assert_matches}; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; use ruma::{owned_event_id, user_id, EventId}; @@ -267,4 +269,40 @@ mod tests { assert!(events.next().is_none()); } } + + #[test] + fn test_bloom_filter_growth() { + // This test was used as a testbed to observe, using `valgrind --tool=massive`, + // the total memory allocated by the deduplicator. We keep it checked in + // to revive this experiment in the future, if needs be. + + let num_rooms = if let Ok(num_rooms) = std::env::var("ROOMS") { + num_rooms.parse().unwrap() + } else { + 10 + }; + + let num_events = if let Ok(num_events) = std::env::var("EVENTS") { + num_events.parse().unwrap() + } else { + 100 + }; + + let mut dedups = Vec::with_capacity(num_rooms); + + for _ in 0..num_rooms { + let dedup = Deduplicator::new(); + let existing_events = RoomEvents::new(); + + for i in 0..num_events { + let event = sync_timeline_event(&EventId::parse(format!("$event{i}")).unwrap()); + let mut it = dedup.scan_and_learn([event].into_iter(), &existing_events); + + assert_matches!(it.next(), Some(Decoration::Unique(..))); + assert_matches!(it.next(), None); + } + + dedups.push(dedup); + } + } } From 16583971393059fc751f955014817ff14bee81a5 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 15:28:43 +0100 Subject: [PATCH 469/979] refactor!(media): rename `MediaThumbnailsSetting::new` to `with_method()` --- bindings/matrix-sdk-ffi/src/client.rs | 2 +- .../src/event_cache_store/integration_tests.rs | 2 +- crates/matrix-sdk-base/src/media.rs | 2 +- crates/matrix-sdk-sqlite/src/event_cache_store.rs | 2 +- crates/matrix-sdk/tests/integration/media.rs | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 0b5208c510f..2ab87909686 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -741,7 +741,7 @@ impl Client { .get_media_content( &MediaRequestParameters { source, - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( + format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method( Method::Scale, UInt::new(width).unwrap(), UInt::new(height).unwrap(), diff --git a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs index 788aa4a0cae..6b774a081a9 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs @@ -47,7 +47,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { }; let request_thumbnail = MediaRequestParameters { source: MediaSource::Plain(uri.to_owned()), - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( + format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method( Method::Crop, uint!(100), uint!(100), diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index b9ea760f644..4a72f1bee1e 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -70,7 +70,7 @@ pub struct MediaThumbnailSettings { impl MediaThumbnailSettings { /// Constructs a new `MediaThumbnailSettings` with the given method, width /// and height. - pub fn new(method: Method, width: UInt, height: UInt) -> Self { + pub fn with_method(method: Method, width: UInt, height: UInt) -> Self { Self { method, width, height, animated: false } } } diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 77387de9b6d..87a81657901 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -328,7 +328,7 @@ mod tests { }; let thumbnail_request = MediaRequestParameters { source: MediaSource::Plain(uri.to_owned()), - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( + format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method( Method::Crop, uint!(100), uint!(100), diff --git a/crates/matrix-sdk/tests/integration/media.rs b/crates/matrix-sdk/tests/integration/media.rs index dc108b56a39..ec7032919d1 100644 --- a/crates/matrix-sdk/tests/integration/media.rs +++ b/crates/matrix-sdk/tests/integration/media.rs @@ -161,7 +161,7 @@ async fn test_get_media_file_no_auth() { .media() .get_thumbnail( &event_content, - MediaThumbnailSettings::new(Method::Scale, uint!(100), uint!(100)), + MediaThumbnailSettings::with_method(Method::Scale, uint!(100), uint!(100)), true, ) .await @@ -272,7 +272,7 @@ async fn test_get_media_file_with_auth_matrix_1_11() { .media() .get_thumbnail( &event_content, - MediaThumbnailSettings::new(Method::Scale, uint!(100), uint!(100)), + MediaThumbnailSettings::with_method(Method::Scale, uint!(100), uint!(100)), true, ) .await @@ -387,7 +387,7 @@ async fn test_get_media_file_with_auth_matrix_stable_feature() { .media() .get_thumbnail( &event_content, - MediaThumbnailSettings::new(Method::Scale, uint!(100), uint!(100)), + MediaThumbnailSettings::with_method(Method::Scale, uint!(100), uint!(100)), true, ) .await From 237419c740d3a774dff968a29100b3b3e58f1a86 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 15:35:41 +0100 Subject: [PATCH 470/979] feat(media): introduce a stripped down `MediaThumbnailSettings::new` only taking a width and height This one is used when caching a thumbnail everywhere, and when attempting to retrieve it; it gives us a single place where to coordinate the default `MediaThumbnailSettings` parameters. --- bindings/matrix-sdk-ffi/src/client.rs | 4 +-- crates/matrix-sdk-base/src/media.rs | 10 ++++++++ crates/matrix-sdk/src/room/mod.rs | 9 +------ crates/matrix-sdk/src/send_queue/upload.rs | 16 +++++------- .../tests/integration/room/attachment/mod.rs | 14 ++--------- .../tests/integration/send_queue.rs | 25 ++++++------------- 6 files changed, 28 insertions(+), 50 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 2ab87909686..f58c38ab246 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -26,7 +26,6 @@ use matrix_sdk::{ reqwest::StatusCode, ruma::{ api::client::{ - media::get_content_thumbnail::v3::Method, push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind}, room::{create_room, Visibility}, session::get_login_types, @@ -741,8 +740,7 @@ impl Client { .get_media_content( &MediaRequestParameters { source, - format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method( - Method::Scale, + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( UInt::new(width).unwrap(), UInt::new(height).unwrap(), )), diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index 4a72f1bee1e..8a18e8258d2 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -70,9 +70,19 @@ pub struct MediaThumbnailSettings { impl MediaThumbnailSettings { /// Constructs a new `MediaThumbnailSettings` with the given method, width /// and height. + /// + /// Requests a non-animated thumbnail by default. pub fn with_method(method: Method, width: UInt, height: UInt) -> Self { Self { method, width, height, animated: false } } + + /// Constructs a new `MediaThumbnailSettings` with the given width and + /// height. + /// + /// Requests scaling, and a non-animated thumbnail. + pub fn new(width: UInt, height: UInt) -> Self { + Self { method: Method::Scale, width, height, animated: false } + } } impl UniqueKey for MediaThumbnailSettings { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 2aba1c9b852..87f1ca8866a 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2006,16 +2006,9 @@ impl Room { { debug!("caching the thumbnail"); - // Do a best guess at figuring the media request: not animated, cropped - // thumbnail of the original size. let request = MediaRequestParameters { source: source.clone(), - format: MediaFormat::Thumbnail(MediaThumbnailSettings { - method: ruma::media::Method::Scale, - width, - height, - animated: false, - }), + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(width, height)), }; if let Err(err) = cache_store_lock_guard.add_media_content(&request, data).await { diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 2c38977387c..4b6fd240373 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -29,7 +29,6 @@ use ruma::{ message::{MessageType, RoomMessageEventContent}, MediaSource, ThumbnailInfo, }, - media::Method, uint, OwnedMxcUri, OwnedTransactionId, TransactionId, UInt, }; use tracing::{debug, error, instrument, trace, warn, Span}; @@ -73,15 +72,12 @@ fn make_local_thumbnail_media_request( width: UInt, ) -> MediaRequestParameters { // See comment in [`make_local_file_media_request`]. - let source = - MediaSource::Plain(OwnedMxcUri::from(format!("mxc://send-queue.localhost/{}", txn_id))); - let format = MediaFormat::Thumbnail(MediaThumbnailSettings { - method: Method::Scale, - width, - height, - animated: false, - }); - MediaRequestParameters { source, format } + MediaRequestParameters { + source: MediaSource::Plain(OwnedMxcUri::from(format!( + "mxc://send-queue.localhost/{txn_id}" + ))), + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(width, height)), + } } /// Replace the source by the final ones in all the media types handled by diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 012a0832168..3e500bd1a1b 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -248,12 +248,7 @@ async fn test_room_attachment_send_info_thumbnail() { MediaRequestParameters { source: MediaSource::Plain(media_mxc), format: MediaFormat::File }; let thumbnail_request = MediaRequestParameters { source: MediaSource::Plain(thumbnail_mxc.clone()), - format: MediaFormat::Thumbnail(MediaThumbnailSettings { - method: ruma::media::Method::Scale, - width: uint!(480), - height: uint!(360), - animated: false, - }), + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(480), uint!(360))), }; let _ = client.media().get_media_content(&media_request, true).await.unwrap_err(); let _ = client.media().get_media_content(&thumbnail_request, true).await.unwrap_err(); @@ -309,12 +304,7 @@ async fn test_room_attachment_send_info_thumbnail() { // But it is not found when requesting it as a thumbnail with a different size. let thumbnail_request = MediaRequestParameters { source: MediaSource::Plain(thumbnail_mxc), - format: MediaFormat::Thumbnail(MediaThumbnailSettings { - method: ruma::media::Method::Scale, - width: uint!(42), - height: uint!(1337), - animated: false, - }), + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(42), uint!(1337))), }; let _ = client.media().get_media_content(&thumbnail_request, true).await.unwrap_err(); } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 7ec87d6023e..ecdb57f451f 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -40,7 +40,6 @@ use ruma::{ }, AnyMessageLikeEventContent, EventContent as _, Mentions, }, - media::Method, mxc_uri, owned_user_id, room_id, serde::Raw, uint, EventId, MxcUri, OwnedEventId, TransactionId, @@ -2150,14 +2149,10 @@ async fn test_media_uploads() { .get_media_content( &MediaRequestParameters { source: local_thumbnail_source, - // TODO: extract this reasonable query into a helper function shared across the - // codebase - format: MediaFormat::Thumbnail(MediaThumbnailSettings { - height: tinfo.height.unwrap(), - width: tinfo.width.unwrap(), - method: Method::Scale, - animated: false, - }), + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( + tinfo.width.unwrap(), + tinfo.height.unwrap(), + )), }, true, ) @@ -2222,14 +2217,10 @@ async fn test_media_uploads() { .get_media_content( &MediaRequestParameters { source: new_thumbnail_source, - // TODO: extract this reasonable query into a helper function shared across the - // codebase - format: MediaFormat::Thumbnail(MediaThumbnailSettings { - height: tinfo.height.unwrap(), - width: tinfo.width.unwrap(), - method: Method::Scale, - animated: false, - }), + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( + tinfo.width.unwrap(), + tinfo.height.unwrap(), + )), }, true, ) From 219be9b73126eabc2fa1b88e96681ef57633785c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 29 Oct 2024 14:43:37 +0100 Subject: [PATCH 471/979] refactor(base)!: Rename DisplayName to RoomDisplayName --- bindings/matrix-sdk-ffi/src/room_alias.rs | 4 +- crates/matrix-sdk-base/src/client.rs | 4 +- crates/matrix-sdk-base/src/lib.rs | 2 +- crates/matrix-sdk-base/src/rooms/mod.rs | 36 ++++++--- crates/matrix-sdk-base/src/rooms/normal.rs | 81 ++++++++++--------- crates/matrix-sdk/src/lib.rs | 7 +- .../tests/integration/room/common.rs | 14 ++-- 7 files changed, 83 insertions(+), 65 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_alias.rs b/bindings/matrix-sdk-ffi/src/room_alias.rs index 125cad31512..491820c5799 100644 --- a/bindings/matrix-sdk-ffi/src/room_alias.rs +++ b/bindings/matrix-sdk-ffi/src/room_alias.rs @@ -1,4 +1,4 @@ -use matrix_sdk::DisplayName; +use matrix_sdk::RoomDisplayName; use ruma::RoomAliasId; /// Verifies the passed `String` matches the expected room alias format. @@ -10,5 +10,5 @@ fn is_room_alias_format_valid(alias: String) -> bool { /// Transforms a Room's display name into a valid room alias name. #[matrix_sdk_ffi_macros::export] fn room_alias_name_from_room_display_name(room_name: String) -> String { - DisplayName::Named(room_name).to_room_alias_name() + RoomDisplayName::Named(room_name).to_room_alias_name() } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 20801ee022f..b8aa6f67b5a 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1737,7 +1737,7 @@ mod tests { use super::BaseClient; use crate::{ - store::StateStoreExt, test_utils::logged_in_base_client, DisplayName, RoomState, + store::StateStoreExt, test_utils::logged_in_base_client, RoomDisplayName, RoomState, SessionMeta, }; @@ -1869,7 +1869,7 @@ mod tests { assert_eq!(room.state(), RoomState::Invited); assert_eq!( room.compute_display_name().await.expect("fetching display name failed"), - DisplayName::Calculated("Kyra".to_owned()) + RoomDisplayName::Calculated("Kyra".to_owned()) ); } diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 611305a59e6..f884448c7a0 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -56,7 +56,7 @@ pub use http; pub use matrix_sdk_crypto as crypto; pub use once_cell; pub use rooms::{ - DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, + Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState, RoomStateFilter, }; diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index 9aa9b102750..b0aafcb87ce 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -50,7 +50,7 @@ use crate::MinimalStateEvent; /// The name of the room, either from the metadata or calculated /// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room) #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum DisplayName { +pub enum RoomDisplayName { /// The room has been named explicitly as Named(String), /// The room has a canonical alias that should be used @@ -68,7 +68,7 @@ pub enum DisplayName { const WHITESPACE_REGEX: &str = r"\s+"; const INVALID_SYMBOLS_REGEX: &str = r"[#,:]+"; -impl DisplayName { +impl RoomDisplayName { /// Transforms the current display name into the name part of a /// `RoomAliasId`. pub fn to_room_alias_name(&self) -> String { @@ -97,14 +97,16 @@ impl DisplayName { } } -impl fmt::Display for DisplayName { +impl fmt::Display for RoomDisplayName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => { + RoomDisplayName::Named(s) + | RoomDisplayName::Calculated(s) + | RoomDisplayName::Aliased(s) => { write!(f, "{s}") } - DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"), - DisplayName::Empty => write!(f, "Empty Room"), + RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"), + RoomDisplayName::Empty => write!(f, "Empty Room"), } } } @@ -574,7 +576,7 @@ mod tests { use ruma::events::tag::{TagInfo, TagName, Tags}; use super::{BaseRoomInfo, RoomNotableTags}; - use crate::DisplayName; + use crate::RoomDisplayName; #[test] fn test_handle_notable_tags_favourite() { @@ -608,21 +610,33 @@ mod tests { #[test] fn test_room_alias_from_room_display_name_lowercases() { - assert_eq!("roomalias", DisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()); + assert_eq!( + "roomalias", + RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name() + ); } #[test] fn test_room_alias_from_room_display_name_removes_whitespace() { - assert_eq!("room-alias", DisplayName::Named("Room Alias".to_owned()).to_room_alias_name()); + assert_eq!( + "room-alias", + RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name() + ); } #[test] fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() { - assert_eq!("roomalias", DisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()); + assert_eq!( + "roomalias", + RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name() + ); } #[test] fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() { - assert_eq!("roomalias", DisplayName::Named("#Room,Alias:".to_owned()).to_room_alias_name()); + assert_eq!( + "roomalias", + RoomDisplayName::Named("#Room,Alias:".to_owned()).to_room_alias_name() + ); } } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 7181d4d4824..49fad5cfcca 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -61,7 +61,7 @@ use tokio::sync::broadcast; use tracing::{debug, field::debug, info, instrument, warn}; use super::{ - members::MemberRoomInfo, BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent, + members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomMember, RoomNotableTags, }; #[cfg(feature = "experimental-sliding-sync")] @@ -572,8 +572,8 @@ impl Room { /// [`Self::cached_display_name`]. /// /// [spec]: - pub async fn compute_display_name(&self) -> StoreResult { - let update_cache = |new_val: DisplayName| { + pub async fn compute_display_name(&self) -> StoreResult { + let update_cache = |new_val: RoomDisplayName| { self.inner.update_if(|info| { if info.cached_display_name.as_ref() != Some(&new_val) { info.cached_display_name = Some(new_val.clone()); @@ -591,13 +591,13 @@ impl Room { if let Some(name) = inner.name() { let name = name.trim().to_owned(); drop(inner); // drop the lock on `self.inner` to avoid deadlocking in `update_cache`. - return Ok(update_cache(DisplayName::Named(name))); + return Ok(update_cache(RoomDisplayName::Named(name))); } if let Some(alias) = inner.canonical_alias() { let alias = alias.alias().trim().to_owned(); drop(inner); // See above comment. - return Ok(update_cache(DisplayName::Aliased(alias))); + return Ok(update_cache(RoomDisplayName::Aliased(alias))); } inner.summary.clone() @@ -703,7 +703,7 @@ impl Room { /// /// This cache is refilled every time we call /// [`Self::compute_display_name`]. - pub fn cached_display_name(&self) -> Option { + pub fn cached_display_name(&self) -> Option { self.inner.read().cached_display_name.clone() } @@ -1103,7 +1103,7 @@ pub struct RoomInfo { /// Filled by calling [`Room::compute_display_name`]. It's automatically /// filled at start when creating a room, or on every successful sync. #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) cached_display_name: Option, + pub(crate) cached_display_name: Option, /// Cached user defined notification mode. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1767,7 +1767,10 @@ impl RoomStateFilter { /// Calculate room name according to step 3 of the [naming algorithm]. /// /// [naming algorithm]: https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room -fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&str>) -> DisplayName { +fn compute_display_name_from_heroes( + num_joined_invited: u64, + mut heroes: Vec<&str>, +) -> RoomDisplayName { let num_heroes = heroes.len() as u64; let num_joined_invited_except_self = num_joined_invited.saturating_sub(1); @@ -1789,12 +1792,12 @@ fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&st // User is alone. if num_joined_invited <= 1 { if names.is_empty() { - DisplayName::Empty + RoomDisplayName::Empty } else { - DisplayName::EmptyWas(names) + RoomDisplayName::EmptyWas(names) } } else { - DisplayName::Calculated(names) + RoomDisplayName::Calculated(names) } } @@ -1851,7 +1854,7 @@ mod tests { use crate::{ rooms::RoomNotableTags, store::{IntoStateStore, MemoryStore, StateChanges, StateStore}, - BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, + BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName, RoomInfoNotableUpdateReasons, SessionMeta, }; @@ -2126,7 +2129,7 @@ mod tests { assert_eq!( info.cached_display_name.as_ref(), - Some(&DisplayName::Calculated("lol".to_owned())), + Some(&RoomDisplayName::Calculated("lol".to_owned())), ); assert_eq!( info.cached_user_defined_notification_mode.as_ref(), @@ -2331,7 +2334,7 @@ mod tests { #[async_test] async fn test_display_name_for_joined_room_is_empty_if_no_info() { let (_, room) = make_room_test_helper(RoomState::Joined); - assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty); + assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty); } #[async_test] @@ -2341,7 +2344,7 @@ mod tests { .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event())); assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Aliased("test".to_owned()) + RoomDisplayName::Aliased("test".to_owned()) ); } @@ -2352,20 +2355,20 @@ mod tests { .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event())); assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Aliased("test".to_owned()) + RoomDisplayName::Aliased("test".to_owned()) ); room.inner.update(|info| info.base_info.name = Some(make_name_event())); // Display name wasn't cached when we asked for it above, and name overrides assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Named("Test Room".to_owned()) + RoomDisplayName::Named("Test Room".to_owned()) ); } #[async_test] async fn test_display_name_for_invited_room_is_empty_if_no_info() { let (_, room) = make_room_test_helper(RoomState::Invited); - assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty); + assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty); } #[async_test] @@ -2378,7 +2381,7 @@ mod tests { }); room.inner.update(|info| info.base_info.name = Some(room_name)); - assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty); + assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty); } #[async_test] @@ -2388,7 +2391,7 @@ mod tests { .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event())); assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Aliased("test".to_owned()) + RoomDisplayName::Aliased("test".to_owned()) ); } @@ -2399,13 +2402,13 @@ mod tests { .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event())); assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Aliased("test".to_owned()) + RoomDisplayName::Aliased("test".to_owned()) ); room.inner.update(|info| info.base_info.name = Some(make_name_event())); // Display name wasn't cached when we asked for it above, and name overrides assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Named("Test Room".to_owned()) + RoomDisplayName::Named("Test Room".to_owned()) ); } @@ -2447,7 +2450,7 @@ mod tests { room.inner.update_if(|info| info.update_from_ruma_summary(&summary)); assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Calculated("Matthew".to_owned()) + RoomDisplayName::Calculated("Matthew".to_owned()) ); } @@ -2469,7 +2472,7 @@ mod tests { assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Calculated("Matthew".to_owned()) + RoomDisplayName::Calculated("Matthew".to_owned()) ); } @@ -2499,7 +2502,7 @@ mod tests { room.inner.update_if(|info| info.update_from_ruma_summary(&summary)); assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Calculated("Matthew".to_owned()) + RoomDisplayName::Calculated("Matthew".to_owned()) ); } @@ -2524,7 +2527,7 @@ mod tests { assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Calculated("Matthew".to_owned()) + RoomDisplayName::Calculated("Matthew".to_owned()) ); } @@ -2579,7 +2582,7 @@ mod tests { assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned()) + RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned()) ); } @@ -2628,7 +2631,7 @@ mod tests { assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned()) + RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned()) ); } @@ -2658,7 +2661,7 @@ mod tests { room.inner.update_if(|info| info.update_from_ruma_summary(&summary)); assert_eq!( room.compute_display_name().await.unwrap(), - DisplayName::EmptyWas("Matthew".to_owned()) + RoomDisplayName::EmptyWas("Matthew".to_owned()) ); } @@ -3054,34 +3057,34 @@ mod tests { #[test] fn test_calculate_room_name() { let mut actual = compute_display_name_from_heroes(2, vec!["a"]); - assert_eq!(DisplayName::Calculated("a".to_owned()), actual); + assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual); actual = compute_display_name_from_heroes(3, vec!["a", "b"]); - assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual); + assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual); actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]); - assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual); + assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual); actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]); - assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual); + assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual); actual = compute_display_name_from_heroes(5, vec![]); - assert_eq!(DisplayName::Calculated("5 people".to_owned()), actual); + assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual); actual = compute_display_name_from_heroes(0, vec![]); - assert_eq!(DisplayName::Empty, actual); + assert_eq!(RoomDisplayName::Empty, actual); actual = compute_display_name_from_heroes(1, vec![]); - assert_eq!(DisplayName::Empty, actual); + assert_eq!(RoomDisplayName::Empty, actual); actual = compute_display_name_from_heroes(1, vec!["a"]); - assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual); + assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual); actual = compute_display_name_from_heroes(1, vec!["a", "b"]); - assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual); + assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual); actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]); - assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual); + assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual); } #[test] diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 758f25d43be..370061fa8de 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -25,9 +25,10 @@ pub use matrix_sdk_base::crypto; pub use matrix_sdk_base::{ deserialized_responses, store::{DynStateStore, MemoryStore, StateStoreExt}, - ComposerDraft, ComposerDraftType, DisplayName, QueueWedgeError, Room as BaseRoom, - RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, RoomMember as BaseRoomMember, - RoomMemberships, RoomState, SessionMeta, StateChanges, StateStore, StoreError, + ComposerDraft, ComposerDraftType, QueueWedgeError, Room as BaseRoom, + RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, + RoomMember as BaseRoomMember, RoomMemberships, RoomState, SessionMeta, StateChanges, + StateStore, StoreError, }; pub use matrix_sdk_common::*; pub use reqwest; diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index f742ee3bfe4..33efb813eac 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -3,7 +3,7 @@ use std::{iter, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use js_int::uint; use matrix_sdk::{ - config::SyncSettings, room::RoomMember, test_utils::events::EventFactory, DisplayName, + config::SyncSettings, room::RoomMember, test_utils::events::EventFactory, RoomDisplayName, RoomMemberships, }; use matrix_sdk_test::{ @@ -66,7 +66,7 @@ async fn test_calculate_room_names_from_summary() { let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); assert_eq!( - DisplayName::Calculated("example2".to_owned()), + RoomDisplayName::Calculated("example2".to_owned()), room.compute_display_name().await.unwrap() ); } @@ -86,7 +86,7 @@ async fn test_room_names() { let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); assert_eq!( - DisplayName::Aliased("tutorial".to_owned()), + RoomDisplayName::Aliased("tutorial".to_owned()), room.compute_display_name().await.unwrap() ); @@ -100,7 +100,7 @@ async fn test_room_names() { let invited_room = client.get_room(room_id!("!696r7674:example.com")).unwrap(); assert_eq!( - DisplayName::Named("My Room Name".to_owned()), + RoomDisplayName::Named("My Room Name".to_owned()), invited_room.compute_display_name().await.unwrap() ); @@ -136,7 +136,7 @@ async fn test_room_names() { let room = client.get_room(room_id).unwrap(); assert_eq!( - DisplayName::Calculated( + RoomDisplayName::Calculated( "user_0, user_1, user_10, user_11, user_12, and 10 others".to_owned() ), room.compute_display_name().await.unwrap() @@ -186,7 +186,7 @@ async fn test_room_names() { let room = client.get_room(room_id).unwrap(); assert_eq!( - DisplayName::Calculated("Bob, example1".to_owned()), + RoomDisplayName::Calculated("Bob, example1".to_owned()), room.compute_display_name().await.unwrap() ); @@ -206,7 +206,7 @@ async fn test_room_names() { let room = client.get_room(room_id).unwrap(); assert_eq!( - DisplayName::EmptyWas("user_0, user_1, user_2".to_owned()), + RoomDisplayName::EmptyWas("user_0, user_1, user_2".to_owned()), room.compute_display_name().await.unwrap() ); } From 1304902cb40fe8b8b7bd0b5f8f07f18fc99c47fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 4 Nov 2024 15:03:02 +0100 Subject: [PATCH 472/979] refactor(base): Rename AmbiguityMap to DisplayNameUsers The ambiguity map tracks the users which are using a single display name, so let's reflect that in the name. --- .../src/store/ambiguity_map.rs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index 907ee7c3e4e..5eabfcd7d5f 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -39,13 +39,13 @@ pub(crate) struct AmbiguityCache { pub changes: BTreeMap>, } -#[derive(Debug)] -struct AmbiguityMap { +#[derive(Debug, Clone)] +struct DisplayNameUsers { display_name: String, users: BTreeSet, } -impl AmbiguityMap { +impl DisplayNameUsers { fn remove(&mut self, user_id: &UserId) -> Option { self.users.remove(user_id); @@ -105,6 +105,8 @@ impl AmbiguityCache { _ => false, }; + // If the user's display name didn't change, then there's nothing more to + // calculate here. if display_names_same { return Ok(()); } @@ -134,8 +136,8 @@ impl AmbiguityCache { fn update( &mut self, room_id: &RoomId, - old_map: Option, - new_map: Option, + old_map: Option, + new_map: Option, ) { let entry = self.cache.entry(room_id.to_owned()).or_default(); @@ -157,7 +159,7 @@ impl AmbiguityCache { changes: &StateChanges, room_id: &RoomId, member_event: &SyncRoomMemberEvent, - ) -> Result<(Option, Option)> { + ) -> Result<(Option, Option)> { use MembershipState::*; let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| { @@ -202,7 +204,10 @@ impl AmbiguityCache { self.store.get_users_with_display_name(room_id, old_name).await? }; - Some(AmbiguityMap { display_name: old_name.to_owned(), users: old_display_name_map }) + Some(DisplayNameUsers { + display_name: old_name.to_owned(), + users: old_display_name_map, + }) } else { None }; @@ -232,7 +237,7 @@ impl AmbiguityCache { self.store.get_users_with_display_name(room_id, new_display_name).await? }; - Some(AmbiguityMap { + Some(DisplayNameUsers { display_name: new_display_name.to_owned(), users: new_display_name_map, }) From 4039359512754fecc3e3e65b14b0283d905ce458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 4 Nov 2024 15:10:17 +0100 Subject: [PATCH 473/979] chore(base): Clean up the display name ambiguity calculation logic --- .../src/store/ambiguity_map.rs | 133 ++++++++++-------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index 5eabfcd7d5f..74ce0e00e69 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -128,7 +128,10 @@ impl AmbiguityCache { trace!(user_id = ?member_event.state_key(), "Handling display name ambiguity: {change:#?}"); - self.add_change(room_id, member_event.event_id().to_owned(), change); + self.changes + .entry(room_id.to_owned()) + .or_default() + .insert(member_event.event_id().to_owned(), change); Ok(()) } @@ -150,64 +153,82 @@ impl AmbiguityCache { } } - fn add_change(&mut self, room_id: &RoomId, event_id: OwnedEventId, change: AmbiguityChange) { - self.changes.entry(room_id.to_owned()).or_default().insert(event_id, change); - } - - async fn get( - &mut self, + async fn get_old_display_name( + &self, changes: &StateChanges, room_id: &RoomId, - member_event: &SyncRoomMemberEvent, - ) -> Result<(Option, Option)> { + new_event: &SyncRoomMemberEvent, + ) -> Result> { use MembershipState::*; - let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| { - events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str()) - }) { + let user_id = new_event.state_key(); + + let old_event = if let Some(m) = changes + .state + .get(room_id) + .and_then(|events| events.get(&StateEventType::RoomMember)?.get(user_id.as_str())) + { Some(RawMemberEvent::Sync(m.clone().cast())) } else { - self.store.get_member_event(room_id, member_event.state_key()).await? + self.store.get_member_event(room_id, user_id).await? }; - // FIXME: Use let chains once stable - let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) { - if matches!(event.membership(), Join | Invite) { - let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| { - p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref() - }) { - Some(d.to_owned()) - } else if let Some(d) = self - .store - .get_profile(room_id, member_event.state_key()) - .await? - .and_then(|p| p.into_original()?.content.displayname) - { - Some(d) - } else { - event.original_content().and_then(|c| c.displayname.clone()) - }; - - Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned())) + let Some(Ok(old_event)) = old_event.map(|r| r.deserialize()) else { return Ok(None) }; + + if matches!(old_event.membership(), Join | Invite) { + let display_name = if let Some(d) = changes + .profiles + .get(room_id) + .and_then(|p| p.get(user_id)?.as_original()?.content.displayname.as_deref()) + { + Some(d.to_owned()) + } else if let Some(d) = self + .store + .get_profile(room_id, user_id) + .await? + .and_then(|p| p.into_original()?.content.displayname) + { + Some(d) } else { - None - } + old_event.original_content().and_then(|c| c.displayname.clone()) + }; + + Ok(Some(display_name.unwrap_or_else(|| user_id.localpart().to_owned()))) } else { - None - }; + Ok(None) + } + } + + async fn get_users_with_display_name( + &mut self, + room_id: &RoomId, + display_name: &str, + ) -> Result { + Ok(if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(display_name) { + DisplayNameUsers { display_name: display_name.to_owned(), users: u.clone() } + } else { + let users_with_display_name = + self.store.get_users_with_display_name(room_id, display_name).await?; + + DisplayNameUsers { + display_name: display_name.to_owned(), + users: users_with_display_name, + } + }) + } + + async fn get( + &mut self, + changes: &StateChanges, + room_id: &RoomId, + member_event: &SyncRoomMemberEvent, + ) -> Result<(Option, Option)> { + use MembershipState::*; + + let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?; let old_map = if let Some(old_name) = old_display_name.as_deref() { - let old_display_name_map = - if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(old_name) { - u.clone() - } else { - self.store.get_users_with_display_name(room_id, old_name).await? - }; - - Some(DisplayNameUsers { - display_name: old_name.to_owned(), - users: old_display_name_map, - }) + Some(self.get_users_with_display_name(room_id, old_name).await?) } else { None }; @@ -218,9 +239,8 @@ impl AmbiguityCache { .and_then(|ev| ev.content.displayname.as_deref()) .unwrap_or_else(|| member_event.state_key().localpart()); - // We don't allow other users to set the display name, so if we - // have a more trusted version of the display - // name use that. + // We don't allow other users to set the display name, so if we have a more + // trusted version of the display name use that. let new_display_name = if member_event.sender().as_str() == member_event.state_key() { new } else if let Some(old) = old_display_name.as_deref() { @@ -229,18 +249,7 @@ impl AmbiguityCache { new }; - let new_display_name_map = if let Some(u) = - self.cache.entry(room_id.to_owned()).or_default().get(new_display_name) - { - u.clone() - } else { - self.store.get_users_with_display_name(room_id, new_display_name).await? - }; - - Some(DisplayNameUsers { - display_name: new_display_name.to_owned(), - users: new_display_name_map, - }) + Some(self.get_users_with_display_name(room_id, new_display_name).await?) } else { None }; From 4ca69da93c2d33fbe2914166815955d037f3e9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 5 Nov 2024 15:53:22 +0100 Subject: [PATCH 474/979] chore(base): Improve the docs for the DisplayNameUsers struct --- .../src/store/ambiguity_map.rs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index 74ce0e00e69..6dbdb4b8a27 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -39,6 +39,7 @@ pub(crate) struct AmbiguityCache { pub changes: BTreeMap>, } +/// A map of users that use a certain display name. #[derive(Debug, Clone)] struct DisplayNameUsers { display_name: String, @@ -46,6 +47,8 @@ struct DisplayNameUsers { } impl DisplayNameUsers { + /// Remove the given [`UserId`] from the map, marking that the [`UserId`] + /// doesn't use the display name anymore. fn remove(&mut self, user_id: &UserId) -> Option { self.users.remove(user_id); @@ -56,6 +59,8 @@ impl DisplayNameUsers { } } + /// Add the given [`UserId`] from the map, marking that the [`UserId`] + /// is using the display name. fn add(&mut self, user_id: OwnedUserId) -> Option { let ambiguous_user = if self.user_count() == 1 { self.users.iter().next().cloned() } else { None }; @@ -65,10 +70,12 @@ impl DisplayNameUsers { ambiguous_user } + /// How many users are using this display name. fn user_count(&self) -> usize { self.users.len() } + /// Is the display name considered to be ambiguous. fn is_ambiguous(&self) -> bool { self.user_count() > 1 } @@ -85,15 +92,14 @@ impl AmbiguityCache { room_id: &RoomId, member_event: &SyncRoomMemberEvent, ) -> Result<()> { - // Synapse seems to have a bug where it puts the same event into the - // state and the timeline sometimes. + // Synapse seems to have a bug where it puts the same event into the state and + // the timeline sometimes. // - // Since our state, e.g. the old display name, already ended up inside - // the state changes and we're pulling stuff out of the cache if it's - // there calculating this twice for the same event will result in an - // incorrect AmbiguityChange overwriting the correct one. In other - // words, this method is not idempotent so we make it by ignoring - // duplicate events. + // Since our state, e.g. the old display name, already ended up inside the state + // changes and we're pulling stuff out of the cache if it's there calculating + // this twice for the same event will result in an incorrect AmbiguityChange + // overwriting the correct one. In other words, this method is not idempotent so + // we make it by ignoring duplicate events. if self.changes.get(room_id).is_some_and(|c| c.contains_key(member_event.event_id())) { return Ok(()); } From df465a0420ad21e9234bebf1cd89c3aa9268dfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 7 Nov 2024 15:06:31 +0100 Subject: [PATCH 475/979] chore(base): Improve the docs for the AmbiguityCache --- .../src/store/ambiguity_map.rs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index 6dbdb4b8a27..b55955563f7 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -82,10 +82,12 @@ impl DisplayNameUsers { } impl AmbiguityCache { + /// Create a new [`AmbiguityCache`] backed by the given state store. pub fn new(store: Arc) -> Self { Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() } } + /// Handle a newly received [`SyncRoomMemberEvent`] for the given room. pub async fn handle_event( &mut self, changes: &StateChanges, @@ -104,7 +106,8 @@ impl AmbiguityCache { return Ok(()); } - let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?; + let (mut old_map, mut new_map) = + self.calculate_changes(changes, room_id, member_event).await?; let display_names_same = match (&old_map, &new_map) { (Some(a), Some(b)) => a.display_name == b.display_name, @@ -142,6 +145,8 @@ impl AmbiguityCache { Ok(()) } + /// Update the [`AmbiguityCache`] state for the given room with a pair of + /// [`DisplayNameUsers`] that got created by a new [`SyncRoomMemberEvent`]. fn update( &mut self, room_id: &RoomId, @@ -159,6 +164,8 @@ impl AmbiguityCache { } } + /// Get the previously used display name, if any, of the member described in + /// the given new [`SyncRoomMemberEvent`]. async fn get_old_display_name( &self, changes: &StateChanges, @@ -205,6 +212,12 @@ impl AmbiguityCache { } } + /// Get the [`DisplayNameUsers`] for the given display name in the given + /// room. + /// + /// This method will get the [`DisplayNameUsers`] from the cache, if the + /// cache doesn't contain such an entry, it falls back to the state + /// store. async fn get_users_with_display_name( &mut self, room_id: &RoomId, @@ -223,7 +236,13 @@ impl AmbiguityCache { }) } - async fn get( + /// Calculate the change in the users that use a display name a + /// [`SyncRoomMemberEvent`] will cause for a given room. + /// + /// Returns the [`DisplayNameUsers`] before the member event is applied and + /// the [`DisplayNameUsers`] after the member event is applied to the + /// room state. + async fn calculate_changes( &mut self, changes: &StateChanges, room_id: &RoomId, From 5d8380814374afeb34841d8fe9b495904ad33878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 7 Nov 2024 15:07:27 +0100 Subject: [PATCH 476/979] feat(base): Consider knocked members to be part of the room for display name disambiguation --- .../src/store/ambiguity_map.rs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index b55955563f7..f02de1cc645 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -32,13 +32,6 @@ use crate::{ store::StateStoreExt, }; -#[derive(Debug)] -pub(crate) struct AmbiguityCache { - pub store: Arc, - pub cache: BTreeMap>>, - pub changes: BTreeMap>, -} - /// A map of users that use a certain display name. #[derive(Debug, Clone)] struct DisplayNameUsers { @@ -81,6 +74,18 @@ impl DisplayNameUsers { } } +fn is_member_active(membership: &MembershipState) -> bool { + use MembershipState::*; + matches!(membership, Join | Invite | Knock) +} + +#[derive(Debug)] +pub(crate) struct AmbiguityCache { + pub store: Arc, + pub cache: BTreeMap>>, + pub changes: BTreeMap>, +} + impl AmbiguityCache { /// Create a new [`AmbiguityCache`] backed by the given state store. pub fn new(store: Arc) -> Self { @@ -172,8 +177,6 @@ impl AmbiguityCache { room_id: &RoomId, new_event: &SyncRoomMemberEvent, ) -> Result> { - use MembershipState::*; - let user_id = new_event.state_key(); let old_event = if let Some(m) = changes @@ -188,7 +191,7 @@ impl AmbiguityCache { let Some(Ok(old_event)) = old_event.map(|r| r.deserialize()) else { return Ok(None) }; - if matches!(old_event.membership(), Join | Invite) { + if is_member_active(old_event.membership()) { let display_name = if let Some(d) = changes .profiles .get(room_id) @@ -248,8 +251,6 @@ impl AmbiguityCache { room_id: &RoomId, member_event: &SyncRoomMemberEvent, ) -> Result<(Option, Option)> { - use MembershipState::*; - let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?; let old_map = if let Some(old_name) = old_display_name.as_deref() { @@ -258,7 +259,7 @@ impl AmbiguityCache { None }; - let new_map = if matches!(member_event.membership(), Join | Invite) { + let new_map = if is_member_active(member_event.membership()) { let new = member_event .as_original() .and_then(|ev| ev.content.displayname.as_deref()) From 57137cdd5b80e87baa870b8b533cf2a1bfa4ea9f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 31 Oct 2024 13:59:58 +0100 Subject: [PATCH 477/979] task(tests): introduce prebuilt mocks and mocking helpers --- .../src/deserialized_responses.rs | 8 +- crates/matrix-sdk/src/test_utils.rs | 3 + crates/matrix-sdk/src/test_utils/mocks.rs | 471 +++++++++++++ crates/matrix-sdk/tests/integration/main.rs | 30 +- .../tests/integration/room/joined.rs | 67 +- .../tests/integration/send_queue.rs | 664 +++++------------- 6 files changed, 673 insertions(+), 570 deletions(-) create mode 100644 crates/matrix-sdk/src/test_utils/mocks.rs diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index c63a0e42968..65dcb153196 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -343,7 +343,7 @@ impl SyncTimelineEvent { /// Get the event id of this `SyncTimelineEvent` if the event has any valid /// id. pub fn event_id(&self) -> Option { - self.kind.raw().get_field::("event_id").ok().flatten() + self.kind.event_id() } /// Returns a reference to the (potentially decrypted) Matrix event inside @@ -529,6 +529,12 @@ impl TimelineEventKind { } } + /// Get the event id of this `TimelineEventKind` if the event has any valid + /// id. + pub fn event_id(&self) -> Option { + self.raw().get_field::("event_id").ok().flatten() + } + /// If the event was a decrypted event that was successfully decrypted, get /// its encryption info. Otherwise, `None`. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { diff --git a/crates/matrix-sdk/src/test_utils.rs b/crates/matrix-sdk/src/test_utils.rs index f4aee4b6e14..0a6996ad756 100644 --- a/crates/matrix-sdk/src/test_utils.rs +++ b/crates/matrix-sdk/src/test_utils.rs @@ -14,6 +14,9 @@ use url::Url; pub mod events; +#[cfg(not(target_arch = "wasm32"))] +pub mod mocks; + use crate::{ config::RequestConfig, matrix_auth::{MatrixSession, MatrixSessionTokens}, diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs new file mode 100644 index 00000000000..935ccc8c639 --- /dev/null +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -0,0 +1,471 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Helpers to mock a server and have a client automatically connected to that +//! server, for the purpose of integration tests. + +#![allow(missing_debug_implementations)] + +use std::sync::{Arc, Mutex}; + +use matrix_sdk_base::deserialized_responses::TimelineEvent; +use matrix_sdk_test::{ + test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, + SyncResponseBuilder, +}; +use ruma::{OwnedEventId, OwnedRoomId, RoomId}; +use serde_json::json; +use wiremock::{ + matchers::{header, method, path, path_regex}, + Mock, MockBuilder, MockGuard, MockServer, Respond, ResponseTemplate, Times, +}; + +use super::logged_in_client; +use crate::{Client, Room}; + +/// A `wiremock` server along with a client connected to it, with useful methods +/// to help mocking Matrix client-server API endpoints easily. +/// +/// This is a pair of a [`MockServer`] and a [`Client]` (one can retrieve them +/// respectively with [`Self::server()`] and [`Self::client()`]). +/// +/// It implements mock endpoints, limiting the shared code as much as possible, +/// so the mocks are still flexible to use as scoped/unscoped mounts, named, and +/// so on. +/// +/// It works like this: +/// +/// - start by saying which endpoint you'd like to mock, e.g. +/// [`Self::mock_room_send()`]. This returns a specialized `MockSomething` +/// data structure, with its own impl. For this example, it's +/// [`MockRoomSend`]. +/// - configure the response on the endpoint-specific mock data structure. For +/// instance, if you want the sending to result in a transient failure, call +/// [`MockRoomSend::error500`]; if you want it to succeed and return the event +/// `$42`, call [`MockRoomSend::ok`]. It's still possible to call +/// [`MockRoomSend::respond_with()`], as we do with wiremock MockBuilder, for +/// maximum flexibility when the helpers aren't sufficient. +/// - once the endpoint's response is configured, for any mock builder, you get +/// a [`MatrixMock`]; this is a plain [`wiremock::Mock`] with the server +/// curried, so one doesn't have to pass it around when calling +/// [`MatrixMock::mount()`] or [`MatrixMock::mount_as_scoped()`]. As such, it +/// mostly defers its implementations to [`wiremock::Mock`] under the hood. +pub struct MatrixMockServer { + server: MockServer, + client: Client, + + /// Make the sync response builder stateful, to keep in memory the batch + /// token and avoid the client ignoring subsequent responses after the first + /// one. + sync_response_builder: Arc>, +} + +impl MatrixMockServer { + /// Create a new `wiremock` server specialized for Matrix usage. + pub async fn new() -> Self { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri().to_string())).await; + Self { client, server, sync_response_builder: Default::default() } + } + + /// Creates a new [`MatrixMockServer`] when both parts have been already + /// created. + pub fn from_parts(server: MockServer, client: Client) -> Self { + Self { client, server, sync_response_builder: Default::default() } + } + + /// Return the underlying client. + pub fn client(&self) -> Client { + self.client.clone() + } + + /// Return the underlying server. + pub fn server(&self) -> &MockServer { + &self.server + } + + /// Overrides the sync/ endpoint with knowledge that the given + /// invited/joined/knocked/left room exists, runs a sync and returns the + /// given room. + pub async fn sync_room(&self, room_id: &RoomId, room_data: impl Into) -> Room { + let any_room = room_data.into(); + + self.mock_sync() + .ok_and_run(move |builder| match any_room { + AnyRoomBuilder::Invited(invited) => { + builder.add_invited_room(invited); + } + AnyRoomBuilder::Joined(joined) => { + builder.add_joined_room(joined); + } + AnyRoomBuilder::Left(left) => { + builder.add_left_room(left); + } + AnyRoomBuilder::Knocked(knocked) => { + builder.add_knocked_room(knocked); + } + }) + .await; + + self.client.get_room(room_id).expect("look at me, the room is known now") + } + + /// Overrides the sync/ endpoint with knowledge that the given room exists + /// in the joined state, runs a sync and returns the given room. + pub async fn sync_joined_room(&self, room_id: &RoomId) -> Room { + self.sync_room(room_id, JoinedRoomBuilder::new(room_id)).await + } + + /// Verify that the previous mocks expected number of requests match + /// reality, and then cancels all active mocks. + pub async fn verify_and_reset(&self) { + self.server.verify().await; + self.server.reset().await; + } +} + +// Specific mount endpoints. +impl MatrixMockServer { + /// Mocks a sync endpoint. + pub fn mock_sync(&self) -> MockSync<'_> { + let mock = Mock::given(method("GET")) + .and(path("/_matrix/client/r0/sync")) + .and(header("authorization", "Bearer 1234")); + MockSync { + mock, + client: self.client.clone(), + server: &self.server, + sync_response_builder: self.sync_response_builder.clone(), + } + } + + /// Creates a prebuilt mock for sending an event in a room. + /// + /// Note: works with *any* room. + pub fn mock_room_send(&self) -> MockRoomSend<'_> { + let mock = Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")); + MockRoomSend { mock, server: &self.server } + } + + /// Creates a prebuilt mock for asking whether *a* room is encrypted or not. + /// + /// Note: Applies to all rooms. + pub fn mock_room_state_encryption(&self) -> MockEncryptionState<'_> { + let mock = Mock::given(method("GET")) + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); + MockEncryptionState { mock, server: &self.server } + } + + /// Creates a prebuilt mock for setting the room encryption state. + /// + /// Note: Applies to all rooms. + pub fn mock_set_room_state_encryption(&self) -> MockSetEncryptionState<'_> { + let mock = Mock::given(method("PUT")) + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); + MockSetEncryptionState { mock, server: &self.server } + } + + /// Creates a prebuilt mock for the room redact endpoint. + pub fn mock_room_redact(&self) -> MockRoomRedact<'_> { + let mock = Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) + .and(header("authorization", "Bearer 1234")); + MockRoomRedact { mock, server: &self.server } + } + + /// Creates a prebuilt mock for retrieving an event with /room/.../event. + pub fn mock_room_event(&self) -> MockRoomEvent<'_> { + let mock = Mock::given(method("GET")).and(header("authorization", "Bearer 1234")); + MockRoomEvent { mock, server: &self.server, room: None, match_event_id: false } + } +} + +/// Parameter to [`MatrixMockServer::sync_room`]. +pub enum AnyRoomBuilder { + /// A room we've been invited to. + Invited(InvitedRoomBuilder), + /// A room we've joined. + Joined(JoinedRoomBuilder), + /// A room we've left. + Left(LeftRoomBuilder), + /// A room we've knocked to. + Knocked(KnockedRoomBuilder), +} + +impl From for AnyRoomBuilder { + fn from(val: InvitedRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Invited(val) + } +} + +impl From for AnyRoomBuilder { + fn from(val: JoinedRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Joined(val) + } +} + +impl From for AnyRoomBuilder { + fn from(val: LeftRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Left(val) + } +} + +impl From for AnyRoomBuilder { + fn from(val: KnockedRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Knocked(val) + } +} + +/// A wrapper for a [`Mock`] as well as a [`MockServer`], allowing us to call +/// [`Mock::mount`] or [`Mock::mount_as_scoped`] without having to pass the +/// [`MockServer`] reference (i.e. call `mount()` instead of `mount(&server)`). +pub struct MatrixMock<'a> { + mock: Mock, + server: &'a MockServer, +} + +impl<'a> MatrixMock<'a> { + /// Set an expectation on the number of times this [`MatrixMock`] should + /// match in the current test case. + /// + /// Expectations are verified when the server is shutting down: if + /// the expectation is not satisfied, the [`MatrixMockServer`] will panic + /// and the `error_message` is shown. + /// + /// By default, no expectation is set for [`MatrixMock`]s. + pub fn expect>(self, num_calls: T) -> Self { + Self { mock: self.mock.expect(num_calls), ..self } + } + + /// Assign a name to your mock. + /// + /// The mock name will be used in error messages (e.g. if the mock + /// expectation is not satisfied) and debug logs to help you identify + /// what failed. + pub fn named(self, name: impl Into) -> Self { + Self { mock: self.mock.named(name), ..self } + } + + /// Respond to a response of this endpoint exactly once. + /// + /// After it's been called, subsequent responses will hit the next handler + /// or a 404. + /// + /// Also verifies that it's been called once. + pub fn mock_once(self) -> Self { + Self { mock: self.mock.up_to_n_times(1).expect(1), ..self } + } + + /// Mount a [`MatrixMock`] on the attached server. + /// + /// The [`MatrixMock`] will remain active until the [`MatrixMockServer`] is + /// shut down. If you want to control or limit how long your + /// [`MatrixMock`] stays active, check out [`Self::mount_as_scoped`]. + pub async fn mount(self) { + self.mock.mount(self.server).await; + } + + /// Mount a [`MatrixMock`] as **scoped** on the attached server. + /// + /// When using [`Self::mount`], your [`MatrixMock`]s will be active until + /// the [`MatrixMockServer`] is shut down. + /// + /// When using `mount_as_scoped`, your [`MatrixMock`]s will be active as + /// long as the returned [`MockGuard`] is not dropped. + /// + /// When the returned [`MockGuard`] is dropped, [`MatrixMockServer`] will + /// verify that the expectations set on the scoped [`MatrixMock`] were + /// verified - if not, it will panic. + pub async fn mount_as_scoped(self) -> MockGuard { + self.mock.mount_as_scoped(self.server).await + } +} + +/// A prebuilt mock for sending events to a room. +pub struct MockRoomSend<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockRoomSend<'a> { + /// Returns a send endpoint that emulates success, i.e. the event has been + /// sent with the given event id. + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + let returned_event_id = returned_event_id.into(); + MatrixMock { + mock: self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "event_id": returned_event_id + }))), + server: self.server, + } + } + + /// Returns a send endpoint that emulates a transient failure, i.e responds + /// with error 500. + pub fn error500(self) -> MatrixMock<'a> { + MatrixMock { mock: self.mock.respond_with(ResponseTemplate::new(500)), server: self.server } + } + + /// Returns a send endpoint that emulates a permanent failure (event is too + /// large). + pub fn error_too_large(self) -> MatrixMock<'a> { + MatrixMock { + mock: self.mock.respond_with(ResponseTemplate::new(413).set_body_json(json!({ + // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response + "errcode": "M_TOO_LARGE", + }))), + server: self.server, + } + } + + /// Specify how to respond to a query (viz., like + /// [`MockBuilder::respond_with`] does), when other predefined responses + /// aren't sufficient. + pub fn respond_with(self, func: R) -> MatrixMock<'a> { + MatrixMock { mock: self.mock.respond_with(func), server: self.server } + } +} + +/// A prebuilt mock for running sync v2. +pub struct MockSync<'a> { + mock: MockBuilder, + server: &'a MockServer, + sync_response_builder: Arc>, + client: Client, +} + +impl<'a> MockSync<'a> { + /// Temporarily mocks the sync with the given endpoint and runs a client + /// sync with it. + /// + /// After calling this function, the sync endpoint isn't mocked anymore. + pub async fn ok_and_run(self, func: F) { + let json_response = { + let mut builder = self.sync_response_builder.lock().unwrap(); + func(&mut builder); + builder.build_json_sync_response() + }; + + let _scope = self + .mock + .respond_with(ResponseTemplate::new(200).set_body_json(json_response)) + .mount_as_scoped(self.server) + .await; + + let _response = self.client.sync_once(Default::default()).await.unwrap(); + } +} + +/// A prebuilt mock for reading the encryption state of a room. +pub struct MockEncryptionState<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockEncryptionState<'a> { + /// Marks the room as encrypted. + pub fn encrypted(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with( + ResponseTemplate::new(200).set_body_json(&*test_json::sync_events::ENCRYPTION_CONTENT), + ); + MatrixMock { mock, server: self.server } + } + + /// Marks the room as not encrypted. + pub fn plain(self) -> MatrixMock<'a> { + let mock = self + .mock + .respond_with(ResponseTemplate::new(404).set_body_json(&*test_json::NOT_FOUND)); + MatrixMock { mock, server: self.server } + } +} + +/// A prebuilt mock for setting the encryption state of a room. +pub struct MockSetEncryptionState<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockSetEncryptionState<'a> { + /// Returns a mock for a successful setting of the encryption state event. + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + let event_id = returned_event_id.into(); + let mock = self.mock.respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id })), + ); + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for redacting an event in a room. +pub struct MockRoomRedact<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockRoomRedact<'a> { + /// Returns a redact endpoint that emulates success, i.e. the redaction + /// event has been sent with the given event id. + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + let event_id = returned_event_id.into(); + let mock = self.mock.respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id })), + ); + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for getting a single event in a room. +pub struct MockRoomEvent<'a> { + room: Option, + match_event_id: bool, + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockRoomEvent<'a> { + /// Limits the scope of this mock to a specific room. + pub fn room(self, room: impl Into) -> Self { + Self { room: Some(room.into()), ..self } + } + + /// Whether the mock checks for the event id from the event. + pub fn match_event_id(self) -> Self { + Self { match_event_id: true, ..self } + } + + /// Returns a redact endpoint that emulates success, i.e. the redaction + /// event has been sent with the given event id. + pub fn ok(self, event: TimelineEvent) -> MatrixMock<'a> { + let event_path = if self.match_event_id { + let event_id = event.kind.event_id().expect("an event id is required"); + event_id.to_string() + } else { + // Event is at the end, so no need to add anything. + "".to_owned() + }; + + let room_path = self.room.map_or_else(|| ".*".to_owned(), |room| room.to_string()); + + let mock = self + .mock + .and(path_regex(format!("^/_matrix/client/r0/rooms/{room_path}/event/{event_path}"))) + .respond_with(ResponseTemplate::new(200).set_body_json(event.into_raw().json())); + MatrixMock { server: self.server, mock } + } +} diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 13cebccde96..10205a5604f 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -1,9 +1,8 @@ // The http mocking library is not supported for wasm32 #![cfg(not(target_arch = "wasm32"))] -use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Client, Room}; -use matrix_sdk_test::{test_json, SyncResponseBuilder}; -use ruma::RoomId; +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Client}; +use matrix_sdk_test::test_json; use serde::Serialize; use wiremock::{ matchers::{header, method, path, query_param, query_param_is_missing}, @@ -79,28 +78,3 @@ async fn mock_sync_scoped( .mount_as_scoped(server) .await } - -/// Does a sync for a given room, and returns its `Room` object. -/// -/// Note this sync is token-less. -async fn mock_sync_with_new_room( - func: F, - client: &Client, - server: &MockServer, - room_id: &RoomId, -) -> Room { - let mut sync_response_builder = SyncResponseBuilder::default(); - func(&mut sync_response_builder); - let json_response = sync_response_builder.build_json_sync_response(); - - let _scope = Mock::given(method("GET")) - .and(path("/_matrix/client/r0/sync")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json_response)) - .mount_as_scoped(server) - .await; - - let _response = client.sync_once(Default::default()).await.unwrap(); - - client.get_room(room_id).expect("we should find the room we just sync'd from") -} diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index e8f294bb0d0..70e4d31200d 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -7,7 +7,7 @@ use futures_util::future::join_all; use matrix_sdk::{ config::SyncSettings, room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, - test_utils::events::EventFactory, + test_utils::{events::EventFactory, mocks::MatrixMockServer}, }; use matrix_sdk_base::RoomState; use matrix_sdk_test::{ @@ -33,7 +33,7 @@ use wiremock::{ Mock, ResponseTemplate, }; -use crate::{logged_in_client_with_server, mock_sync, mock_sync_with_new_room, synced_client}; +use crate::{logged_in_client_with_server, mock_sync, synced_client}; #[async_test] async fn test_invite_user_by_id() { let (client, server) = logged_in_client_with_server().await; @@ -720,78 +720,43 @@ async fn test_make_reply_event_doesnt_require_event_cache() { // Even if we don't have enabled the event cache, we'll resort to using the // /event query to get details on an event. - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; + let user_id = mock.client().user_id().unwrap().to_owned(); - let event_id = event_id!("$1"); - let resp_event_id = event_id!("$resp"); let room_id = room_id!("!galette:saucisse.bzh"); + let room = mock.sync_joined_room(room_id).await; + let event_id = event_id!("$1"); let f = EventFactory::new(); - - let raw_original_event = f - .text_msg("hi") - .event_id(event_id) - .sender(client.user_id().unwrap()) - .room(room_id) - .into_raw_timeline(); - - mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(raw_original_event.json())) + mock.mock_room_event() + .ok(f.text_msg("hi").event_id(event_id).sender(&user_id).room(room_id).into_timeline()) .expect(1) .named("/event") - .mount(&server) + .mount() .await; let new_content = RoomMessageEventContentWithoutRelation::text_plain("uh i mean bonjour"); - let room = client.get_room(room_id).unwrap(); - // make_edit_event works, even if the event cache hasn't been enabled. - room.make_edit_event(resp_event_id, EditedContent::RoomMessage(new_content)).await.unwrap(); + room.make_edit_event(event_id, EditedContent::RoomMessage(new_content)).await.unwrap(); } #[async_test] async fn test_enable_encryption_doesnt_stay_unencrypted() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; - mock_encryption_state(&server, false).await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$1"}))) - .mount(&server) - .await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_set_room_state_encryption().ok(event_id!("$1")).mount().await; let room_id = room_id!("!a:b.c"); - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; assert!(!room.is_encrypted().await.unwrap()); room.enable_encryption().await.expect("enabling encryption should work"); - server.reset().await; - mock_encryption_state(&server, true).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().encrypted().mount().await; assert!(room.is_encrypted().await.unwrap()); } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index ecdb57f451f..34e3efa7b6a 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1,11 +1,4 @@ -use std::{ - ops::Not as _, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex as StdMutex, - }, - time::Duration, -}; +use std::{ops::Not as _, sync::Arc, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ @@ -17,15 +10,11 @@ use matrix_sdk::{ RoomSendQueueUpdate, }, test_utils::{ - events::EventFactory, logged_in_client, logged_in_client_with_server, set_client_session, + events::EventFactory, logged_in_client, mocks::MatrixMockServer, set_client_session, }, Client, MemoryStore, }; -use matrix_sdk_test::{ - async_test, - mocks::{mock_encryption_state, mock_redaction}, - InvitedRoomBuilder, JoinedRoomBuilder, LeftRoomBuilder, -}; +use matrix_sdk_test::{async_test, InvitedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder}; use ruma::{ api::MatrixVersion, event_id, @@ -42,7 +31,7 @@ use ruma::{ }, mxc_uri, owned_user_id, room_id, serde::Raw, - uint, EventId, MxcUri, OwnedEventId, TransactionId, + uint, MxcUri, OwnedEventId, TransactionId, }; use serde_json::json; use tokio::{ @@ -50,12 +39,10 @@ use tokio::{ time::{sleep, timeout}, }; use wiremock::{ - matchers::{header, method, path, path_regex}, + matchers::{header, method, path}, Mock, Request, ResponseTemplate, }; -use crate::mock_sync_with_new_room; - // TODO put into the MatrixMockServer fn mock_jpeg_upload(mxc: &MxcUri, lock: Arc>) -> Mock { let mxc = mxc.to_owned(); @@ -79,24 +66,6 @@ fn mock_jpeg_upload(mxc: &MxcUri, lock: Arc>) -> Mock { }) } -fn mock_send_event(returned_event_id: &EventId) -> Mock { - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "event_id": returned_event_id, - }))) -} - -/// Return a mock that will fail all requests to /rooms/ROOM_ID/send with a -/// transient 500 error. -fn mock_send_transient_failure() -> Mock { - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(500)) -} - // A macro to assert on a stream of `RoomSendQueueUpdate`s. macro_rules! assert_update { // Check the next stream event is a local echo for an uploaded media. @@ -238,20 +207,11 @@ macro_rules! assert_update { #[async_test] async fn test_cant_send_invited_room() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // When I'm invited to a room, let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_invited_room(InvitedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_room(room_id, InvitedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -262,20 +222,28 @@ async fn test_cant_send_invited_room() { #[async_test] async fn test_cant_send_left_room() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // When I've left a room, let room_id = room_id!("!a:b.c"); + let room = mock.sync_room(room_id, LeftRoomBuilder::new(room_id)).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_left_room(LeftRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + // I can't send message to it with the send queue. + assert_matches!( + room.send_queue() + .send(RoomMessageEventContent::text_plain("Farewell, World!").into()) + .await, + Err(RoomSendQueueError::RoomNotJoined) + ); +} + +#[async_test] +async fn test_cant_send_knocked_room() { + let mock = MatrixMockServer::new().await; + + // When I've knocked into a room, + let room_id = room_id!("!a:b.c"); + let room = mock.sync_room(room_id, KnockedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -288,26 +256,17 @@ async fn test_cant_send_left_room() { #[async_test] async fn test_nothing_sent_when_disabled() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; // When I disable the send queue, let event_id = event_id!("$1"); - mock_send_event(event_id).expect(0).mount(&server).await; + mock.mock_room_send().ok(event_id).expect(0).mount().await; - client.send_queue().set_enabled(false).await; + mock.client().send_queue().set_enabled(false).await; // A message is queued, but never sent. room.send_queue() @@ -316,11 +275,10 @@ async fn test_nothing_sent_when_disabled() { .unwrap(); // But I can still send it with room.send(). - server.verify().await; - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; - mock_send_event(event_id).expect(1).mount(&server).await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id).expect(1).mount().await; let response = room.send(RoomMessageEventContent::text_plain("Hello, World!")).await.unwrap(); assert_eq!(response.event_id, event_id); @@ -328,20 +286,11 @@ async fn test_nothing_sent_when_disabled() { #[async_test] async fn test_smoke() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -357,11 +306,9 @@ async fn test_smoke() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -378,7 +325,7 @@ async fn test_smoke() { })) }) .expect(1) - .mount(&server) + .mount() .await; room.send_queue().send(RoomMessageEventContent::text_plain("1").into()).await.unwrap(); @@ -403,20 +350,11 @@ async fn test_smoke() { #[async_test] async fn test_smoke_raw() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -427,8 +365,8 @@ async fn test_smoke_raw() { // When the queue is enabled and I send message in some order, it does send it. let event_id = event_id!("$1"); - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$1")).mount(&server).await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id).mount().await; let json_content = r#"{"baguette": 42}"#.to_owned(); let event = Raw::from_json_string(json_content.clone()).unwrap(); @@ -456,8 +394,9 @@ async fn test_smoke_raw() { #[async_test] async fn test_error_then_locally_reenabling() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -466,16 +405,7 @@ async fn test_error_then_locally_reenabling() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -488,11 +418,10 @@ async fn test_error_then_locally_reenabling() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + let scoped_send = mock + .mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -507,7 +436,7 @@ async fn test_error_then_locally_reenabling() { ResponseTemplate::new(500) }) .expect(3) - .mount(&server) + .mount_as_scoped() .await; q.send(RoomMessageEventContent::text_plain("1").into()).await.unwrap(); @@ -548,8 +477,8 @@ async fn test_error_then_locally_reenabling() { // But the room send queue is disabled. assert!(!room.send_queue().is_enabled()); - server.reset().await; - mock_send_event(event_id!("$42")).expect(1).mount(&server).await; + drop(scoped_send); + mock.mock_room_send().ok(event_id!("$42")).expect(1).mount().await; // Re-enabling the *room* queue will re-send the same message in that room. room.send_queue().set_enabled(true); @@ -567,8 +496,9 @@ async fn test_error_then_locally_reenabling() { #[async_test] async fn test_error_then_globally_reenabling() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -577,16 +507,7 @@ async fn test_error_then_globally_reenabling() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -594,9 +515,9 @@ async fn test_error_then_globally_reenabling() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; - mock_encryption_state(&server, false).await; - mock_send_transient_failure().expect(3).mount(&server).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().error500().expect(3).mount().await; q.send(RoomMessageEventContent::text_plain("1").into()).await.unwrap(); @@ -622,9 +543,9 @@ async fn test_error_then_globally_reenabling() { assert!(watch.is_empty()); - server.reset().await; - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$42")).expect(1).mount(&server).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$42")).expect(1).mount().await; // Re-enabling the global queue will cause the event to be sent. client.send_queue().set_enabled(true).await; @@ -640,21 +561,13 @@ async fn test_error_then_globally_reenabling() { #[async_test] async fn test_reenabling_queue() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -694,25 +607,11 @@ async fn test_reenabling_queue() { assert!(watch.is_empty()); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - let mut num_request = num_request.lock().unwrap(); - - let event_id = format!("${}", *num_request); - *num_request += 1; - - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": event_id, - })) - }) - .expect(3) - .mount(&server) - .await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$2")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$3")).mock_once().mount().await; // But when reenabling the queue globally, client.send_queue().set_enabled(true).await; @@ -733,23 +632,16 @@ async fn test_reenabling_queue() { #[async_test] async fn test_disjoint_enabled_status() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id1 = room_id!("!a:b.c"); let room_id2 = room_id!("!b:b.c"); - let room1 = mock_sync_with_new_room( - |builder| { - builder - .add_joined_room(JoinedRoomBuilder::new(room_id1)) - .add_joined_room(JoinedRoomBuilder::new(room_id2)); - }, - &client, - &server, - room_id1, - ) - .await; - let room2 = client.get_room(room_id2).unwrap(); + + let room1 = mock.sync_joined_room(room_id1).await; + let room2 = mock.sync_joined_room(room_id2).await; + + let client = mock.client(); // When I start with a disabled send queue, client.send_queue().set_enabled(false).await; @@ -778,20 +670,11 @@ async fn test_disjoint_enabled_status() { #[async_test] async fn test_cancellation() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -805,12 +688,10 @@ async fn test_cancellation() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -832,11 +713,11 @@ async fn test_cancellation() { })) }) .expect(2) - .mount(&server) + .mount() .await; // The redact of txn1 will happen because we asked for it previously. - mock_redaction(event_id!("$1")).expect(1).mount(&server).await; + mock.mock_room_redact().ok(event_id!("$1")).mount().await; let handle1 = q.send(RoomMessageEventContent::text_plain("msg1").into()).await.unwrap(); let handle2 = q.send(RoomMessageEventContent::text_plain("msg2").into()).await.unwrap(); @@ -905,20 +786,11 @@ async fn test_edit() { // to edit a local echo, since if the cancellation test passes, all ways // would work here too similarly. - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -932,12 +804,10 @@ async fn test_edit() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -959,27 +829,22 @@ async fn test_edit() { })) }) .expect(3) - .mount(&server) + .mount() .await; // The /event endpoint is used to retrieve the original event, during creation // of the edit event. - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) - .and(header("authorization", "Bearer 1234")) - .respond_with( - ResponseTemplate::new(200).set_body_json( - EventFactory::new() - .text_msg("msg1") - .sender(client.user_id().unwrap()) - .room(room_id) - .into_raw_timeline() - .json(), - ), - ) + let client = mock.client(); + mock.mock_room_event() + .room(room_id) + .ok(EventFactory::new() + .text_msg("msg1") + .sender(client.user_id().unwrap()) + .room(room_id) + .into_timeline()) .expect(1) - .named("get_event") - .mount(&server) + .named("room_event") + .mount() .await; let handle1 = q.send(RoomMessageEventContent::text_plain("msg1").into()).await.unwrap(); @@ -1025,20 +890,11 @@ async fn test_edit() { #[async_test] async fn test_edit_with_poll_start() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1052,12 +908,10 @@ async fn test_edit_with_poll_start() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1080,27 +934,21 @@ async fn test_edit_with_poll_start() { }) .named("send_event") .expect(2) - .mount(&server) + .mount() .await; // The /event endpoint is used to retrieve the original event, during creation // of the edit event. - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) - .and(header("authorization", "Bearer 1234")) - .respond_with( - ResponseTemplate::new(200).set_body_json( - EventFactory::new() - .poll_start("poll_start", "question", vec!["Answer A"]) - .sender(client.user_id().unwrap()) - .room(room_id) - .into_raw_timeline() - .json(), - ), - ) + let client = mock.client(); + mock.mock_room_event() + .ok(EventFactory::new() + .poll_start("poll_start", "question", vec!["Answer A"]) + .sender(client.user_id().unwrap()) + .room(room_id) + .into_timeline()) .expect(1) .named("get_event") - .mount(&server) + .mount() .await; let poll_answers: UnstablePollAnswers = @@ -1170,20 +1018,11 @@ async fn test_edit_with_poll_start() { #[async_test] async fn test_edit_while_being_sent_and_fails() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1197,11 +1036,9 @@ async fn test_edit_while_being_sent_and_fails() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1216,7 +1053,7 @@ async fn test_edit_while_being_sent_and_fails() { ResponseTemplate::new(500) }) .expect(3) // reattempts, because of short_retry() - .mount(&server) + .mount() .await; let handle = q.send(RoomMessageEventContent::text_plain("yo").into()).await.unwrap(); @@ -1261,20 +1098,11 @@ async fn test_edit_while_being_sent_and_fails() { #[async_test] async fn test_edit_wakes_the_sending_task() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1283,18 +1111,9 @@ async fn test_edit_wakes_the_sending_task() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - let send_mock_scope = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(413).set_body_json(json!({ - // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response - "errcode": "M_TOO_LARGE", - }))) - .expect(1) - .mount_as_scoped(&server) - .await; + let send_mock_scope = mock.mock_room_send().error_too_large().expect(1).mount_as_scoped().await; let handle = q.send(RoomMessageEventContent::text_plain("welcome to my ted talk").into()).await.unwrap(); @@ -1311,7 +1130,7 @@ async fn test_edit_wakes_the_sending_task() { // Now edit the event's content (imagine we make it "shorter"). drop(send_mock_scope); - mock_send_event(event_id!("$1")).mount(&server).await; + mock.mock_room_send().ok(event_id!("$1")).mount().await; let edited = handle .edit(RoomMessageEventContent::text_plain("here's the summary of my ted talk").into()) @@ -1328,21 +1147,13 @@ async fn test_edit_wakes_the_sending_task() { #[async_test] async fn test_abort_after_disable() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1359,12 +1170,12 @@ async fn test_abort_after_disable() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; // Respond to /send with a transient 500 error. - mock_send_transient_failure().expect(3).mount(&server).await; + mock.mock_room_send().error500().expect(3).mount().await; // One message is queued. let handle = q.send(RoomMessageEventContent::text_plain("hey there").into()).await.unwrap(); @@ -1394,22 +1205,14 @@ async fn test_abort_after_disable() { #[async_test] async fn test_abort_or_edit_after_send() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; // Start with an enabled sending queue. + let client = mock.client(); client.send_queue().set_enabled(true).await; let q = room.send_queue(); @@ -1418,9 +1221,9 @@ async fn test_abort_or_edit_after_send() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$1")).mount(&server).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$1")).mount().await; let handle = q.send(RoomMessageEventContent::text_plain("hey there").into()).await.unwrap(); @@ -1445,20 +1248,11 @@ async fn test_abort_or_edit_after_send() { #[async_test] async fn test_abort_while_being_sent_and_fails() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1472,11 +1266,9 @@ async fn test_abort_while_being_sent_and_fails() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1491,7 +1283,7 @@ async fn test_abort_while_being_sent_and_fails() { ResponseTemplate::new(500) }) .expect(3) // reattempts, because of short_retry() - .mount(&server) + .mount() .await; let handle = q.send(RoomMessageEventContent::text_plain("yo").into()).await.unwrap(); @@ -1524,21 +1316,13 @@ async fn test_abort_while_being_sent_and_fails() { #[async_test] async fn test_unrecoverable_errors() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1555,33 +1339,14 @@ async fn test_unrecoverable_errors() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; - - let respond_with_unrecoverable = AtomicBool::new(true); + mock.mock_room_state_encryption().plain().mount().await; // Respond to the first /send with an unrecoverable error. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - // The first message gets M_TOO_LARGE, subsequent messages will encounter a - // great success. - if respond_with_unrecoverable.swap(false, Ordering::SeqCst) { - ResponseTemplate::new(413).set_body_json(json!({ - // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response - "errcode": "M_TOO_LARGE", - })) - } else { - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": "$42", - })) - } - }) - .expect(2) - .mount(&server) - .await; + mock.mock_room_send().error_too_large().mock_once().mount().await; + // Respond to the second /send with an OK response. + mock.mock_room_send().ok(event_id!("$42")).mock_once().mount().await; // Queue two messages. q.send(RoomMessageEventContent::text_plain("i'm too big for ya").into()).await.unwrap(); @@ -1613,21 +1378,13 @@ async fn test_unrecoverable_errors() { #[async_test] async fn test_unwedge_unrecoverable_errors() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1644,33 +1401,14 @@ async fn test_unwedge_unrecoverable_errors() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; - - let respond_with_unrecoverable = AtomicBool::new(true); + mock.mock_room_state_encryption().plain().mount().await; // Respond to the first /send with an unrecoverable error. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - // The first message gets M_TOO_LARGE, subsequent messages will encounter a - // great success. - if respond_with_unrecoverable.swap(false, Ordering::SeqCst) { - ResponseTemplate::new(413).set_body_json(json!({ - // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response - "errcode": "M_TOO_LARGE", - })) - } else { - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": "$42", - })) - } - }) - .expect(2) - .mount(&server) - .await; + mock.mock_room_send().error_too_large().mock_once().mount().await; + // Respond to the second /send with an OK response. + mock.mock_room_send().ok(event_id!("$42")).mock_once().mount().await; // Queue the unrecoverable message. q.send(RoomMessageEventContent::text_plain("i'm too big for ya").into()).await.unwrap(); @@ -1709,25 +1447,16 @@ async fn test_no_network_access_error_is_recoverable() { // server in a static. Using the line below will create a "bare" server, // which is effectively dropped upon `drop()`. let server = wiremock::MockServer::builder().start().await; - let client = logged_in_client(Some(server.uri().to_string())).await; + let mock = MatrixMockServer::from_parts(server, client.clone()); // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; // Dropping the server: any subsequent attempt to connect mimics an unreachable // server, which might be caused by missing network. - drop(server); + drop(mock); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1783,20 +1512,11 @@ async fn test_reloading_rooms_with_unsent_events() { .unwrap(); set_client_session(&client).await; - // Mark two rooms as joined. - let room = mock_sync_with_new_room( - |builder| { - builder - .add_joined_room(JoinedRoomBuilder::new(room_id)) - .add_joined_room(JoinedRoomBuilder::new(room_id2)); - }, - &client, - &server, - room_id, - ) - .await; + let mock = MatrixMockServer::from_parts(server, client.clone()); - let room2 = client.get_room(room_id2).unwrap(); + // Mark two rooms as joined. + let room = mock.sync_joined_room(room_id).await; + let room2 = mock.sync_joined_room(room_id2).await; // Globally disable the send queue. let q = client.send_queue(); @@ -1819,7 +1539,7 @@ async fn test_reloading_rooms_with_unsent_events() { sleep(Duration::from_millis(300)).await; assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; { // Kill the client, let it close background tasks. @@ -1832,26 +1552,13 @@ async fn test_reloading_rooms_with_unsent_events() { // Create a new client with the same memory backend. As the send queues are // enabled by default, it will respawn tasks for sending events to those two // rooms in the background. - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - let event_id = StdMutex::new(0); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - let mut event_id_guard = event_id.lock().unwrap(); - let event_id = *event_id_guard; - *event_id_guard += 1; - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": event_id - })) - }) - .expect(2) - .mount(&server) - .await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$2")).mock_once().mount().await; let client = Client::builder() - .homeserver_url(server.uri()) + .homeserver_url(mock.server().uri()) .server_versions([MatrixVersion::V1_0]) .store_config(StoreConfig::new().state_store(store)) .request_config(RequestConfig::new().disable_retry()) @@ -1866,25 +1573,16 @@ async fn test_reloading_rooms_with_unsent_events() { sleep(Duration::from_secs(1)).await; // The real assertion is on the expect(2) on the above Mock. - server.verify().await; + mock.verify_and_reset().await; } #[async_test] async fn test_reactions() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1897,11 +1595,9 @@ async fn test_reactions() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1921,18 +1617,12 @@ async fn test_reactions() { })) }) .expect(3) - .mount(&server) + .mount() .await; // Sending of the second emoji has started; abort it, it will result in a redact // request. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({"event_id": "$3"}))) - .expect(1) - .mount(&server) - .await; + mock.mock_room_redact().ok(event_id!("$3")).expect(1).mount().await; // Send a message. let msg_handle = @@ -2010,20 +1700,12 @@ async fn test_reactions() { #[async_test] async fn test_media_uploads() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -2063,12 +1745,13 @@ async fn test_media_uploads() { // ---------------------- // Prepare endpoints. - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$1")).expect(1).mount(&server).await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; let allow_upload_lock = Arc::new(Mutex::new(())); let block_upload = allow_upload_lock.lock().await; + let server = mock.server(); mock_jpeg_upload(mxc_uri!("mxc://sdk.rs/thumbnail"), allow_upload_lock.clone()) .up_to_n_times(1) .expect(1) @@ -2119,6 +1802,7 @@ async fn test_media_uploads() { assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); // The media is immediately available from the cache. + let client = mock.client(); let file_media = client .media() .get_media_content( From f032d16d2031dcabbd6ca427e259e6c3c10ffd64 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 17:27:43 +0100 Subject: [PATCH 478/979] task(tests): mock upload too --- crates/matrix-sdk/src/test_utils/mocks.rs | 47 +++- .../tests/integration/room/attachment/mod.rs | 248 +++++++----------- .../tests/integration/send_queue.rs | 62 ++--- 3 files changed, 169 insertions(+), 188 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 935ccc8c639..0feb623bb25 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -24,10 +24,10 @@ use matrix_sdk_test::{ test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, }; -use ruma::{OwnedEventId, OwnedRoomId, RoomId}; +use ruma::{MxcUri, OwnedEventId, OwnedRoomId, RoomId}; use serde_json::json; use wiremock::{ - matchers::{header, method, path, path_regex}, + matchers::{body_partial_json, header, method, path, path_regex}, Mock, MockBuilder, MockGuard, MockServer, Respond, ResponseTemplate, Times, }; @@ -193,6 +193,14 @@ impl MatrixMockServer { let mock = Mock::given(method("GET")).and(header("authorization", "Bearer 1234")); MockRoomEvent { mock, server: &self.server, room: None, match_event_id: false } } + + /// Create a prebuilt mock for uploading media. + pub fn mock_upload(&self) -> MockUpload<'_> { + let mock = Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")); + MockUpload { mock, server: &self.server } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -303,6 +311,12 @@ pub struct MockRoomSend<'a> { } impl<'a> MockRoomSend<'a> { + /// Ensures that the body of the request is a superset of the provided + /// `body` parameter. + pub fn body_matches_partial_json(self, body: serde_json::Value) -> Self { + Self { mock: self.mock.and(body_partial_json(body)), ..self } + } + /// Returns a send endpoint that emulates success, i.e. the event has been /// sent with the given event id. pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { @@ -469,3 +483,32 @@ impl<'a> MockRoomEvent<'a> { MatrixMock { server: self.server, mock } } } + +/// A prebuilt mock for uploading media. +pub struct MockUpload<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockUpload<'a> { + /// Expect that the content type matches what's given here. + pub fn expect_mime_type(self, content_type: &str) -> Self { + Self { mock: self.mock.and(header("content-type", content_type)), ..self } + } + + /// Returns a redact endpoint that emulates success, i.e. the redaction + /// event has been sent with the given event id. + pub fn ok(self, mxc_id: &MxcUri) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": mxc_id + }))); + MatrixMock { server: self.server, mock } + } + + /// Specify how to respond to a query (viz., like + /// [`MockBuilder::respond_with`] does), when other predefined responses + /// aren't sufficient. + pub fn respond_with(self, func: R) -> MatrixMock<'a> { + MatrixMock { mock: self.mock.respond_with(func), server: self.server } + } +} diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 3e500bd1a1b..d915f05a80d 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -1,62 +1,47 @@ -use std::{sync::Mutex, time::Duration}; +use std::time::Duration; use matrix_sdk::{ attachment::{ AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, Thumbnail, }, - config::SyncSettings, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, - test_utils::logged_in_client_with_server, + test_utils::mocks::MatrixMockServer, }; -use matrix_sdk_test::{async_test, mocks::mock_encryption_state, test_json, DEFAULT_TEST_ROOM_ID}; +use matrix_sdk_test::{async_test, DEFAULT_TEST_ROOM_ID}; use ruma::{ event_id, events::{room::MediaSource, Mentions}, - owned_mxc_uri, owned_user_id, uint, + mxc_uri, owned_mxc_uri, owned_user_id, uint, }; use serde_json::json; -use wiremock::{ - matchers::{body_partial_json, header, method, path, path_regex}, - Mock, ResponseTemplate, -}; - -use crate::mock_sync; #[async_test] async fn test_room_attachment_send() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ + let expected_event_id = event_id!("$h29iv0s8:example.com"); + + mock.mock_room_send() + .body_matches_partial_json(json!({ "info": { "mimetype": "image/jpeg", } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) + })) + .ok(expected_event_id) + .mock_once() + .mount() .await; - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() .await; - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; let response = room .send_attachment( @@ -68,45 +53,36 @@ async fn test_room_attachment_send() { .await .unwrap(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id); + assert_eq!(expected_event_id, response.event_id); } #[async_test] async fn test_room_attachment_send_info() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ + let expected_event_id = event_id!("$h29iv0s8:example.com"); + mock.mock_room_send() + .body_matches_partial_json(json!({ "info": { "mimetype": "image/jpeg", "h": 600, "w": 800, } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) + })) + .ok(expected_event_id) + .mock_once() + .mount() .await; - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() .await; - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; let config = AttachmentConfig::new() .info(AttachmentInfo::Image(BaseImageInfo { @@ -122,46 +98,42 @@ async fn test_room_attachment_send_info() { .await .unwrap(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + assert_eq!(expected_event_id, response.event_id) } #[async_test] async fn test_room_attachment_send_wrong_info() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ + // Note: this mock is NOT called because the height and width are lost, because + // we're trying to send the attachment as an image, while we provide a + // `VideoInfo`. + // + // So long for static typing. + + mock.mock_room_send() + .body_matches_partial_json(json!({ "info": { "mimetype": "image/jpeg", "h": 600, "w": 800, } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) + })) + .ok(event_id!("$unused")) + .mount() .await; - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/yo")) + .mock_once() + .mount() .await; - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; + // Here, using `AttachmentInfo::Video`… let config = AttachmentConfig::new() .info(AttachmentInfo::Video(BaseVideoInfo { height: Some(uint!(600)), @@ -172,23 +144,26 @@ async fn test_room_attachment_send_wrong_info() { })) .caption(Some("image caption".to_owned())); + // But here, using `image/jpeg`. let response = room.send_attachment("image.jpg", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config).await; + // In the real-world, this would lead to the size information getting lost, + // instead of an error during upload. …Is this test any useful? response.unwrap_err(); } #[async_test] async fn test_room_attachment_send_info_thumbnail() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; let media_mxc = owned_mxc_uri!("mxc://example.com/media"); let thumbnail_mxc = owned_mxc_uri!("mxc://example.com/thumbnail"); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ + let expected_event_id = event_id!("$h29iv0s8:example.com"); + + mock.mock_room_send() + .body_matches_partial_json(json!({ "info": { "mimetype": "image/jpeg", "h": 600, @@ -201,47 +176,20 @@ async fn test_room_attachment_send_info_thumbnail() { }, "thumbnail_url": thumbnail_mxc, } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - let counter = Mutex::new(0); - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with({ - // First request: return the thumbnail MXC; - // Second request: return the media MXC. - let media_mxc = media_mxc.clone(); - let thumbnail_mxc = thumbnail_mxc.clone(); - move |_: &wiremock::Request| { - let mut counter = counter.lock().unwrap(); - if *counter == 0 { - *counter += 1; - ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": &thumbnail_mxc - })) - } else { - ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": &media_mxc - })) - } - } - }) - .expect(2) - .mount(&server) + })) + .ok(expected_event_id) + .mock_once() + .mount() .await; - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; + // First request to /upload: return the thumbnail MXC. + mock.mock_upload().expect_mime_type("image/jpeg").ok(&thumbnail_mxc).mock_once().mount().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + // Second request: return the media MXC. + mock.mock_upload().expect_mime_type("image/jpeg").ok(&media_mxc).mock_once().mount().await; - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; // Preconditions: nothing is found in the cache. let media_request = @@ -250,6 +198,8 @@ async fn test_room_attachment_send_info_thumbnail() { source: MediaSource::Plain(thumbnail_mxc.clone()), format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(480), uint!(360))), }; + + let client = mock.client(); let _ = client.media().get_media_content(&media_request, true).await.unwrap_err(); let _ = client.media().get_media_content(&thumbnail_request, true).await.unwrap_err(); @@ -277,7 +227,7 @@ async fn test_room_attachment_send_info_thumbnail() { .unwrap(); // The event was sent. - assert_eq!(response.event_id, event_id!("$h29iv0s8:example.com")); + assert_eq!(response.event_id, expected_event_id); // The media is immediately cached in the cache store, so we don't need to set // up another mock endpoint for getting the media. @@ -311,38 +261,30 @@ async fn test_room_attachment_send_info_thumbnail() { #[async_test] async fn test_room_attachment_send_mentions() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ + let expected_event_id = event_id!("$h29iv0s8:example.com"); + + mock.mock_room_send() + .body_matches_partial_json(json!({ "m.mentions": { "user_ids": ["@user:localhost"], } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) + })) + .ok(expected_event_id) + .mock_once() + .mount() .await; - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() .await; - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; let response = room .send_attachment( @@ -355,5 +297,5 @@ async fn test_room_attachment_send_mentions() { .await .unwrap(); - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + assert_eq!(expected_event_id, response.event_id); } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 34e3efa7b6a..4fd5738ae48 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -10,7 +10,10 @@ use matrix_sdk::{ RoomSendQueueUpdate, }, test_utils::{ - events::EventFactory, logged_in_client, mocks::MatrixMockServer, set_client_session, + events::EventFactory, + logged_in_client, + mocks::{MatrixMock, MatrixMockServer}, + set_client_session, }, Client, MemoryStore, }; @@ -38,32 +41,28 @@ use tokio::{ sync::Mutex, time::{sleep, timeout}, }; -use wiremock::{ - matchers::{header, method, path}, - Mock, Request, ResponseTemplate, -}; +use wiremock::{Request, ResponseTemplate}; -// TODO put into the MatrixMockServer -fn mock_jpeg_upload(mxc: &MxcUri, lock: Arc>) -> Mock { +fn mock_jpeg_upload<'a>( + mock: &'a MatrixMockServer, + mxc: &MxcUri, + lock: Arc>, +) -> MatrixMock<'a> { let mxc = mxc.to_owned(); - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(move |_req: &Request| { - // Wait for the signal from the main task that we can process this query. - let mock_lock = lock.clone(); - std::thread::spawn(move || { - tokio::runtime::Runtime::new().unwrap().block_on(async { - drop(mock_lock.lock().await); - }); - }) - .join() - .unwrap(); - ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": mxc - })) + mock.mock_upload().expect_mime_type("image/jpeg").respond_with(move |_req: &Request| { + // Wait for the signal from the main task that we can process this query. + let mock_lock = lock.clone(); + std::thread::spawn(move || { + tokio::runtime::Runtime::new().unwrap().block_on(async { + drop(mock_lock.lock().await); + }); }) + .join() + .unwrap(); + ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": mxc + })) + }) } // A macro to assert on a stream of `RoomSendQueueUpdate`s. @@ -1751,16 +1750,13 @@ async fn test_media_uploads() { let allow_upload_lock = Arc::new(Mutex::new(())); let block_upload = allow_upload_lock.lock().await; - let server = mock.server(); - mock_jpeg_upload(mxc_uri!("mxc://sdk.rs/thumbnail"), allow_upload_lock.clone()) - .up_to_n_times(1) - .expect(1) - .mount(&server) + mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/thumbnail"), allow_upload_lock.clone()) + .mock_once() + .mount() .await; - mock_jpeg_upload(mxc_uri!("mxc://sdk.rs/media"), allow_upload_lock.clone()) - .up_to_n_times(1) - .expect(1) - .mount(&server) + mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/media"), allow_upload_lock.clone()) + .mock_once() + .mount() .await; // ---------------------- From 965a59d5b80693ab4ec2735cd6115472b1d5bb13 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 7 Nov 2024 15:50:43 +0100 Subject: [PATCH 479/979] task(tests): create the client with `MatrixMockServer::make_client()` instead of embedding one into the struct --- crates/matrix-sdk/src/test_utils/mocks.rs | 43 +++++---- .../tests/integration/room/attachment/mod.rs | 16 ++-- .../tests/integration/room/joined.rs | 8 +- .../tests/integration/send_queue.rs | 94 ++++++++++--------- 4 files changed, 88 insertions(+), 73 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 0feb623bb25..b03f482bc7d 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -34,11 +34,8 @@ use wiremock::{ use super::logged_in_client; use crate::{Client, Room}; -/// A `wiremock` server along with a client connected to it, with useful methods -/// to help mocking Matrix client-server API endpoints easily. -/// -/// This is a pair of a [`MockServer`] and a [`Client]` (one can retrieve them -/// respectively with [`Self::server()`] and [`Self::client()`]). +/// A `wiremock` [`MockServer`] along with useful methods to help mocking Matrix +/// client-server API endpoints easily. /// /// It implements mock endpoints, limiting the shared code as much as possible, /// so the mocks are still flexible to use as scoped/unscoped mounts, named, and @@ -63,7 +60,6 @@ use crate::{Client, Room}; /// mostly defers its implementations to [`wiremock::Mock`] under the hood. pub struct MatrixMockServer { server: MockServer, - client: Client, /// Make the sync response builder stateful, to keep in memory the batch /// token and avoid the client ignoring subsequent responses after the first @@ -75,19 +71,19 @@ impl MatrixMockServer { /// Create a new `wiremock` server specialized for Matrix usage. pub async fn new() -> Self { let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri().to_string())).await; - Self { client, server, sync_response_builder: Default::default() } + Self { server, sync_response_builder: Default::default() } } /// Creates a new [`MatrixMockServer`] when both parts have been already /// created. - pub fn from_parts(server: MockServer, client: Client) -> Self { - Self { client, server, sync_response_builder: Default::default() } + pub fn from_server(server: MockServer) -> Self { + Self { server, sync_response_builder: Default::default() } } - /// Return the underlying client. - pub fn client(&self) -> Client { - self.client.clone() + /// Creates a new [`Client`] configured to use this server, preconfigured + /// with a session expected by the server endpoints. + pub async fn make_client(&self) -> Client { + logged_in_client(Some(self.server.uri().to_string())).await } /// Return the underlying server. @@ -98,11 +94,16 @@ impl MatrixMockServer { /// Overrides the sync/ endpoint with knowledge that the given /// invited/joined/knocked/left room exists, runs a sync and returns the /// given room. - pub async fn sync_room(&self, room_id: &RoomId, room_data: impl Into) -> Room { + pub async fn sync_room( + &self, + client: &Client, + room_id: &RoomId, + room_data: impl Into, + ) -> Room { let any_room = room_data.into(); self.mock_sync() - .ok_and_run(move |builder| match any_room { + .ok_and_run(client, move |builder| match any_room { AnyRoomBuilder::Invited(invited) => { builder.add_invited_room(invited); } @@ -118,13 +119,13 @@ impl MatrixMockServer { }) .await; - self.client.get_room(room_id).expect("look at me, the room is known now") + client.get_room(room_id).expect("look at me, the room is known now") } /// Overrides the sync/ endpoint with knowledge that the given room exists /// in the joined state, runs a sync and returns the given room. - pub async fn sync_joined_room(&self, room_id: &RoomId) -> Room { - self.sync_room(room_id, JoinedRoomBuilder::new(room_id)).await + pub async fn sync_joined_room(&self, client: &Client, room_id: &RoomId) -> Room { + self.sync_room(client, room_id, JoinedRoomBuilder::new(room_id)).await } /// Verify that the previous mocks expected number of requests match @@ -144,7 +145,6 @@ impl MatrixMockServer { .and(header("authorization", "Bearer 1234")); MockSync { mock, - client: self.client.clone(), server: &self.server, sync_response_builder: self.sync_response_builder.clone(), } @@ -360,7 +360,6 @@ pub struct MockSync<'a> { mock: MockBuilder, server: &'a MockServer, sync_response_builder: Arc>, - client: Client, } impl<'a> MockSync<'a> { @@ -368,7 +367,7 @@ impl<'a> MockSync<'a> { /// sync with it. /// /// After calling this function, the sync endpoint isn't mocked anymore. - pub async fn ok_and_run(self, func: F) { + pub async fn ok_and_run(self, client: &Client, func: F) { let json_response = { let mut builder = self.sync_response_builder.lock().unwrap(); func(&mut builder); @@ -381,7 +380,7 @@ impl<'a> MockSync<'a> { .mount_as_scoped(self.server) .await; - let _response = self.client.sync_once(Default::default()).await.unwrap(); + let _response = client.sync_once(Default::default()).await.unwrap(); } } diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index d915f05a80d..22c8a356a4f 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -40,7 +40,8 @@ async fn test_room_attachment_send() { .mount() .await; - let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; let response = room @@ -81,7 +82,8 @@ async fn test_room_attachment_send_info() { .mount() .await; - let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; let config = AttachmentConfig::new() @@ -130,7 +132,8 @@ async fn test_room_attachment_send_wrong_info() { .mount() .await; - let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; // Here, using `AttachmentInfo::Video`… @@ -188,7 +191,8 @@ async fn test_room_attachment_send_info_thumbnail() { // Second request: return the media MXC. mock.mock_upload().expect_mime_type("image/jpeg").ok(&media_mxc).mock_once().mount().await; - let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; // Preconditions: nothing is found in the cache. @@ -199,7 +203,6 @@ async fn test_room_attachment_send_info_thumbnail() { format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(480), uint!(360))), }; - let client = mock.client(); let _ = client.media().get_media_content(&media_request, true).await.unwrap_err(); let _ = client.media().get_media_content(&thumbnail_request, true).await.unwrap_err(); @@ -283,7 +286,8 @@ async fn test_room_attachment_send_mentions() { .mount() .await; - let room = mock.sync_joined_room(&DEFAULT_TEST_ROOM_ID).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; let response = room diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 70e4d31200d..d8b247685fc 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -721,10 +721,11 @@ async fn test_make_reply_event_doesnt_require_event_cache() { // /event query to get details on an event. let mock = MatrixMockServer::new().await; - let user_id = mock.client().user_id().unwrap().to_owned(); + let client = mock.make_client().await; + let user_id = client.user_id().unwrap().to_owned(); let room_id = room_id!("!galette:saucisse.bzh"); - let room = mock.sync_joined_room(room_id).await; + let room = mock.sync_joined_room(&client, room_id).await; let event_id = event_id!("$1"); let f = EventFactory::new(); @@ -744,12 +745,13 @@ async fn test_make_reply_event_doesnt_require_event_cache() { #[async_test] async fn test_enable_encryption_doesnt_stay_unencrypted() { let mock = MatrixMockServer::new().await; + let client = mock.make_client().await; mock.mock_room_state_encryption().plain().mount().await; mock.mock_set_room_state_encryption().ok(event_id!("$1")).mount().await; let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let room = mock.sync_joined_room(&client, room_id).await; assert!(!room.is_encrypted().await.unwrap()); diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 4fd5738ae48..9d18578864e 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -210,7 +210,8 @@ async fn test_cant_send_invited_room() { // When I'm invited to a room, let room_id = room_id!("!a:b.c"); - let room = mock.sync_room(room_id, InvitedRoomBuilder::new(room_id)).await; + let client = mock.make_client().await; + let room = mock.sync_room(&client, room_id, InvitedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -225,7 +226,8 @@ async fn test_cant_send_left_room() { // When I've left a room, let room_id = room_id!("!a:b.c"); - let room = mock.sync_room(room_id, LeftRoomBuilder::new(room_id)).await; + let client = mock.make_client().await; + let room = mock.sync_room(&client, room_id, LeftRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -242,7 +244,8 @@ async fn test_cant_send_knocked_room() { // When I've knocked into a room, let room_id = room_id!("!a:b.c"); - let room = mock.sync_room(room_id, KnockedRoomBuilder::new(room_id)).await; + let client = mock.make_client().await; + let room = mock.sync_room(&client, room_id, KnockedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -259,13 +262,14 @@ async fn test_nothing_sent_when_disabled() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; // When I disable the send queue, let event_id = event_id!("$1"); mock.mock_room_send().ok(event_id).expect(0).mount().await; - mock.client().send_queue().set_enabled(false).await; + client.send_queue().set_enabled(false).await; // A message is queued, but never sent. room.send_queue() @@ -289,7 +293,9 @@ async fn test_smoke() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -353,7 +359,8 @@ async fn test_smoke_raw() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -395,7 +402,7 @@ async fn test_smoke_raw() { async fn test_error_then_locally_reenabling() { let mock = MatrixMockServer::new().await; - let client = mock.client(); + let client = mock.make_client().await; let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -404,7 +411,7 @@ async fn test_error_then_locally_reenabling() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -497,7 +504,7 @@ async fn test_error_then_locally_reenabling() { async fn test_error_then_globally_reenabling() { let mock = MatrixMockServer::new().await; - let client = mock.client(); + let client = mock.make_client().await; let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -506,7 +513,7 @@ async fn test_error_then_globally_reenabling() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -564,9 +571,9 @@ async fn test_reenabling_queue() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; - let client = mock.client(); let errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -637,10 +644,9 @@ async fn test_disjoint_enabled_status() { let room_id1 = room_id!("!a:b.c"); let room_id2 = room_id!("!b:b.c"); - let room1 = mock.sync_joined_room(room_id1).await; - let room2 = mock.sync_joined_room(room_id2).await; - - let client = mock.client(); + let client = mock.make_client().await; + let room1 = mock.sync_joined_room(&client, room_id1).await; + let room2 = mock.sync_joined_room(&client, room_id2).await; // When I start with a disabled send queue, client.send_queue().set_enabled(false).await; @@ -673,7 +679,8 @@ async fn test_cancellation() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -789,7 +796,8 @@ async fn test_edit() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -833,7 +841,6 @@ async fn test_edit() { // The /event endpoint is used to retrieve the original event, during creation // of the edit event. - let client = mock.client(); mock.mock_room_event() .room(room_id) .ok(EventFactory::new() @@ -893,7 +900,8 @@ async fn test_edit_with_poll_start() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -938,7 +946,6 @@ async fn test_edit_with_poll_start() { // The /event endpoint is used to retrieve the original event, during creation // of the edit event. - let client = mock.client(); mock.mock_room_event() .ok(EventFactory::new() .poll_start("poll_start", "question", vec!["Answer A"]) @@ -1021,7 +1028,8 @@ async fn test_edit_while_being_sent_and_fails() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1101,7 +1109,8 @@ async fn test_edit_wakes_the_sending_task() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1150,9 +1159,9 @@ async fn test_abort_after_disable() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; - let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1208,10 +1217,10 @@ async fn test_abort_or_edit_after_send() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; // Start with an enabled sending queue. - let client = mock.client(); client.send_queue().set_enabled(true).await; let q = room.send_queue(); @@ -1251,7 +1260,8 @@ async fn test_abort_while_being_sent_and_fails() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1319,9 +1329,9 @@ async fn test_unrecoverable_errors() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; - let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1381,9 +1391,9 @@ async fn test_unwedge_unrecoverable_errors() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; - let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1447,11 +1457,11 @@ async fn test_no_network_access_error_is_recoverable() { // which is effectively dropped upon `drop()`. let server = wiremock::MockServer::builder().start().await; let client = logged_in_client(Some(server.uri().to_string())).await; - let mock = MatrixMockServer::from_parts(server, client.clone()); + let mock = MatrixMockServer::from_server(server); // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let room = mock.sync_joined_room(&client, room_id).await; // Dropping the server: any subsequent attempt to connect mimics an unreachable // server, which might be caused by missing network. @@ -1511,11 +1521,11 @@ async fn test_reloading_rooms_with_unsent_events() { .unwrap(); set_client_session(&client).await; - let mock = MatrixMockServer::from_parts(server, client.clone()); + let mock = MatrixMockServer::from_server(server); // Mark two rooms as joined. - let room = mock.sync_joined_room(room_id).await; - let room2 = mock.sync_joined_room(room_id2).await; + let room = mock.sync_joined_room(&client, room_id).await; + let room2 = mock.sync_joined_room(&client, room_id2).await; // Globally disable the send queue. let q = client.send_queue(); @@ -1581,7 +1591,8 @@ async fn test_reactions() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1703,8 +1714,8 @@ async fn test_media_uploads() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock.sync_joined_room(room_id).await; + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1798,7 +1809,6 @@ async fn test_media_uploads() { assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); // The media is immediately available from the cache. - let client = mock.client(); let file_media = client .media() .get_media_content( From 7c600fddf00abc4cdc2a1f434664a09b89113ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 8 Nov 2024 09:16:32 +0100 Subject: [PATCH 480/979] refactor(ffi): Improve `is_room_alias_format_valid` so it's more strict. Previously this only used the Ruma checks, which only handled the initial `#` char and the domain part. With these changes, the name part is also validated, checking it's lowercase, with no whitespaces and containing only allowed chars, similar to what `DisplayName::to_room_alias_name` does. Moved the code to the SDK crate so it can be properly tested. --- bindings/matrix-sdk-ffi/src/room_alias.rs | 9 ++- crates/matrix-sdk/src/utils.rs | 80 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_alias.rs b/bindings/matrix-sdk-ffi/src/room_alias.rs index 491820c5799..1ab5e5311c3 100644 --- a/bindings/matrix-sdk-ffi/src/room_alias.rs +++ b/bindings/matrix-sdk-ffi/src/room_alias.rs @@ -1,10 +1,13 @@ use matrix_sdk::RoomDisplayName; -use ruma::RoomAliasId; -/// Verifies the passed `String` matches the expected room alias format. +/// Verifies the passed `String` matches the expected room alias format: +/// +/// This means it's lowercase, with no whitespace chars, has a single leading +/// `#` char and a single `:` separator between the local and domain parts, and +/// the local part only contains characters that can't be percent encoded. #[matrix_sdk_ffi_macros::export] fn is_room_alias_format_valid(alias: String) -> bool { - RoomAliasId::parse(alias).is_ok() + matrix_sdk::utils::is_room_alias_format_valid(alias) } /// Transforms a Room's display name into a valid room alias name. diff --git a/crates/matrix-sdk/src/utils.rs b/crates/matrix-sdk/src/utils.rs index bef0b63b042..5c1a83ee57b 100644 --- a/crates/matrix-sdk/src/utils.rs +++ b/crates/matrix-sdk/src/utils.rs @@ -24,6 +24,7 @@ use futures_util::StreamExt; use ruma::{ events::{AnyMessageLikeEventContent, AnyStateEventContent}, serde::Raw, + RoomAliasId, }; use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; #[cfg(feature = "e2e-encryption")] @@ -190,8 +191,37 @@ impl IntoRawStateEventContent for &Box { } } +const INVALID_ROOM_ALIAS_NAME_CHARS: &str = "#,:"; + +/// Verifies the passed `String` matches the expected room alias format: +/// +/// This means it's lowercase, with no whitespace chars, has a single leading +/// `#` char and a single `:` separator between the local and domain parts, and +/// the local part only contains characters that can't be percent encoded. +pub fn is_room_alias_format_valid(alias: String) -> bool { + let alias_parts: Vec<&str> = alias.split(':').collect(); + if alias_parts.len() != 2 { + return false; + } + + let local_part = alias_parts[0]; + let has_valid_format = local_part.chars().skip(1).all(|c| { + c.is_ascii() + && !c.is_whitespace() + && !c.is_control() + && !INVALID_ROOM_ALIAS_NAME_CHARS.contains(c) + }); + + let is_lowercase = alias.to_lowercase() == alias; + + // Checks both the local part and the domain part + has_valid_format && is_lowercase && RoomAliasId::parse(alias).is_ok() +} + #[cfg(test)] mod test { + use crate::utils::is_room_alias_format_valid; + #[cfg(feature = "e2e-encryption")] #[test] fn test_channel_observable_get_set() { @@ -202,4 +232,54 @@ mod test { assert_eq!(observable.set(10), 1); assert_eq!(observable.get(), 10); } + + #[test] + fn test_is_room_alias_format_valid_when_it_has_no_leading_hash_char_is_not_valid() { + assert!(!is_room_alias_format_valid("alias:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_it_has_several_colon_chars_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias:something:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_it_has_no_colon_chars_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias.domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_server_part_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias:".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_has_whitespace_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias with whitespace:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_has_control_char_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias\u{0009}:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_has_invalid_char_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias,test:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_is_not_lowercase_is_not_valid() { + assert!(!is_room_alias_format_valid("#Alias:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_server_part_is_not_lowercase_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias:Domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_has_valid_format() { + assert!(is_room_alias_format_valid("#alias.test:domain.org".to_owned())) + } } From 46232ee2c1fbad4b3836e7d231eb5d66bd2fd6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 8 Nov 2024 10:24:12 +0100 Subject: [PATCH 481/979] fix(sdk): add more invalid characters for room aliases --- crates/matrix-sdk-base/src/rooms/mod.rs | 4 ++-- crates/matrix-sdk/src/utils.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index b0aafcb87ce..cd884e9c898 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -66,7 +66,7 @@ pub enum RoomDisplayName { } const WHITESPACE_REGEX: &str = r"\s+"; -const INVALID_SYMBOLS_REGEX: &str = r"[#,:]+"; +const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+"; impl RoomDisplayName { /// Transforms the current display name into the name part of a @@ -636,7 +636,7 @@ mod tests { fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() { assert_eq!( "roomalias", - RoomDisplayName::Named("#Room,Alias:".to_owned()).to_room_alias_name() + RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name() ); } } diff --git a/crates/matrix-sdk/src/utils.rs b/crates/matrix-sdk/src/utils.rs index 5c1a83ee57b..327c0410176 100644 --- a/crates/matrix-sdk/src/utils.rs +++ b/crates/matrix-sdk/src/utils.rs @@ -191,7 +191,7 @@ impl IntoRawStateEventContent for &Box { } } -const INVALID_ROOM_ALIAS_NAME_CHARS: &str = "#,:"; +const INVALID_ROOM_ALIAS_NAME_CHARS: &str = "#,:{}\\"; /// Verifies the passed `String` matches the expected room alias format: /// @@ -265,7 +265,7 @@ mod test { #[test] fn test_is_room_alias_format_valid_when_name_part_has_invalid_char_is_not_valid() { - assert!(!is_room_alias_format_valid("#alias,test:domain.org".to_owned())) + assert!(!is_room_alias_format_valid("#a#lias,{t\\est}:domain.org".to_owned())) } #[test] From 26bee1cc385b2b6155c083ba809482405502ffa0 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 8 Nov 2024 12:11:19 +0100 Subject: [PATCH 482/979] doc: start an architecture document with a high-level description of the crates --- ARCHITECTURE.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000000..1510aea38a5 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,119 @@ +# Architecture + +The SDK is split into multiple layers: + +``` + WASM (external crate matrix-rust-sdk-crypto-wasm) + / + / uniffi + / / + / bindings (matrix-sdk-ffi) + crypto | +bindings | + | | + | UI (matrix-sdk-ui) + | \ + | \ + | main (matrix-sdk) + | / / +crypto / + \ / + store (matrix-sdk-base, + all the store impls) + | + common (matrix-sdk-common) +``` + +Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as +`MemoryStore` which is defined in `matrix-sdk-base`. + +## `crates/matrix-sdk` + +This is the main crate, and one that is expected to be used by most consumers. Notable data types +include: + +- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running + sync, etc. +- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and + allows running queries that are room-specific, notably sending events. + +## `crates/matrix-sdk-base` + +A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O +happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing +storage backends, as well as dummy in-memory implementations of these traits. + +## `crates/matrix-sdk-common` + +Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own +crates (the only crate it's using is test helpers). + +## `crates/matrix-sdk-crypto` + +A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix +clients. It defines a `CryptoStore` trait representing storage backends that will perform the +actual storage I/O later, as well as a dummy in-memory implementation of this trait. + +## `crates/matrix-sdk-indexeddb` + +Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a +indexeddb backend (for use in Web browsers, via WebAssembly). + +## `crates/matrix-sdk-qrcode` + +Implementation of QR codes for interactive verifications, used in the crypto crate. + +## `crates/matrix-sdk-sqlite` + +Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a +SQLite backend. + +## `crates/matrix-sdk-store-encryption` + +Low-level primitives for encrypting/decrypting/hashing values. Store implementations that +implement encryption at rest can use those primitives. + +## `crates/matrix-sdk-ui` + +Very high-level primitives implementing the best practices and cutting-edge Matrix tech: + +- `EncryptionService`: a specialized service running simplified sliding sync (MSC4186) for + everything related to crypto and E2EE for the current `Client`. +- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for + retrieving the list of current rooms, and exposing its entries. +- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting + down. +- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events + (aggregations) into single timeline items. + +## `bindings/matrix-sdk-crypto-ffi/` + +FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use +`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy +Element apps, as of 2024-11-07. + +## `bindings/matrix-sdk-ffi/` + +FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with +[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like +Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07. + +## `bindings/matrix-sdk-ffi-macros/` + +Macros used in `bindings/matrix-sdk-ffi`. + +## `testing/matrix-sdk-test/` + +Common test helpers, used by all the other crates. + +## `testing/matrix-sdk-test-macros/` + +Implementation of the `#[async_test]` test macro. + +## `testing/matrix-sdk-integration-testing/` + +Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose +setup is provided to ease running the tests, and it is compatible for running with Podman too. + +# Inspiration + +This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html). From ab61077a8ba018404c5d2a7605d1dfbfd0b9f1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 6 Nov 2024 19:07:41 +0100 Subject: [PATCH 483/979] fix(ffi): match the right status code in `Client::is_room_alias_available` --- bindings/matrix-sdk-ffi/src/client.rs | 21 +++----- crates/matrix-sdk/src/client/mod.rs | 66 +++++++++++++++++++++-- crates/matrix-sdk/src/test_utils/mocks.rs | 39 ++++++++++++++ 3 files changed, 110 insertions(+), 16 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index f58c38ab246..d46a9108180 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1130,21 +1130,16 @@ impl Client { Ok(()) } - /// Checks if a room alias is available in the current homeserver. + /// Checks if a room alias is not in use yet. + /// + /// Returns: + /// - `Ok(true)` if the room alias is available. + /// - `Ok(false)` if it's not (the resolve alias request returned a `404` + /// status code). + /// - An `Err` otherwise. pub async fn is_room_alias_available(&self, alias: String) -> Result { let alias = RoomAliasId::parse(alias)?; - match self.inner.resolve_room_alias(&alias).await { - // The room alias was resolved, so it's already in use. - Ok(_) => Ok(false), - Err(HttpError::Reqwest(error)) => { - match error.status() { - // The room alias wasn't found, so it's available. - Some(StatusCode::NOT_FOUND) => Ok(true), - _ => Err(HttpError::Reqwest(error).into()), - } - } - Err(error) => Err(error.into()), - } + self.inner.is_room_alias_available(&alias).await.map_err(Into::into) } } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index e9f0eb763cb..900c9fe22f7 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -52,6 +52,7 @@ use ruma::{ get_capabilities::{self, Capabilities}, get_supported_versions, }, + error::ErrorKind, filter::{create_filter::v3::Request as FilterUploadRequest, FilterDefinition}, knock::knock_room, membership::{join_room_by_id, join_room_by_id_or_alias}, @@ -1038,6 +1039,27 @@ impl Client { self.send(request, None).await } + /// Checks if a room alias is not in use yet. + /// + /// Returns: + /// - `Ok(true)` if the room alias is available. + /// - `Ok(false)` if it's not (the resolve alias request returned a `404` + /// status code). + /// - An `Err` otherwise. + pub async fn is_room_alias_available(&self, alias: &RoomAliasId) -> HttpResult { + match self.resolve_room_alias(alias).await { + // The room alias was resolved, so it's already in use. + Ok(_) => Ok(false), + Err(error) => { + match error.client_api_error_kind() { + // The room alias wasn't found, so it's available. + Some(ErrorKind::NotFound) => Ok(true), + _ => Err(error), + } + } + } + } + /// Update the homeserver from the login response well-known if needed. /// /// # Arguments @@ -2328,7 +2350,7 @@ pub(crate) mod tests { api::{client::room::create_room::v3::Request as CreateRoomRequest, MatrixVersion}, assign, events::ignored_user_list::IgnoredUserListEventContent, - owned_room_id, room_id, RoomId, ServerName, UserId, + owned_room_id, room_alias_id, room_id, RoomId, ServerName, UserId, }; use serde_json::json; use tokio::{ @@ -2346,8 +2368,8 @@ pub(crate) mod tests { client::WeakClient, config::{RequestConfig, SyncSettings}, test_utils::{ - logged_in_client, no_retry_test_client, set_client_session, test_client_builder, - test_client_builder_with_server, + logged_in_client, mocks::MatrixMockServer, no_retry_test_client, set_client_session, + test_client_builder, test_client_builder_with_server, }, Error, }; @@ -2915,4 +2937,42 @@ pub(crate) mod tests { .await .unwrap_err(); } + + #[async_test] + async fn test_is_room_alias_available_if_alias_is_not_resolved() { + let server = MatrixMockServer::new().await; + let client = logged_in_client(Some(server.server().uri())).await; + + server.mock_room_directory_resolve_alias().not_found().expect(1).mount().await; + + let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await; + assert_matches!(ret, Ok(true)); + } + + #[async_test] + async fn test_is_room_alias_available_if_alias_is_resolved() { + let server = MatrixMockServer::new().await; + let client = logged_in_client(Some(server.server().uri())).await; + + server + .mock_room_directory_resolve_alias() + .ok("!some_room_id:matrix.org", Vec::new()) + .expect(1) + .mount() + .await; + + let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await; + assert_matches!(ret, Ok(false)); + } + + #[async_test] + async fn test_is_room_alias_available_if_error_found() { + let server = MatrixMockServer::new().await; + let client = logged_in_client(Some(server.server().uri())).await; + + server.mock_room_directory_resolve_alias().error500().expect(1).mount().await; + + let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await; + assert_matches!(ret, Err(_)); + } } diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index b03f482bc7d..cee3deeee9b 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -201,6 +201,13 @@ impl MatrixMockServer { .and(header("authorization", "Bearer 1234")); MockUpload { mock, server: &self.server } } + + /// Create a prebuilt mock for resolving room aliases. + pub fn mock_room_directory_resolve_alias(&self) -> MockResolveRoomAlias<'_> { + let mock = + Mock::given(method("GET")).and(path_regex(r"/_matrix/client/r0/directory/room/.*")); + MockResolveRoomAlias { mock, server: &self.server } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -511,3 +518,35 @@ impl<'a> MockUpload<'a> { MatrixMock { mock: self.mock.respond_with(func), server: self.server } } } + +/// A prebuilt mock for resolving a room alias. +pub struct MockResolveRoomAlias<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockResolveRoomAlias<'a> { + /// Returns a data endpoint with a resolved room alias. + pub fn ok(self, room_id: &str, servers: Vec) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "room_id": room_id, + "servers": servers, + }))); + MatrixMock { server: self.server, mock } + } + + /// Returns a data endpoint for a room alias that does not exit. + pub fn not_found(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "Room alias not found." + }))); + MatrixMock { server: self.server, mock } + } + + /// Returns a data endpoint with a server error. + pub fn error500(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(500)); + MatrixMock { server: self.server, mock } + } +} From b8a61cfc17bba3d327913cb225ed38e263b2b13f Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:21:35 +0100 Subject: [PATCH 484/979] feat(WidgetDriver): Support widget redacts (#3987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog: Implement proper redact handling in the widget driver. This allows the Rust SDK widget driver to support widgets that rely on redacting. Co-authored-by: Damir Jelić --- crates/matrix-sdk/src/widget/machine/mod.rs | 14 ++- .../src/widget/machine/tests/error.rs | 19 +++- crates/matrix-sdk/src/widget/matrix.rs | 21 +++- crates/matrix-sdk/tests/integration/widget.rs | 96 ++++++++++++------- 4 files changed, 101 insertions(+), 49 deletions(-) diff --git a/crates/matrix-sdk/src/widget/machine/mod.rs b/crates/matrix-sdk/src/widget/machine/mod.rs index 31e96fc7da9..eb0c521878e 100644 --- a/crates/matrix-sdk/src/widget/machine/mod.rs +++ b/crates/matrix-sdk/src/widget/machine/mod.rs @@ -295,7 +295,10 @@ impl WidgetMachine { ReadEventRequest::ReadMessageLikeEvent { event_type, limit } => { let filter_fn = |f: &EventFilter| f.matches_message_like_event_type(&event_type); if !capabilities.read.iter().any(filter_fn) { - return Some(self.send_from_widget_error_response(raw_request, "Not allowed")); + return Some(self.send_from_widget_error_response( + raw_request, + "Not allowed to read message like event", + )); } const DEFAULT_EVENT_LIMIT: u32 = 50; @@ -345,7 +348,10 @@ impl WidgetMachine { }); action } else { - Some(self.send_from_widget_error_response(raw_request, "Not allowed")) + Some(self.send_from_widget_error_response( + raw_request, + "Not allowed to read state event", + )) } } } @@ -381,7 +387,9 @@ impl WidgetMachine { )); } if !capabilities.send.iter().any(|filter| filter.matches(&filter_in)) { - return Some(self.send_from_widget_error_response(raw_request, "Not allowed")); + return Some( + self.send_from_widget_error_response(raw_request, "Not allowed to send event"), + ); } let (request, action) = self.send_matrix_driver_request(request); diff --git a/crates/matrix-sdk/src/widget/machine/tests/error.rs b/crates/matrix-sdk/src/widget/machine/tests/error.rs index 5db05727b33..ccce797d478 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/error.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/error.rs @@ -98,7 +98,10 @@ fn read_request_for_non_allowed_message_like_events() { assert_eq!(request_id, "get-me-some-messages"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "org.matrix.msc2876.read_events"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!( + msg["response"]["error"]["message"].as_str().unwrap(), + "Not allowed to read message like event" + ); } #[test] @@ -124,7 +127,10 @@ fn read_request_for_non_allowed_state_events() { assert_eq!(request_id, "get-me-some-messages"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "org.matrix.msc2876.read_events"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!( + msg["response"]["error"]["message"].as_str().unwrap(), + "Not allowed to read state event" + ); } #[test] @@ -156,7 +162,7 @@ fn send_request_for_non_allowed_state_events() { assert_eq!(request_id, "send-me-a-message"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "send_event"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed to send event"); } #[test] @@ -188,7 +194,7 @@ fn send_request_for_non_allowed_message_like_events() { assert_eq!(request_id, "send-me-a-message"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "send_event"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed to send event"); } #[test] @@ -218,5 +224,8 @@ fn read_request_for_message_like_with_disallowed_msg_type_fails() { assert_eq!(request_id, "get-me-some-messages"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "org.matrix.msc2876.read_events"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!( + msg["response"]["error"]["message"].as_str().unwrap(), + "Not allowed to read message like event" + ); } diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index 96a40d423aa..e30e342d60e 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -29,10 +29,10 @@ use ruma::{ AnyMessageLikeEventContent, AnyStateEventContent, AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType, TimelineEventType, }, - serde::Raw, - RoomId, TransactionId, + serde::{from_raw_json_value, Raw}, + EventId, RoomId, TransactionId, }; -use serde_json::value::RawValue as RawJsonValue; +use serde_json::{value::RawValue as RawJsonValue, Value}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::error; @@ -107,7 +107,11 @@ impl MatrixDriver { Ok(events) } - /// Sends a given `event` to the room. + /// Sends the given `event` to the room. + /// + /// This method allows the widget machine to handle widget requests by + /// providing a unified, high-level widget-specific API for sending events + /// to the room. pub(crate) async fn send( &self, event_type: TimelineEventType, @@ -116,6 +120,15 @@ impl MatrixDriver { delayed_event_parameters: Option, ) -> Result { let type_str = event_type.to_string(); + + if let Some(redacts) = from_raw_json_value::(&content) + .ok() + .and_then(|b| b["redacts"].as_str().and_then(|s| EventId::parse(s).ok())) + { + return Ok(SendEventResponse::from_event_id( + self.room.redact(&redacts, None, None).await?.event_id, + )); + } Ok(match (state_key, delayed_event_parameters) { (None, None) => SendEventResponse::from_event_id( self.room.send_raw(&type_str, content).await?.event_id, diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index 72d29fcbd04..909880dcd40 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -19,6 +19,7 @@ use async_trait::async_trait; use futures_util::FutureExt; use matrix_sdk::{ config::SyncSettings, + test_utils::mocks::MatrixMockServer, widget::{ Capabilities, CapabilitiesProvider, WidgetDriver, WidgetDriverHandle, WidgetSettings, }, @@ -26,11 +27,11 @@ use matrix_sdk::{ }; use matrix_sdk_common::{executor::spawn, timeout::timeout}; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, - ALICE, BOB, + async_test, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, }; use once_cell::sync::Lazy; use ruma::{ + event_id, events::room::{ member::{MembershipState, RoomMemberEventContent}, message::RoomMessageEventContent, @@ -46,10 +47,10 @@ use serde_json::{json, Value as JsonValue}; use tracing::error; use wiremock::{ matchers::{header, method, path_regex, query_param}, - Mock, MockServer, ResponseTemplate, + Mock, ResponseTemplate, }; -use crate::{logged_in_client_with_server, mock_sync}; +use crate::mock_sync; /// Create a JSON string from a [`json!`][serde_json::json] "literal". #[macro_export] @@ -60,7 +61,9 @@ macro_rules! json_string { const WIDGET_ID: &str = "test-widget"; static ROOM_ID: Lazy = Lazy::new(|| owned_room_id!("!a98sd12bjh:example.org")); -async fn run_test_driver(init_on_content_load: bool) -> (Client, MockServer, WidgetDriverHandle) { +async fn run_test_driver( + init_on_content_load: bool, +) -> (Client, MatrixMockServer, WidgetDriverHandle) { struct DummyCapabilitiesProvider; #[async_trait] @@ -70,20 +73,11 @@ async fn run_test_driver(init_on_content_load: bool) -> (Client, MockServer, Wid capabilities } } + let mock_server = MatrixMockServer::new().await; + let client = mock_server.make_client().await; - let (client, mock_server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(&ROOM_ID)); - - mock_sync(&mock_server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - mock_server.reset().await; - - mock_encryption_state(&mock_server, false).await; - - let room = client.get_room(&ROOM_ID).unwrap(); + let room = mock_server.sync_joined_room(&client, &ROOM_ID).await; + mock_server.mock_room_state_encryption().plain().mount().await; let (driver, handle) = WidgetDriver::new( WidgetSettings::new(WIDGET_ID.to_owned(), init_on_content_load, "https://foo.bar/widget") @@ -257,7 +251,7 @@ async fn test_read_messages() { .and(query_param("limit", "2")) .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; // Ask the driver to read messages @@ -282,8 +276,6 @@ async fn test_read_messages() { let first_event = &events[0]; assert_eq!(first_event["content"]["body"], "hello"); } - - mock_server.verify().await; } #[async_test] @@ -354,7 +346,7 @@ async fn test_read_messages_with_msgtype_capabilities() { .and(query_param("limit", "3")) .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; // Ask the driver to read messages @@ -379,8 +371,6 @@ async fn test_read_messages_with_msgtype_capabilities() { let first_event = &events[0]; assert_eq!(first_event["content"]["body"], "hello"); } - - mock_server.verify().await; } #[async_test] @@ -488,7 +478,7 @@ async fn test_receive_live_events() { )), ); - mock_sync(&mock_server, sync_builder.build_json_sync_response(), None).await; + mock_sync(mock_server.server(), sync_builder.build_json_sync_response(), None).await; let _response = client.sync_once(SyncSettings::new().timeout(Duration::from_millis(3000))).await.unwrap(); @@ -531,7 +521,7 @@ async fn test_send_room_message() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/m.room.message/.*$")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -556,7 +546,6 @@ async fn test_send_room_message() { assert_eq!(event_id, "$foobar"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -573,7 +562,7 @@ async fn test_send_room_name() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.name/?$")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -598,7 +587,6 @@ async fn test_send_room_name() { assert_eq!(event_id, "$foobar"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -620,7 +608,7 @@ async fn test_send_delayed_message_event() { "delay_id": "1234", }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -646,7 +634,6 @@ async fn test_send_delayed_message_event() { assert_eq!(delay_id, "1234"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -668,7 +655,7 @@ async fn test_send_delayed_state_event() { "delay_id": "1234", }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -694,7 +681,6 @@ async fn test_send_delayed_state_event() { assert_eq!(delay_id, "1234"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -744,7 +730,7 @@ async fn test_update_delayed_event() { .and(path_regex(r"^/_matrix/client/unstable/org.matrix.msc4140/delayed_events/1234")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -764,9 +750,6 @@ async fn test_update_delayed_event() { assert_eq!(response["action"], "org.matrix.msc4157.update_delayed_event"); let empty_response = response["response"].clone(); assert_eq!(empty_response, serde_json::from_str::("{}").unwrap()); - - // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -827,6 +810,45 @@ async fn test_try_update_delayed_event_without_permission_negotiate() { } } +#[async_test] +async fn test_send_redaction() { + let (_, mock_server, driver_handle) = run_test_driver(false).await; + + negotiate_capabilities( + &driver_handle, + json!([ + // "org.matrix.msc4157.send.delayed_event", + "org.matrix.msc2762.send.event:m.room.redaction" + ]), + ) + .await; + + mock_server.mock_room_redact().ok(event_id!("$redact_event_id")).mock_once().mount().await; + + send_request( + &driver_handle, + "send-redact-message", + "send_event", + json!({ + "type": "m.room.redaction", + "content": { + "redacts": "$1234" + }, + }), + ) + .await; + + // Receive the response + let msg = recv_message(&driver_handle).await; + assert_eq!(msg["api"], "fromWidget"); + assert_eq!(msg["action"], "send_event"); + let redact_event_id = msg["response"]["event_id"].as_str().unwrap(); + let redact_room_id = msg["response"]["room_id"].as_str().unwrap(); + + assert_eq!(redact_event_id, "$redact_event_id"); + assert_eq!(redact_room_id, "!a98sd12bjh:example.org"); +} + async fn negotiate_capabilities(driver_handle: &WidgetDriverHandle, caps: JsonValue) { { // Receive toWidget capabilities request From ca8c635f6291f8b0c1e905d11e8e79477001daca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 8 Nov 2024 11:57:03 +0100 Subject: [PATCH 485/979] feat(ffi): add `reason` field to `TimelineItemContent::RoomMembership` --- .../matrix-sdk-ffi/src/timeline/content.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index e39f2c0798d..0b01ef5005b 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -16,7 +16,7 @@ use std::{collections::HashMap, sync::Arc}; use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes}; use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails}; -use ruma::events::room::MediaSource; +use ruma::events::{room::MediaSource, FullStateEventContent}; use super::ProfileDetails; use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind}; @@ -49,11 +49,18 @@ impl From for TimelineItemContent TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) } } - Content::MembershipChange(membership) => TimelineItemContent::RoomMembership { - user_id: membership.user_id().to_string(), - user_display_name: membership.display_name(), - change: membership.change().map(Into::into), - }, + Content::MembershipChange(membership) => { + let reason = match membership.content() { + FullStateEventContent::Original { content, .. } => content.reason.clone(), + _ => None, + }; + TimelineItemContent::RoomMembership { + user_id: membership.user_id().to_string(), + user_display_name: membership.display_name(), + change: membership.change().map(Into::into), + reason, + } + } Content::ProfileChange(profile) => { let (display_name, prev_display_name) = profile @@ -161,6 +168,7 @@ pub enum TimelineItemContent { user_id: String, user_display_name: Option, change: Option, + reason: Option, }, ProfileChange { display_name: Option, From 204e6e4ca053055c8de32734c4926899178d383e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 8 Nov 2024 16:17:44 +0100 Subject: [PATCH 486/979] feat(sliding_sync): Add `m.room.join_rules` to the required state We need the join rules state event to prevent the SDK from assuming a room with an unknown (as in, not loaded) join rule is public. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 1 + crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 5a52f610757..cde34078ce9 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -88,6 +88,7 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[ (StateEventType::RoomCanonicalAlias, ""), (StateEventType::RoomPowerLevels, ""), (StateEventType::CallMember, "*"), + (StateEventType::RoomJoinRules, ""), ]; /// The default `required_state` constant value for sliding sync room 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 0c9429203e3..c8b150f7b51 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -357,6 +357,7 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], ["org.matrix.msc3401.call.member", "*"], + ["m.room.join_rules", ""], ], "include_heroes": true, "filters": { @@ -2220,6 +2221,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], ["org.matrix.msc3401.call.member", "*"], + ["m.room.join_rules", ""], ["m.room.create", ""], ["m.room.pinned_events", ""], ], @@ -2258,6 +2260,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["m.room.canonical_alias", ""], ["m.room.power_levels", ""], ["org.matrix.msc3401.call.member", "*"], + ["m.room.join_rules", ""], ["m.room.create", ""], ["m.room.pinned_events", ""], ], From f483f3557394463332f1ffa15d687cccdbade402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 11 Nov 2024 10:11:01 +0100 Subject: [PATCH 487/979] chore: Allow backoff to be used in the cargo-deny config Backoff seems to be unmaintained, there's no drop-in replacement so let's silence the warning for now. --- .deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.deny.toml b/.deny.toml index 7c9a53f6320..5b60ee4be2f 100644 --- a/.deny.toml +++ b/.deny.toml @@ -10,6 +10,7 @@ exclude = [ version = 2 ignore = [ { id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" }, + { id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." }, ] [licenses] From 53900294d07e5d21dd517b156f0823380a1edb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 11 Nov 2024 12:11:45 +0100 Subject: [PATCH 488/979] feat(room_alias): Add `create_room_alias` function This associates a room alias with an existing room through its room id. --- crates/matrix-sdk/src/client/mod.rs | 25 ++++++++++++++++++++++- crates/matrix-sdk/src/test_utils/mocks.rs | 21 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 900c9fe22f7..12eb9fb88a3 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -45,7 +45,7 @@ use ruma::{ api::{ client::{ account::whoami, - alias::get_alias, + alias::{create_alias, get_alias}, device::{delete_devices, get_devices, update_device}, directory::{get_public_rooms, get_public_rooms_filtered}, discovery::{ @@ -1060,6 +1060,13 @@ impl Client { } } + /// Creates a new room alias associated with a room. + pub async fn create_room_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> HttpResult<()> { + let request = create_alias::v3::Request::new(alias.to_owned(), room_id.to_owned()); + self.send(request, None).await?; + Ok(()) + } + /// Update the homeserver from the login response well-known if needed. /// /// # Arguments @@ -2975,4 +2982,20 @@ pub(crate) mod tests { let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await; assert_matches!(ret, Err(_)); } + + #[async_test] + async fn test_create_room_alias() { + let server = MatrixMockServer::new().await; + let client = logged_in_client(Some(server.server().uri())).await; + + server.mock_create_room_alias().ok().expect(1).mount().await; + + let ret = client + .create_room_alias( + room_alias_id!("#some_alias:matrix.org"), + room_id!("!some_room:matrix.org"), + ) + .await; + assert_matches!(ret, Ok(())); + } } diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index cee3deeee9b..35f6970e2af 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -208,6 +208,13 @@ impl MatrixMockServer { Mock::given(method("GET")).and(path_regex(r"/_matrix/client/r0/directory/room/.*")); MockResolveRoomAlias { mock, server: &self.server } } + + /// Create a prebuilt mock for creating room aliases. + pub fn mock_create_room_alias(&self) -> MockCreateRoomAlias<'_> { + let mock = + Mock::given(method("PUT")).and(path_regex(r"/_matrix/client/r0/directory/room/.*")); + MockCreateRoomAlias { mock, server: &self.server } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -550,3 +557,17 @@ impl<'a> MockResolveRoomAlias<'a> { MatrixMock { server: self.server, mock } } } + +/// A prebuilt mock for creating a room alias. +pub struct MockCreateRoomAlias<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockCreateRoomAlias<'a> { + /// Returns a data endpoint for creating a room alias. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + MatrixMock { server: self.server, mock } + } +} From 57e78dd22b42bcc99ab8fc3c0ab90882d25add05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 11 Nov 2024 12:16:01 +0100 Subject: [PATCH 489/979] feat(ffi): Add `Client::create_room_alias` function --- bindings/matrix-sdk-ffi/src/client.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index d46a9108180..e93067bcc2b 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1141,6 +1141,17 @@ impl Client { let alias = RoomAliasId::parse(alias)?; self.inner.is_room_alias_available(&alias).await.map_err(Into::into) } + + /// Creates a new room alias associated with the provided room id. + pub async fn create_room_alias( + &self, + room_alias: String, + room_id: String, + ) -> Result<(), ClientError> { + let room_alias = RoomAliasId::parse(room_alias)?; + let room_id = RoomId::parse(room_id)?; + self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into) + } } #[matrix_sdk_ffi_macros::export(callback_interface)] From 031a96200b16d281ce479734045c07b20160b9ef Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 6 Nov 2024 14:24:03 +0100 Subject: [PATCH 490/979] feat(sdk): Add `Client::cross_proces_store_locks_holder_name()`. This patch adds `ClientInner::cross_process_store_locks_holder_name` and its public method `Client::cross_process_store_locks_holder_name`. This patch also adds `ClientBuilder::cross_process_store_locks_holider_name` to configure this value. --- crates/matrix-sdk/src/client/builder/mod.rs | 40 +++++++++++++++++++++ crates/matrix-sdk/src/client/mod.rs | 24 +++++++++++++ 2 files changed, 64 insertions(+) diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 078a08f0f2a..37839bc5baf 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -101,6 +101,7 @@ pub struct ClientBuilder { room_key_recipient_strategy: CollectStrategy, #[cfg(feature = "e2e-encryption")] decryption_trust_requirement: TrustRequirement, + cross_process_store_locks_holder_name: String, } impl ClientBuilder { @@ -122,6 +123,7 @@ impl ClientBuilder { room_key_recipient_strategy: Default::default(), #[cfg(feature = "e2e-encryption")] decryption_trust_requirement: TrustRequirement::Untrusted, + cross_process_store_locks_holder_name: "main".to_owned(), } } @@ -424,6 +426,20 @@ impl ClientBuilder { self } + /// Set the cross-process store locks holder name. + /// + /// The SDK provides cross-process store locks (see + /// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The + /// `holder_name` will be the value used for all cross-process store locks + /// used by the `Client` being built. + /// + /// If 2 concurrent `Client`s are running in 2 different process, this + /// method must be called with different `hold_name` values. + pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self { + self.cross_process_store_locks_holder_name = holder_name; + self + } + /// Create a [`Client`] with the options set on this builder. /// /// # Errors @@ -529,6 +545,7 @@ impl ClientBuilder { send_queue, #[cfg(feature = "e2e-encryption")] self.encryption_settings, + self.cross_process_store_locks_holder_name, ) .await; @@ -1129,4 +1146,27 @@ pub(crate) mod tests { object }) } + + #[async_test] + async fn test_cross_process_lock_stores_holder_name() { + { + let homeserver = make_mock_homeserver().await; + let client = + ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap(); + + assert_eq!(client.cross_process_lock_stores_holder_name(), "main"); + } + + { + let homeserver = make_mock_homeserver().await; + let client = ClientBuilder::new() + .homeserver_url(homeserver.uri()) + .cross_process_store_locks_holder_name("foo".to_owned()) + .build() + .await + .unwrap(); + + assert_eq!(client.cross_process_lock_stores_holder_name(), "foo"); + } + } } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 12eb9fb88a3..ef7e357841a 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -275,6 +275,17 @@ pub(crate) struct ClientInner { /// deduplicate multiple calls to a method. pub(crate) locks: ClientLocks, + /// The cross-process store locks holder name. + /// + /// The SDK provides cross-process store locks (see + /// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The + /// `holder_name` is the value used for all cross-process store locks + /// used by this `Client`. + /// + /// If multiple `Client`s are running in different processes, this + /// value MUST be different for each `Client`. + cross_process_store_locks_holder_name: String, + /// A mapping of the times at which the current user sent typing notices, /// keyed by room. pub(crate) typing_notice_times: StdRwLock>, @@ -341,6 +352,7 @@ impl ClientInner { event_cache: OnceCell, send_queue: Arc, #[cfg(feature = "e2e-encryption")] encryption_settings: EncryptionSettings, + cross_process_store_locks_holder_name: String, ) -> Arc { let client = Self { server, @@ -351,6 +363,7 @@ impl ClientInner { http_client, base_client, locks: Default::default(), + cross_process_store_locks_holder_name, server_capabilities: RwLock::new(server_capabilities), typing_notice_times: Default::default(), event_handlers: Default::default(), @@ -425,6 +438,16 @@ impl Client { &self.inner.locks } + /// The cross-process store locks holder name. + /// + /// The SDK provides cross-process store locks (see + /// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The + /// `holder_name` is the value used for all cross-process store locks + /// used by this `Client`. + pub fn cross_process_store_locks_holder_name(&self) -> &str { + &self.inner.cross_process_store_locks_holder_name + } + /// Change the homeserver URL used by this client. /// /// # Arguments @@ -2246,6 +2269,7 @@ impl Client { self.inner.send_queue_data.clone(), #[cfg(feature = "e2e-encryption")] self.inner.e2ee.encryption_settings, + self.inner.cross_process_store_locks_holder_name.clone(), ) .await, }; From 90b8ba3c2e055e851b7f7d8d8c1a2eb769886570 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 6 Nov 2024 15:27:24 +0100 Subject: [PATCH 491/979] feat: `Client::cross_process_store_locks_holder_name` is used everywhere. See the Changelog Section to get the details. Changelog: `Client::cross_process_store_locks_holder_name` is used everywhere: - `StoreConfig::new()` now takes a `cross_process_store_locks_holder_name` argument. - `StoreConfig` no longer implements `Default`. - `BaseClient::new()` has been removed. - `BaseClient::clone_with_in_memory_state_store()` now takes a `cross_process_store_locks_holder_name` argument. - `BaseClient` no longer implements `Default`. - `EventCacheStoreLock::new()` no longer takes a `key` argument. - `BuilderStoreConfig` no longer has `cross_process_store_locks_holder_name` field for `Sqlite` and `IndexedDb`. --- benchmarks/benches/room_bench.rs | 5 +- benchmarks/benches/store_bench.rs | 10 ++- crates/matrix-sdk-base/src/client.rs | 44 ++++++------ .../src/event_cache_store/mod.rs | 7 +- crates/matrix-sdk-base/src/rooms/normal.rs | 14 ++-- crates/matrix-sdk-base/src/store/mod.rs | 30 ++++---- crates/matrix-sdk-base/src/test_utils.rs | 6 +- crates/matrix-sdk/src/client/builder/mod.rs | 69 ++++++++++--------- crates/matrix-sdk/src/client/mod.rs | 17 ++++- crates/matrix-sdk/tests/integration/client.rs | 3 +- .../tests/integration/send_queue.rs | 9 ++- labs/multiverse/src/main.rs | 2 +- 12 files changed, 128 insertions(+), 88 deletions(-) diff --git a/benchmarks/benches/room_bench.rs b/benchmarks/benches/room_bench.rs index 2283cc955ab..57342857285 100644 --- a/benchmarks/benches/room_bench.rs +++ b/benchmarks/benches/room_bench.rs @@ -74,7 +74,10 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) { .block_on(sqlite_store.save_changes(&changes)) .expect("initial filling of sqlite failed"); - let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store)); + let base_client = BaseClient::with_store_config( + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) + .state_store(sqlite_store), + ); runtime .block_on(base_client.set_session_meta( diff --git a/benchmarks/benches/store_bench.rs b/benchmarks/benches/store_bench.rs index 4e7232c0735..bc6cd8f28ef 100644 --- a/benchmarks/benches/store_bench.rs +++ b/benchmarks/benches/store_bench.rs @@ -69,7 +69,10 @@ pub fn restore_session(c: &mut Criterion) { b.to_async(&runtime).iter(|| async { let client = Client::builder() .homeserver_url("https://matrix.example.com") - .store_config(StoreConfig::new().state_store(store.clone())) + .store_config( + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) + .state_store(store.clone()), + ) .build() .await .expect("Can't build client"); @@ -96,7 +99,10 @@ pub fn restore_session(c: &mut Criterion) { b.to_async(&runtime).iter(|| async { let client = Client::builder() .homeserver_url("https://matrix.example.com") - .store_config(StoreConfig::new().state_store(store.clone())) + .store_config( + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) + .state_store(store.clone()), + ) .build() .await .expect("Can't build client"); diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index b8aa6f67b5a..f36178b56fa 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -139,11 +139,6 @@ impl fmt::Debug for BaseClient { } impl BaseClient { - /// Create a new default client. - pub fn new() -> Self { - BaseClient::with_store_config(StoreConfig::default()) - } - /// Create a new client. /// /// # Arguments @@ -173,8 +168,12 @@ impl BaseClient { /// Clones the current base client to use the same crypto store but a /// different, in-memory store config, and resets transient state. #[cfg(feature = "e2e-encryption")] - pub async fn clone_with_in_memory_state_store(&self) -> Result { - let config = StoreConfig::new().state_store(MemoryStore::new()); + pub async fn clone_with_in_memory_state_store( + &self, + cross_process_store_locks_holder_name: &str, + ) -> Result { + let config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned()) + .state_store(MemoryStore::new()); let config = config.crypto_store(self.crypto_store.clone()); let copy = Self { @@ -207,8 +206,12 @@ impl BaseClient { /// different, in-memory store config, and resets transient state. #[cfg(not(feature = "e2e-encryption"))] #[allow(clippy::unused_async)] - pub async fn clone_with_in_memory_state_store(&self) -> Result { - let config = StoreConfig::new().state_store(MemoryStore::new()); + pub async fn clone_with_in_memory_state_store( + &self, + cross_process_store_locks_holder: &str, + ) -> Result { + let config = StoreConfig::new(cross_process_store_locks_holder.to_owned()) + .state_store(MemoryStore::new()); Ok(Self::with_store_config(config)) } @@ -1689,12 +1692,6 @@ impl BaseClient { } } -impl Default for BaseClient { - fn default() -> Self { - Self::new() - } -} - fn handle_room_member_event_for_profiles( room_id: &RoomId, event: &SyncStateEvent, @@ -1737,8 +1734,9 @@ mod tests { use super::BaseClient; use crate::{ - store::StateStoreExt, test_utils::logged_in_base_client, RoomDisplayName, RoomState, - SessionMeta, + store::{StateStoreExt, StoreConfig}, + test_utils::logged_in_base_client, + RoomDisplayName, RoomState, SessionMeta, }; #[async_test] @@ -1945,7 +1943,9 @@ mod tests { let user_id = user_id!("@alice:example.org"); let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org"); - let client = BaseClient::new(); + let client = BaseClient::with_store_config(StoreConfig::new( + "cross-process-store-locks-holder-name".to_owned(), + )); client .set_session_meta( SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() }, @@ -2003,7 +2003,9 @@ mod tests { let inviter_user_id = user_id!("@bob:example.org"); let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org"); - let client = BaseClient::new(); + let client = BaseClient::with_store_config(StoreConfig::new( + "cross-process-store-locks-holder-name".to_owned(), + )); client .set_session_meta( SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() }, @@ -2063,7 +2065,9 @@ mod tests { let inviter_user_id = user_id!("@bob:example.org"); let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org"); - let client = BaseClient::new(); + let client = BaseClient::with_store_config(StoreConfig::new( + "cross-process-store-locks-holder-name".to_owned(), + )); client .set_session_meta( SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() }, diff --git a/crates/matrix-sdk-base/src/event_cache_store/mod.rs b/crates/matrix-sdk-base/src/event_cache_store/mod.rs index ab41d06327d..76b7032c195 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache_store/mod.rs @@ -60,7 +60,10 @@ impl fmt::Debug for EventCacheStoreLock { impl EventCacheStoreLock { /// Create a new lock around the [`EventCacheStore`]. - pub fn new(store: S, key: String, holder: String) -> Self + /// + /// The `holder` argument represents the holder inside the + /// [`CrossProcessStoreLock::new`]. + pub fn new(store: S, holder: String) -> Self where S: IntoEventCacheStore, { @@ -69,7 +72,7 @@ impl EventCacheStoreLock { Self { cross_process_lock: CrossProcessStoreLock::new( LockableEventCacheStore(store.clone()), - key, + "default".to_owned(), holder, ), store, diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 49fad5cfcca..81bca1b6454 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1853,7 +1853,7 @@ mod tests { use crate::latest_event::LatestEvent; use crate::{ rooms::RoomNotableTags, - store::{IntoStateStore, MemoryStore, StateChanges, StateStore}, + store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig}, BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName, RoomInfoNotableUpdateReasons, SessionMeta, }; @@ -2141,7 +2141,9 @@ mod tests { #[async_test] async fn test_is_favourite() { // Given a room, - let client = BaseClient::new(); + let client = BaseClient::with_store_config(StoreConfig::new( + "cross-process-store-locks-holder-name".to_owned(), + )); client .set_session_meta( @@ -2219,7 +2221,9 @@ mod tests { #[async_test] async fn test_is_low_priority() { // Given a room, - let client = BaseClient::new(); + let client = BaseClient::with_store_config(StoreConfig::new( + "cross-process-store-locks-holder-name".to_owned(), + )); client .set_session_meta( @@ -2675,7 +2679,9 @@ mod tests { use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons}; // Given a room, - let client = BaseClient::new(); + let client = BaseClient::with_store_config(StoreConfig::new( + "cross-process-store-locks-holder-name".to_owned(), + )); client .set_session_meta( diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 1bc7ec70609..cfea522adda 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -483,7 +483,8 @@ impl StateChanges { /// ``` /// # use matrix_sdk_base::store::StoreConfig; /// -/// let store_config = StoreConfig::new(); +/// let store_config = +/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned()); /// ``` #[derive(Clone)] pub struct StoreConfig { @@ -491,6 +492,7 @@ pub struct StoreConfig { pub(crate) crypto_store: Arc, pub(crate) state_store: Arc, pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock, + cross_process_store_locks_holder_name: String, } #[cfg(not(tarpaulin_include))] @@ -502,17 +504,20 @@ impl fmt::Debug for StoreConfig { impl StoreConfig { /// Create a new default `StoreConfig`. + /// + /// To learn more about `cross_process_store_locks_holder_name`, please read + /// [`CrossProcessStoreLock::new`](matrix_sdk_common::store_locks::CrossProcessStoreLock::new). #[must_use] - pub fn new() -> Self { + pub fn new(cross_process_store_locks_holder_name: String) -> Self { Self { #[cfg(feature = "e2e-encryption")] crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(), state_store: Arc::new(MemoryStore::new()), event_cache_store: event_cache_store::EventCacheStoreLock::new( event_cache_store::MemoryStore::new(), - "default-key".to_owned(), - "matrix-sdk-base".to_owned(), + cross_process_store_locks_holder_name.clone(), ), + cross_process_store_locks_holder_name, } } @@ -532,21 +537,14 @@ impl StoreConfig { } /// Set a custom implementation of an `EventCacheStore`. - /// - /// The `key` and `holder` arguments represent the key and holder inside the - /// [`CrossProcessStoreLock::new`][matrix_sdk_common::store_locks::CrossProcessStoreLock::new]. - pub fn event_cache_store(mut self, event_cache_store: S, key: String, holder: String) -> Self + pub fn event_cache_store(mut self, event_cache_store: S) -> Self where S: event_cache_store::IntoEventCacheStore, { - self.event_cache_store = - event_cache_store::EventCacheStoreLock::new(event_cache_store, key, holder); + self.event_cache_store = event_cache_store::EventCacheStoreLock::new( + event_cache_store, + self.cross_process_store_locks_holder_name.clone(), + ); self } } - -impl Default for StoreConfig { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/matrix-sdk-base/src/test_utils.rs b/crates/matrix-sdk-base/src/test_utils.rs index 9ac48491ad8..d64928d3c24 100644 --- a/crates/matrix-sdk-base/src/test_utils.rs +++ b/crates/matrix-sdk-base/src/test_utils.rs @@ -18,12 +18,14 @@ use ruma::{owned_user_id, UserId}; -use crate::{BaseClient, SessionMeta}; +use crate::{store::StoreConfig, BaseClient, SessionMeta}; /// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded /// one otherwise. pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient { - let client = BaseClient::new(); + let client = BaseClient::with_store_config(StoreConfig::new( + "cross-process-store-locks-holder-name".to_owned(), + )); let user_id = user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk")); client diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 37839bc5baf..a498df9f013 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -105,13 +105,17 @@ pub struct ClientBuilder { } impl ClientBuilder { + const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main"; + pub(crate) fn new() -> Self { Self { homeserver_cfg: None, #[cfg(feature = "experimental-sliding-sync")] sliding_sync_version_builder: SlidingSyncVersionBuilder::Native, http_cfg: None, - store_config: BuilderStoreConfig::Custom(StoreConfig::default()), + store_config: BuilderStoreConfig::Custom(StoreConfig::new( + Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(), + )), request_config: Default::default(), respect_login_well_known: true, server_versions: None, @@ -123,7 +127,8 @@ impl ClientBuilder { room_key_recipient_strategy: Default::default(), #[cfg(feature = "e2e-encryption")] decryption_trust_requirement: TrustRequirement::Untrusted, - cross_process_store_locks_holder_name: "main".to_owned(), + cross_process_store_locks_holder_name: + Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(), } } @@ -210,7 +215,6 @@ impl ClientBuilder { path: path.as_ref().to_owned(), cache_path: None, passphrase: passphrase.map(ToOwned::to_owned), - event_cache_store_lock_holder: "matrix-sdk".to_owned(), }; self } @@ -228,7 +232,6 @@ impl ClientBuilder { path: path.as_ref().to_owned(), cache_path: Some(cache_path.as_ref().to_owned()), passphrase: passphrase.map(ToOwned::to_owned), - event_cache_store_lock_holder: "matrix-sdk".to_owned(), }; self } @@ -239,7 +242,6 @@ impl ClientBuilder { self.store_config = BuilderStoreConfig::IndexedDb { name: name.to_owned(), passphrase: passphrase.map(ToOwned::to_owned), - event_cache_store_lock_holder: "matrix-sdk".to_owned(), }; self } @@ -260,7 +262,9 @@ impl ClientBuilder { /// # let custom_state_store = MemoryStore::new(); /// use matrix_sdk::{config::StoreConfig, Client}; /// - /// let store_config = StoreConfig::new().state_store(custom_state_store); + /// let store_config = + /// StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) + /// .state_store(custom_state_store); /// let client_builder = Client::builder().store_config(store_config); /// ``` pub fn store_config(mut self, store_config: StoreConfig) -> Self { @@ -473,13 +477,17 @@ impl ClientBuilder { base_client } else { #[allow(unused_mut)] - let mut client = - BaseClient::with_store_config(build_store_config(self.store_config).await?); + let mut client = BaseClient::with_store_config( + build_store_config(self.store_config, &self.cross_process_store_locks_holder_name) + .await?, + ); + #[cfg(feature = "e2e-encryption")] { client.room_key_recipient_strategy = self.room_key_recipient_strategy; client.decryption_trust_requirement = self.decryption_trust_requirement; } + client }; @@ -564,20 +572,16 @@ pub fn sanitize_server_name(s: &str) -> crate::Result Result { #[allow(clippy::infallible_destructuring_match)] let store_config = match builder_config { #[cfg(feature = "sqlite")] - BuilderStoreConfig::Sqlite { - path, - cache_path, - passphrase, - event_cache_store_lock_holder, - } => { - let store_config = StoreConfig::new() + BuilderStoreConfig::Sqlite { path, cache_path, passphrase } => { + let store_config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned()) .state_store( matrix_sdk_sqlite::SqliteStateStore::open(&path, passphrase.as_deref()).await?, ) @@ -587,8 +591,6 @@ async fn build_store_config( passphrase.as_deref(), ) .await?, - "default-key".to_owned(), - event_cache_store_lock_holder, ); #[cfg(feature = "e2e-encryption")] @@ -600,11 +602,11 @@ async fn build_store_config( } #[cfg(feature = "indexeddb")] - BuilderStoreConfig::IndexedDb { name, passphrase, event_cache_store_lock_holder } => { + BuilderStoreConfig::IndexedDb { name, passphrase } => { build_indexeddb_store_config( &name, passphrase.as_deref(), - event_cache_store_lock_holder, + cross_process_store_locks_holder_name, ) .await? } @@ -620,28 +622,28 @@ async fn build_store_config( async fn build_indexeddb_store_config( name: &str, passphrase: Option<&str>, - event_cache_store_lock_holder: String, + cross_process_store_locks_holder_name: &str, ) -> Result { + let cross_process_store_locks_holder_name = cross_process_store_locks_holder_name.to_owned(); + #[cfg(feature = "e2e-encryption")] let store_config = { let (state_store, crypto_store) = matrix_sdk_indexeddb::open_stores_with_name(name, passphrase).await?; - StoreConfig::new().state_store(state_store).crypto_store(crypto_store) + StoreConfig::new(cross_process_store_locks_holder_name) + .state_store(state_store) + .crypto_store(crypto_store) }; #[cfg(not(feature = "e2e-encryption"))] let store_config = { let state_store = matrix_sdk_indexeddb::open_state_store(name, passphrase).await?; - StoreConfig::new().state_store(state_store) + StoreConfig::new(cross_process_store_locks_holder_name).state_store(state_store) }; let store_config = { tracing::warn!("The IndexedDB backend does not implement an event cache store, falling back to the in-memory event cache store…"); - store_config.event_cache_store( - matrix_sdk_base::event_cache_store::MemoryStore::new(), - "default-key".to_owned(), - event_cache_store_lock_holder, - ) + store_config.event_cache_store(matrix_sdk_base::event_cache_store::MemoryStore::new()) }; Ok(store_config) @@ -651,7 +653,7 @@ async fn build_indexeddb_store_config( async fn build_indexeddb_store_config( _name: &str, _passphrase: Option<&str>, - _event_cache_store_lock_holder: String, + _event_cache_store_lock_holder_name: &str, ) -> Result { panic!("the IndexedDB is only available on the 'wasm32' arch") } @@ -696,13 +698,11 @@ enum BuilderStoreConfig { path: std::path::PathBuf, cache_path: Option, passphrase: Option, - event_cache_store_lock_holder: String, }, #[cfg(feature = "indexeddb")] IndexedDb { name: String, passphrase: Option, - event_cache_store_lock_holder: String, }, Custom(StoreConfig), } @@ -770,6 +770,7 @@ pub(crate) mod tests { use assert_matches::assert_matches; use matrix_sdk_test::{async_test, test_json}; use serde_json::{json_internal, Value as JsonValue}; + #[cfg(feature = "experimental-sliding-sync")] use url::Url; use wiremock::{ matchers::{method, path}, @@ -1148,13 +1149,13 @@ pub(crate) mod tests { } #[async_test] - async fn test_cross_process_lock_stores_holder_name() { + async fn test_cross_process_store_locks_holder_name() { { let homeserver = make_mock_homeserver().await; let client = ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap(); - assert_eq!(client.cross_process_lock_stores_holder_name(), "main"); + assert_eq!(client.cross_process_store_locks_holder_name(), "main"); } { @@ -1166,7 +1167,7 @@ pub(crate) mod tests { .await .unwrap(); - assert_eq!(client.cross_process_lock_stores_holder_name(), "foo"); + assert_eq!(client.cross_process_store_locks_holder_name(), "foo"); } } } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index ef7e357841a..2eac8c7d9c2 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2262,7 +2262,12 @@ impl Client { #[cfg(feature = "experimental-sliding-sync")] self.sliding_sync_version(), self.inner.http_client.clone(), - self.inner.base_client.clone_with_in_memory_state_store().await?, + self.inner + .base_client + .clone_with_in_memory_state_store( + &self.inner.cross_process_store_locks_holder_name, + ) + .await?, self.inner.server_capabilities.read().await.clone(), self.inner.respect_login_well_known, self.inner.event_cache.clone(), @@ -2792,7 +2797,10 @@ pub(crate) mod tests { let memory_store = Arc::new(MemoryStore::new()); let client = Client::builder() .insecure_server_name_no_tls(server_name) - .store_config(StoreConfig::new().state_store(memory_store.clone())) + .store_config( + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) + .state_store(memory_store.clone()), + ) .build() .await .unwrap(); @@ -2811,7 +2819,10 @@ pub(crate) mod tests { let client = Client::builder() .insecure_server_name_no_tls(server_name) - .store_config(StoreConfig::new().state_store(memory_store.clone())) + .store_config( + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) + .state_store(memory_store.clone()), + ) .build() .await .unwrap(); diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 589087296a5..a8d7a9e1115 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -1366,7 +1366,8 @@ async fn test_restore_room() { store.save_changes(&changes).await.unwrap(); // Build a client with that store. - let store_config = StoreConfig::new().state_store(store); + let store_config = + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()).state_store(store); let client = Client::builder() .homeserver_url("http://localhost:1234") .request_config(RequestConfig::new().disable_retry()) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 9d18578864e..c8ee9496090 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1514,7 +1514,10 @@ async fn test_reloading_rooms_with_unsent_events() { let client = Client::builder() .homeserver_url(server.uri()) .server_versions([MatrixVersion::V1_0]) - .store_config(StoreConfig::new().state_store(store.clone())) + .store_config( + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) + .state_store(store.clone()), + ) .request_config(RequestConfig::new().disable_retry()) .build() .await @@ -1569,7 +1572,9 @@ async fn test_reloading_rooms_with_unsent_events() { let client = Client::builder() .homeserver_url(mock.server().uri()) .server_versions([MatrixVersion::V1_0]) - .store_config(StoreConfig::new().state_store(store)) + .store_config( + StoreConfig::new("cross-process-store-locks-holder-name".to_owned()).state_store(store), + ) .request_config(RequestConfig::new().disable_retry()) .build() .await diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index d88ffa92aba..4b3c1d51952 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -948,7 +948,7 @@ async fn configure_client(server_name: String, config_path: String) -> anyhow::R let config_path = PathBuf::from(config_path); let mut client_builder = Client::builder() .store_config( - StoreConfig::default() + StoreConfig::new("multiverse".to_owned()) .crypto_store( SqliteCryptoStore::open(config_path.join("crypto.sqlite"), None).await?, ) From 563c3aae3145a1839caeecb4b9d3e629f875d7a5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 6 Nov 2024 16:55:04 +0100 Subject: [PATCH 492/979] feat(ui): `EncryptionSyncService` and `Notification` are using `Client::cross_process_store_locks_holder_name`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch removes the `process_id` argument from `EncryptionSyncService::new()` and replaces it by `Client::cross_process_store_locks_holder_name`. The “process ID” is set when the `Client` is converted into another `Client` tailore for notification in `NotificationClient` with `Client::notification_client` which now has a new `cross_process_store_locks_holder_name` argument. --- bindings/matrix-sdk-ffi/src/sync_service.rs | 4 ++-- .../src/encryption_sync_service.rs | 12 +++++++----- crates/matrix-sdk-ui/src/notification_client.rs | 4 ++-- crates/matrix-sdk-ui/src/sync_service.rs | 17 ++++------------- .../integration/encryption_sync_service.rs | 17 ++++++----------- crates/matrix-sdk/src/client/mod.rs | 16 +++++++++++----- crates/matrix-sdk/src/encryption/mod.rs | 2 ++ .../src/helpers.rs | 12 ++++++++++++ .../src/tests/nse.rs | 14 +++++++++----- 9 files changed, 55 insertions(+), 43 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/sync_service.rs b/bindings/matrix-sdk-ffi/src/sync_service.rs index dd1e6b89d27..52179678ed7 100644 --- a/bindings/matrix-sdk-ffi/src/sync_service.rs +++ b/bindings/matrix-sdk-ffi/src/sync_service.rs @@ -112,9 +112,9 @@ impl SyncServiceBuilder { #[matrix_sdk_ffi_macros::export] impl SyncServiceBuilder { - pub fn with_cross_process_lock(self: Arc, app_identifier: Option) -> Arc { + pub fn with_cross_process_lock(self: Arc) -> Arc { let this = unwrap_or_clone_arc(self); - let builder = this.builder.with_cross_process_lock(app_identifier); + let builder = this.builder.with_cross_process_lock(); Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook }) } diff --git a/crates/matrix-sdk-ui/src/encryption_sync_service.rs b/crates/matrix-sdk-ui/src/encryption_sync_service.rs index 66da6e8903f..67148fc7f6b 100644 --- a/crates/matrix-sdk-ui/src/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/src/encryption_sync_service.rs @@ -88,11 +88,7 @@ impl EncryptionSyncService { /// Creates a new instance of a `EncryptionSyncService`. /// /// This will create and manage an instance of [`matrix_sdk::SlidingSync`]. - /// The `process_id` is used as the identifier of that instance, as such - /// make sure to not reuse a name used by another process, at the risk - /// of causing problems. pub async fn new( - process_id: String, client: Client, poll_and_network_timeouts: Option<(Duration, Duration)>, with_locking: WithLocking, @@ -119,7 +115,13 @@ impl EncryptionSyncService { if with_locking { // Gently try to enable the cross-process lock on behalf of the user. - match client.encryption().enable_cross_process_store_lock(process_id).await { + match client + .encryption() + .enable_cross_process_store_lock( + client.cross_process_store_locks_holder_name().to_owned(), + ) + .await + { Ok(()) | Err(matrix_sdk::Error::BadCryptoStoreState) => { // Ignore; we've already set the crypto store lock to // something, and that's sufficient as diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 0c80298f0c4..48396c5777d 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -111,7 +111,8 @@ impl NotificationClient { parent_client: Client, process_setup: NotificationProcessSetup, ) -> Result { - let client = parent_client.notification_client().await?; + let client = parent_client.notification_client(Self::LOCK_ID.to_owned()).await?; + Ok(NotificationClient { client, parent_client, @@ -242,7 +243,6 @@ impl NotificationClient { }; let encryption_sync = EncryptionSyncService::new( - Self::LOCK_ID.to_owned(), self.client.clone(), Some((Duration::from_secs(3), Duration::from_secs(4))), with_locking, diff --git a/crates/matrix-sdk-ui/src/sync_service.rs b/crates/matrix-sdk-ui/src/sync_service.rs index 23c8166cafe..7ff37bfbfed 100644 --- a/crates/matrix-sdk-ui/src/sync_service.rs +++ b/crates/matrix-sdk-ui/src/sync_service.rs @@ -435,15 +435,11 @@ pub struct SyncServiceBuilder { /// Is the cross-process lock for the crypto store enabled? with_cross_process_lock: bool, - - /// Application identifier, used as the cross-process lock value, if - /// applicable. - identifier: String, } impl SyncServiceBuilder { fn new(client: Client) -> Self { - Self { client, with_cross_process_lock: false, identifier: "app".to_owned() } + Self { client, with_cross_process_lock: false } } /// Enables the cross-process lock, if the sync service is being built in a @@ -454,14 +450,10 @@ impl SyncServiceBuilder { /// external process attempting to decrypt notifications. In general, /// `with_cross_process_lock` should not be called. /// - /// An app identifier can be provided too, to identify the current process; - /// if it's not provided, a default value of "app" is used as the - /// application identifier. - pub fn with_cross_process_lock(mut self, app_identifier: Option) -> Self { + /// Be sure to have configured + /// [`Client::cross_process_store_locks_holder_name`] accordingly. + pub fn with_cross_process_lock(mut self) -> Self { self.with_cross_process_lock = true; - if let Some(app_identifier) = app_identifier { - self.identifier = app_identifier; - } self } @@ -477,7 +469,6 @@ impl SyncServiceBuilder { let encryption_sync = Arc::new( EncryptionSyncService::new( - self.identifier, self.client, None, WithLocking::from(self.with_cross_process_lock), diff --git a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs index e97bd0b93a1..299dd3f844d 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -40,8 +40,7 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing())); let sync_permit_guard = sync_permit.clone().lock_owned().await; - let encryption_sync = - EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?; + let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?; let stream = encryption_sync.sync(sync_permit_guard); pin_mut!(stream); @@ -186,8 +185,7 @@ async fn test_encryption_sync_one_fixed_iteration() -> anyhow::Result<()> { let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing())); let sync_permit_guard = sync_permit.lock_owned().await; - let encryption_sync = - EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?; + let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?; // Run all the iterations. encryption_sync.run_fixed_iterations(1, sync_permit_guard).await?; @@ -218,8 +216,7 @@ async fn test_encryption_sync_two_fixed_iterations() -> anyhow::Result<()> { let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing())); let sync_permit_guard = sync_permit.lock_owned().await; - let encryption_sync = - EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?; + let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?; encryption_sync.run_fixed_iterations(2, sync_permit_guard).await?; @@ -254,8 +251,7 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result< let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing())); let sync_permit_guard = sync_permit.lock_owned().await; let encryption_sync = - EncryptionSyncService::new("tests".to_owned(), client.clone(), None, WithLocking::Yes) - .await?; + EncryptionSyncService::new(client.clone(), None, WithLocking::Yes).await?; let stream = encryption_sync.sync(sync_permit_guard); pin_mut!(stream); @@ -363,15 +359,14 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a info!("Creating the notification client"); let notification_client = client - .notification_client() + .notification_client("tests".to_owned()) .await .expect("We should be able to build a notification client"); let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing())); let sync_permit_guard = sync_permit.lock_owned().await; let encryption_sync = - EncryptionSyncService::new("tests".to_owned(), client.clone(), None, WithLocking::Yes) - .await?; + EncryptionSyncService::new(client.clone(), None, WithLocking::Yes).await?; let stream = encryption_sync.sync(sync_permit_guard); pin_mut!(stream); diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 2eac8c7d9c2..e8a6f98fb79 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2253,7 +2253,15 @@ impl Client { } /// Create a new specialized `Client` that can process notifications. - pub async fn notification_client(&self) -> Result { + /// + /// See [`CrossProcessStoreLock::new`] to learn more about + /// `cross_process_store_locks_holder_name`. + /// + /// [`CrossProcessStoreLock::new`]: matrix_sdk_common::store_locks::CrossProcessStoreLock::new + pub async fn notification_client( + &self, + cross_process_store_locks_holder_name: String, + ) -> Result { let client = Client { inner: ClientInner::new( self.inner.auth_ctx.clone(), @@ -2264,9 +2272,7 @@ impl Client { self.inner.http_client.clone(), self.inner .base_client - .clone_with_in_memory_state_store( - &self.inner.cross_process_store_locks_holder_name, - ) + .clone_with_in_memory_state_store(&cross_process_store_locks_holder_name) .await?, self.inner.server_capabilities.read().await.clone(), self.inner.respect_login_well_known, @@ -2274,7 +2280,7 @@ impl Client { self.inner.send_queue_data.clone(), #[cfg(feature = "e2e-encryption")] self.inner.e2ee.encryption_settings, - self.inner.cross_process_store_locks_holder_name.clone(), + cross_process_store_locks_holder_name, ) .await, }; diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 0ac2d38dc53..a22ebee2c00 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -1458,6 +1458,8 @@ impl Encryption { /// caches. /// /// The provided `lock_value` must be a unique identifier for this process. + /// Check [`Client::cross_process_store_locks_holder_name`] to + /// get the global value. pub async fn enable_cross_process_store_lock(&self, lock_value: String) -> Result<(), Error> { // If the lock has already been created, don't recreate it from scratch. if let Some(prev_lock) = self.client.locks().cross_process_crypto_store_lock.get() { diff --git a/testing/matrix-sdk-integration-testing/src/helpers.rs b/testing/matrix-sdk-integration-testing/src/helpers.rs index 9b4ee6a3020..18614c6ed4b 100644 --- a/testing/matrix-sdk-integration-testing/src/helpers.rs +++ b/testing/matrix-sdk-integration-testing/src/helpers.rs @@ -37,6 +37,7 @@ pub struct TestClientBuilder { use_sqlite_dir: Option, encryption_settings: EncryptionSettings, http_proxy: Option, + cross_process_store_locks_holder_name: Option, } impl TestClientBuilder { @@ -52,6 +53,7 @@ impl TestClientBuilder { use_sqlite_dir: None, encryption_settings: Default::default(), http_proxy: None, + cross_process_store_locks_holder_name: None, } } @@ -79,6 +81,11 @@ impl TestClientBuilder { self } + pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self { + self.cross_process_store_locks_holder_name = Some(holder_name); + self + } + fn common_client_builder(&self) -> ClientBuilder { let homeserver_url = option_env!("HOMESERVER_URL").unwrap_or("http://localhost:8228").to_owned(); @@ -90,6 +97,11 @@ impl TestClientBuilder { .with_encryption_settings(self.encryption_settings) .request_config(RequestConfig::short_retry()); + if let Some(holder_name) = &self.cross_process_store_locks_holder_name { + client_builder = + client_builder.cross_process_store_locks_holder_name(holder_name.clone()); + } + if let Some(proxy) = &self.http_proxy { client_builder = client_builder.proxy(proxy); } diff --git a/testing/matrix-sdk-integration-testing/src/tests/nse.rs b/testing/matrix-sdk-integration-testing/src/tests/nse.rs index 2bbb1bd1201..56655182b76 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/nse.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/nse.rs @@ -125,14 +125,18 @@ impl ClientWrapper { /// Otherwise, a random path is used. /// /// The contained SyncService always has a cross-process lock. If - /// app_identifier is supplied, it is used to identify this client's - /// process. If not, the default name is used. + /// `cross_process_store_locks_holder_name` is supplied, it is used to + /// identify this client's process. If not, the default name is used. async fn new( username: &str, sqlite_dir: Option<&Path>, - app_identifier: Option, + cross_process_store_locks_holder_name: Option, ) -> Self { - let builder = TestClientBuilder::new(username); + let mut builder = TestClientBuilder::new(username); + + if let Some(holder_name) = cross_process_store_locks_holder_name { + builder = builder.cross_process_store_locks_holder_name(holder_name); + } let builder = if let Some(sqlite_dir) = sqlite_dir { builder.use_sqlite_dir(sqlite_dir) @@ -153,7 +157,7 @@ impl ClientWrapper { let client = SyncTokenAwareClient::new(inner_client.clone()); let sync_service = SyncService::builder(inner_client) - .with_cross_process_lock(app_identifier) + .with_cross_process_lock() .build() .await .expect("Failed to create sync service"); From 3070154a57c6f09b91a100b356039c6cb931af40 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 6 Nov 2024 17:30:32 +0100 Subject: [PATCH 493/979] feat(ffi): Add `ClientBuilder::cross_process_store_locks_holder_name`. --- bindings/matrix-sdk-ffi/src/client.rs | 10 +++++-- bindings/matrix-sdk-ffi/src/client_builder.rs | 29 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index e93067bcc2b..db4c4699fa9 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -194,6 +194,8 @@ pub struct Client { impl Client { pub async fn new( sdk_client: MatrixClient, + // Copy of `MatrixClient::cross_process_store_locks_holder_name` if OIDC stuff has been + // enabled. cross_process_refresh_lock_id: Option, session_delegate: Option>, ) -> Result { @@ -216,13 +218,17 @@ impl Client { session_verification_controller, }; - if let Some(process_id) = cross_process_refresh_lock_id { + if let Some(cross_process_store_locks_holder_name) = cross_process_refresh_lock_id { if session_delegate.is_none() { return Err(anyhow::anyhow!( "missing session delegates when enabling the cross-process lock" ))?; } - client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?; + client + .inner + .oidc() + .enable_cross_process_refresh_lock(cross_process_store_locks_holder_name.clone()) + .await?; } if let Some(session_delegate) = session_delegate { diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index f05b211dafc..92af7ead14e 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -260,7 +260,8 @@ pub struct ClientBuilder { proxy: Option, disable_ssl_verification: bool, disable_automatic_token_refresh: bool, - cross_process_refresh_lock_id: Option, + cross_process_store_locks_holder_name: Option, + enable_oidc_refresh_lock: bool, session_delegate: Option>, additional_root_certificates: Vec>, disable_built_in_root_certificates: bool, @@ -284,7 +285,8 @@ impl ClientBuilder { proxy: None, disable_ssl_verification: false, disable_automatic_token_refresh: false, - cross_process_refresh_lock_id: None, + cross_process_store_locks_holder_name: None, + enable_oidc_refresh_lock: false, session_delegate: None, additional_root_certificates: Default::default(), disable_built_in_root_certificates: false, @@ -300,13 +302,21 @@ impl ClientBuilder { }) } + pub fn cross_process_store_locks_holder_name( + self: Arc, + holder_name: String, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.cross_process_store_locks_holder_name = Some(holder_name); + Arc::new(builder) + } + pub fn enable_cross_process_refresh_lock( self: Arc, - process_id: String, session_delegate: Box, ) -> Arc { let mut builder = unwrap_or_clone_arc(self); - builder.cross_process_refresh_lock_id = Some(process_id); + builder.enable_oidc_refresh_lock = true; builder.session_delegate = Some(session_delegate.into()); Arc::new(builder) } @@ -472,6 +482,11 @@ impl ClientBuilder { let builder = unwrap_or_clone_arc(self); let mut inner_builder = MatrixClient::builder(); + if let Some(holder_name) = &builder.cross_process_store_locks_holder_name { + inner_builder = + inner_builder.cross_process_store_locks_holder_name(holder_name.clone()); + } + if let Some(session_paths) = &builder.session_paths { let data_path = PathBuf::from(&session_paths.data_path); let cache_path = PathBuf::from(&session_paths.cache_path); @@ -616,7 +631,11 @@ impl ClientBuilder { Ok(Arc::new( Client::new( sdk_client, - builder.cross_process_refresh_lock_id, + if builder.enable_oidc_refresh_lock { + builder.cross_process_store_locks_holder_name + } else { + None + }, builder.session_delegate, ) .await?, From d3a232607a690847e8029d52585d29c7c705f4a2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Nov 2024 11:06:40 +0100 Subject: [PATCH 494/979] fix(ffi): Replace `enable_cross_process_refresh_lock` by `enable_oidc_refresh_crypto_lock`. This patch simplifies a little the `ClientBuilder` API: * `enable_cross_process_refresh_lock` is removed * `enable_oidc_refresh_crypto_lock` + `set_session_delegate` must be used instead. --- bindings/matrix-sdk-ffi/src/client_builder.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 92af7ead14e..f019aeb302a 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -261,7 +261,7 @@ pub struct ClientBuilder { disable_ssl_verification: bool, disable_automatic_token_refresh: bool, cross_process_store_locks_holder_name: Option, - enable_oidc_refresh_lock: bool, + enable_oidc_refresh_crypto_lock: bool, session_delegate: Option>, additional_root_certificates: Vec>, disable_built_in_root_certificates: bool, @@ -286,7 +286,7 @@ impl ClientBuilder { disable_ssl_verification: false, disable_automatic_token_refresh: false, cross_process_store_locks_holder_name: None, - enable_oidc_refresh_lock: false, + enable_oidc_refresh_crypto_lock: false, session_delegate: None, additional_root_certificates: Default::default(), disable_built_in_root_certificates: false, @@ -311,13 +311,9 @@ impl ClientBuilder { Arc::new(builder) } - pub fn enable_cross_process_refresh_lock( - self: Arc, - session_delegate: Box, - ) -> Arc { + pub fn enable_oidc_refresh_crypto_lock(self: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); - builder.enable_oidc_refresh_lock = true; - builder.session_delegate = Some(session_delegate.into()); + builder.enable_oidc_refresh_crypto_lock = true; Arc::new(builder) } @@ -631,7 +627,7 @@ impl ClientBuilder { Ok(Arc::new( Client::new( sdk_client, - if builder.enable_oidc_refresh_lock { + if builder.enable_oidc_refresh_crypto_lock { builder.cross_process_store_locks_holder_name } else { None From 4d39d176d99f18efc6776f972c6f33015850ffb1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Nov 2024 11:14:27 +0100 Subject: [PATCH 495/979] fix(ffi): Simplify `Client::new` constructor. This patch continues to simplification of the `matrix_sdk_ffi::Client`. The constructor can receive a `enable_oidc_refresh_lock: bool` instead of `cross_process_refresh_lock_id: Option`, which was a copy of `matrix_sdk::Client::cross_process_store_locks_holder_name`. Now there is a single boolean to indicate whether `Oidc::enable_cross_process_refresh_lock` should be called or not. If it has to be called, it is possible to re-use `matrix_sdk::Client::cross_process_store_locks_holder_name`. Once again, there is a single place to read this data, it's not copied over different semantics. --- bindings/matrix-sdk-ffi/src/client.rs | 12 ++++++----- bindings/matrix-sdk-ffi/src/client_builder.rs | 20 ++++++------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index db4c4699fa9..439b89e35da 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -194,9 +194,7 @@ pub struct Client { impl Client { pub async fn new( sdk_client: MatrixClient, - // Copy of `MatrixClient::cross_process_store_locks_holder_name` if OIDC stuff has been - // enabled. - cross_process_refresh_lock_id: Option, + enable_oidc_refresh_lock: bool, session_delegate: Option>, ) -> Result { let session_verification_controller: Arc< @@ -212,22 +210,26 @@ impl Client { } }); + let cross_process_store_locks_holder_name = + sdk_client.cross_process_store_locks_holder_name().to_owned(); + let client = Client { inner: AsyncRuntimeDropped::new(sdk_client), delegate: RwLock::new(None), session_verification_controller, }; - if let Some(cross_process_store_locks_holder_name) = cross_process_refresh_lock_id { + if enable_oidc_refresh_lock { if session_delegate.is_none() { return Err(anyhow::anyhow!( "missing session delegates when enabling the cross-process lock" ))?; } + client .inner .oidc() - .enable_cross_process_refresh_lock(cross_process_store_locks_holder_name.clone()) + .enable_cross_process_refresh_lock(cross_process_store_locks_holder_name) .await?; } diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index f019aeb302a..0ac13e41798 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -261,7 +261,7 @@ pub struct ClientBuilder { disable_ssl_verification: bool, disable_automatic_token_refresh: bool, cross_process_store_locks_holder_name: Option, - enable_oidc_refresh_crypto_lock: bool, + enable_oidc_refresh_lock: bool, session_delegate: Option>, additional_root_certificates: Vec>, disable_built_in_root_certificates: bool, @@ -286,7 +286,7 @@ impl ClientBuilder { disable_ssl_verification: false, disable_automatic_token_refresh: false, cross_process_store_locks_holder_name: None, - enable_oidc_refresh_crypto_lock: false, + enable_oidc_refresh_lock: false, session_delegate: None, additional_root_certificates: Default::default(), disable_built_in_root_certificates: false, @@ -311,9 +311,9 @@ impl ClientBuilder { Arc::new(builder) } - pub fn enable_oidc_refresh_crypto_lock(self: Arc) -> Arc { + pub fn enable_oidc_refresh_lock(self: Arc) -> Arc { let mut builder = unwrap_or_clone_arc(self); - builder.enable_oidc_refresh_crypto_lock = true; + builder.enable_oidc_refresh_lock = true; Arc::new(builder) } @@ -625,16 +625,8 @@ impl ClientBuilder { let sdk_client = inner_builder.build().await?; Ok(Arc::new( - Client::new( - sdk_client, - if builder.enable_oidc_refresh_crypto_lock { - builder.cross_process_store_locks_holder_name - } else { - None - }, - builder.session_delegate, - ) - .await?, + Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate) + .await?, )) } From 403be3dea096e32064ca67858a4a144782045dc8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Nov 2024 13:12:08 +0100 Subject: [PATCH 496/979] test(ci): Disable Complement Crypto for a short period of time. --- .github/workflows/bindings_ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 89cdf0a38dd..30a9d83142b 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -177,12 +177,12 @@ jobs: - name: Build Framework run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev - complement-crypto: - name: "Run Complement Crypto tests" - uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main - with: - use_rust_sdk: "." # use local checkout - use_complement_crypto: "MATCHING_BRANCH" + # complement-crypto: + # name: "Run Complement Crypto tests" + # uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main + # with: + # use_rust_sdk: "." # use local checkout + # use_complement_crypto: "MATCHING_BRANCH" test-crypto-apple-framework-generation: name: Generate Crypto FFI Apple XCFramework From bd5f5f3fe0326bc3a3f5f04de3fa296a6f3c3a7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:28:12 +0000 Subject: [PATCH 497/979] chore(deps): bump crate-ci/typos from 1.27.0 to 1.27.3 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.0 to 1.27.3. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.27.0...v1.27.3) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2de47b34852..2bfd5b50db2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,7 +304,7 @@ jobs: uses: actions/checkout@v4 - name: Check the spelling of the files in our repo - uses: crate-ci/typos@v1.27.0 + uses: crate-ci/typos@v1.27.3 clippy: name: Run clippy From 66a79729ed8290d17e67ca7cd0f5aa69160bcc7c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 12 Nov 2024 10:30:09 +0100 Subject: [PATCH 498/979] test(ci): Re-enable Complement Crypto. --- .github/workflows/bindings_ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 30a9d83142b..89cdf0a38dd 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -177,12 +177,12 @@ jobs: - name: Build Framework run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev - # complement-crypto: - # name: "Run Complement Crypto tests" - # uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main - # with: - # use_rust_sdk: "." # use local checkout - # use_complement_crypto: "MATCHING_BRANCH" + complement-crypto: + name: "Run Complement Crypto tests" + uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main + with: + use_rust_sdk: "." # use local checkout + use_complement_crypto: "MATCHING_BRANCH" test-crypto-apple-framework-generation: name: Generate Crypto FFI Apple XCFramework From 8e2939bd919caf9f5ebb8e5cdab4d7a112d94335 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Nov 2024 16:09:33 +0100 Subject: [PATCH 499/979] refactor!(send queue): move `RoomSendQueue::unwedge` to the `SendHandle` type --- bindings/matrix-sdk-ffi/src/room.rs | 35 +++------------- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 39 +++++++++++++++++- .../src/timeline/event_item/mod.rs | 5 +++ crates/matrix-sdk/src/send_queue.rs | 41 ++++++++++--------- .../tests/integration/send_queue.rs | 5 ++- 5 files changed, 72 insertions(+), 53 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 9b01c4f6814..ed67a1d4fa4 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -25,8 +25,7 @@ use ruma::{ }, TimelineEventType, }, - EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, TransactionId, - UserId, + EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId, }; use tokio::sync::RwLock; use tracing::error; @@ -40,7 +39,7 @@ use crate::{ room_info::RoomInfo, room_member::RoomMember, ruma::{ImageInfo, Mentions, NotifyType}, - timeline::{FocusEventError, ReceiptType, Timeline}, + timeline::{FocusEventError, ReceiptType, SendHandle, Timeline}, utils::u64_to_uint, TaskHandle, }; @@ -790,10 +789,8 @@ impl Room { pub async fn withdraw_verification_and_resend( &self, user_ids: Vec, - transaction_id: String, + send_handle: Arc, ) -> Result<(), ClientError> { - let transaction_id: OwnedTransactionId = transaction_id.into(); - let user_ids: Vec = user_ids.iter().map(UserId::parse).collect::>()?; @@ -805,7 +802,7 @@ impl Room { } } - self.inner.send_queue().unwedge(&transaction_id).await?; + send_handle.try_resend().await?; Ok(()) } @@ -823,10 +820,8 @@ impl Room { pub async fn ignore_device_trust_and_resend( &self, devices: HashMap>, - transaction_id: String, + send_handle: Arc, ) -> Result<(), ClientError> { - let transaction_id: OwnedTransactionId = transaction_id.into(); - let encryption = self.inner.client().encryption(); for (user_id, device_ids) in devices.iter() { @@ -841,26 +836,8 @@ impl Room { } } - self.inner.send_queue().unwedge(&transaction_id).await?; - - Ok(()) - } + send_handle.try_resend().await?; - /// Attempt to manually resend messages that failed to send due to issues - /// that should now have been fixed. - /// - /// This is useful for example, when there's a - /// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error; - /// the user may have re-verified on a different device and would now - /// like to send the failed message that's waiting on this device. - /// - /// # Arguments - /// - /// * `transaction_id` - The send queue transaction identifier of the local - /// echo that should be unwedged. - pub async fn try_resend(&self, transaction_id: String) -> Result<(), ClientError> { - let transaction_id: &TransactionId = transaction_id.as_str().into(); - self.inner.send_queue().unwedge(transaction_id).await?; Ok(()) } } diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index d82f719ed7d..3b1ace9d3d2 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -270,7 +270,7 @@ impl Timeline { msg: Arc, ) -> Result, ClientError> { match self.inner.send((*msg).to_owned().with_relation(None).into()).await { - Ok(handle) => Ok(Arc::new(SendHandle { inner: Mutex::new(Some(handle)) })), + Ok(handle) => Ok(Arc::new(SendHandle::new(handle))), Err(err) => { error!("error when sending a message: {err}"); Err(anyhow::anyhow!(err).into()) @@ -710,11 +710,18 @@ impl Timeline { } } +/// A handle to perform actions onto a local echo. #[derive(uniffi::Object)] pub struct SendHandle { inner: Mutex>, } +impl SendHandle { + fn new(handle: matrix_sdk::send_queue::SendHandle) -> Self { + Self { inner: Mutex::new(Some(handle)) } + } +} + #[matrix_sdk_ffi_macros::export] impl SendHandle { /// Try to abort the sending of the current event. @@ -732,10 +739,32 @@ impl SendHandle { .await .map_err(|err| anyhow::anyhow!("error when saving in store: {err}"))?) } else { - warn!("trying to abort an send handle that's already been actioned"); + warn!("trying to abort a send handle that's already been actioned"); Ok(false) } } + + /// Attempt to manually resend messages that failed to send due to issues + /// that should now have been fixed. + /// + /// This is useful for example, when there's a + /// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error; + /// the user may have re-verified on a different device and would now + /// like to send the failed message that's waiting on this device. + /// + /// # Arguments + /// + /// * `transaction_id` - The send queue transaction identifier of the local + /// echo that should be unwedged. + pub async fn try_resend(self: Arc) -> Result<(), ClientError> { + let locked = self.inner.lock().await; + if let Some(handle) = locked.as_ref() { + handle.unwedge().await?; + } else { + warn!("trying to unwedge a send handle that's been aborted"); + } + Ok(()) + } } #[derive(Debug, thiserror::Error, uniffi::Error)] @@ -1273,4 +1302,10 @@ impl LazyTimelineItemProvider { latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()), } } + + /// For local echoes, return the associated send handle; returns `None` for + /// remote echoes. + fn get_send_handle(&self) -> Option> { + self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle))) + } } 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 b91dbb72abb..6860d0cb600 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -570,6 +570,11 @@ impl EventTimelineItem { EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id), } } + + /// For local echoes, return the associated send handle. + pub fn local_echo_send_handle(&self) -> Option { + as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone()) + } } impl From for EventTimelineItemKind { diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index a842a9e895e..474b415bc3f 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -753,26 +753,6 @@ impl RoomSendQueue { self.inner.notifier.notify_one(); } } - - /// Unwedge a local echo identified by its transaction identifier and try to - /// resend it. - pub async fn unwedge(&self, transaction_id: &TransactionId) -> Result<(), RoomSendQueueError> { - self.inner - .queue - .mark_as_unwedged(transaction_id) - .await - .map_err(RoomSendQueueError::StorageError)?; - - // Wake up the queue, in case the room was asleep before unwedging the request. - self.inner.notifier.notify_one(); - - let _ = self - .inner - .updates - .send(RoomSendQueueUpdate::RetryEvent { transaction_id: transaction_id.to_owned() }); - - Ok(()) - } } impl From<&crate::Error> for QueueWedgeError { @@ -1712,6 +1692,8 @@ pub enum RoomSendQueueStorageError { pub struct SendHandle { room: RoomSendQueue, transaction_id: OwnedTransactionId, + // TODO(bnjbvr): remove this, once we have settled the `SendHandle` vs `SendAttachmentHandle` + // situation. is_upload: bool, } @@ -1797,6 +1779,25 @@ impl SendHandle { .await } + /// Unwedge a local echo identified by its transaction identifier and try to + /// resend it. + pub async fn unwedge(&self) -> Result<(), RoomSendQueueError> { + let room = &self.room.inner; + room.queue + .mark_as_unwedged(&self.transaction_id) + .await + .map_err(RoomSendQueueError::StorageError)?; + + // Wake up the queue, in case the room was asleep before unwedging the request. + room.notifier.notify_one(); + + let _ = room + .updates + .send(RoomSendQueueUpdate::RetryEvent { transaction_id: self.transaction_id.clone() }); + + Ok(()) + } + /// Send a reaction to the event as soon as it's sent. /// /// If returning `Ok(None)`; this means the reaction couldn't be sent diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index c8ee9496090..e1d138b28f0 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1420,7 +1420,8 @@ async fn test_unwedge_unrecoverable_errors() { mock.mock_room_send().ok(event_id!("$42")).mock_once().mount().await; // Queue the unrecoverable message. - q.send(RoomMessageEventContent::text_plain("i'm too big for ya").into()).await.unwrap(); + let send_handle = + q.send(RoomMessageEventContent::text_plain("i'm too big for ya").into()).await.unwrap(); // Message is seen as a local echo. let (txn1, _) = assert_update!(watch => local echo { body = "i'm too big for ya" }); @@ -1440,7 +1441,7 @@ async fn test_unwedge_unrecoverable_errors() { assert!(client.send_queue().is_enabled()); // Unwedge the previously failed message and try sending it again - q.unwedge(&txn1).await.unwrap(); + send_handle.unwedge().await.unwrap(); // The message should be retried assert_update!(watch => retry { txn=txn1 }); From cfd0c5ce0c66f0444de513c6f3788b15ac78bc73 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 7 Nov 2024 13:39:28 +0100 Subject: [PATCH 500/979] feat(media): allow passing a custom `RequestConfig` to an upload request --- bindings/matrix-sdk-ffi/src/client.rs | 2 +- crates/matrix-sdk/src/account.rs | 2 +- crates/matrix-sdk/src/encryption/futures.rs | 29 +++++++++++++-- crates/matrix-sdk/src/media.rs | 41 +++++++++++++++------ crates/matrix-sdk/src/room/mod.rs | 2 +- 5 files changed, 57 insertions(+), 19 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 439b89e35da..47012ebaffc 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -703,7 +703,7 @@ impl Client { progress_watcher: Option>, ) -> Result { let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?; - let request = self.inner.media().upload(&mime_type, data); + let request = self.inner.media().upload(&mime_type, data, None); if let Some(progress_watcher) = progress_watcher { let mut subscriber = request.subscribe_to_send_progress(); diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 249bfa3129e..2e08bb64154 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -252,7 +252,7 @@ impl Account { /// /// [`Media::upload()`]: crate::Media::upload pub async fn upload_avatar(&self, content_type: &Mime, data: Vec) -> Result { - let upload_response = self.client.media().upload(content_type, data).await?; + let upload_response = self.client.media().upload(content_type, data, None).await?; self.set_avatar_url(Some(&upload_response.content_uri)).await?; Ok(upload_response.content_uri) } diff --git a/crates/matrix-sdk/src/encryption/futures.rs b/crates/matrix-sdk/src/encryption/futures.rs index a31c84851f5..f22e92fd7b7 100644 --- a/crates/matrix-sdk/src/encryption/futures.rs +++ b/crates/matrix-sdk/src/encryption/futures.rs @@ -25,7 +25,7 @@ use eyeball::Subscriber; use matrix_sdk_common::boxed_into_future; use ruma::events::room::{EncryptedFile, EncryptedFileInit}; -use crate::{Client, Result, TransmissionProgress}; +use crate::{config::RequestConfig, Client, Media, Result, TransmissionProgress}; /// Future returned by [`Client::upload_encrypted_file`]. #[allow(missing_debug_implementations)] @@ -34,11 +34,18 @@ pub struct UploadEncryptedFile<'a, R: ?Sized> { content_type: &'a mime::Mime, reader: &'a mut R, send_progress: SharedObservable, + request_config: Option, } impl<'a, R: ?Sized> UploadEncryptedFile<'a, R> { pub(crate) fn new(client: &'a Client, content_type: &'a mime::Mime, reader: &'a mut R) -> Self { - Self { client, content_type, reader, send_progress: Default::default() } + Self { + client, + content_type, + reader, + send_progress: Default::default(), + request_config: None, + } } /// Replace the default `SharedObservable` used for tracking upload @@ -55,6 +62,15 @@ impl<'a, R: ?Sized> UploadEncryptedFile<'a, R> { self } + /// Replace the default request config used for the upload request. + /// + /// The timeout value will be overridden with a reasonable default, based on + /// the size of the encrypted payload. + pub fn with_request_config(mut self, request_config: RequestConfig) -> Self { + self.request_config = Some(request_config); + self + } + /// Get a subscriber to observe the progress of sending the request /// body. #[cfg(not(target_arch = "wasm32"))] @@ -71,16 +87,21 @@ where boxed_into_future!(extra_bounds: 'a); fn into_future(self) -> Self::IntoFuture { - let Self { client, content_type, reader, send_progress } = self; + let Self { client, content_type, reader, send_progress, request_config } = self; Box::pin(async move { let mut encryptor = matrix_sdk_base::crypto::AttachmentEncryptor::new(reader); let mut buf = Vec::new(); encryptor.read_to_end(&mut buf)?; + // Override the reasonable upload timeout value, based on the size of the + // encrypted payload. + let request_config = + request_config.map(|config| config.timeout(Media::reasonable_upload_timeout(&buf))); + let response = client .media() - .upload(content_type, buf) + .upload(content_type, buf, request_config) .with_send_progress_observable(send_progress) .await?; diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 5111812ebbf..9f686f1e2d8 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -40,7 +40,8 @@ use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir}; use tokio::{fs::File as TokioFile, io::AsyncWriteExt}; use crate::{ - attachment::Thumbnail, futures::SendRequest, Client, Error, Result, TransmissionProgress, + attachment::Thumbnail, config::RequestConfig, futures::SendRequest, Client, Error, Result, + TransmissionProgress, }; /// A conservative upload speed of 1Mbps @@ -144,8 +145,11 @@ impl Media { /// * `content_type` - The type of the media, this will be used as the /// content-type header. /// - /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the - /// media. + /// * `data` - Vector of bytes to be uploaded to the server. + /// + /// * `request_config` - Optional request configuration for the HTTP client, + /// overriding the default. If not provided, a reasonable timeout value is + /// inferred. /// /// # Examples /// @@ -159,25 +163,38 @@ impl Media { /// # let mut client = Client::new(homeserver).await?; /// let image = fs::read("/home/example/my-cat.jpg")?; /// - /// let response = client.media().upload(&mime::IMAGE_JPEG, image).await?; + /// let response = + /// client.media().upload(&mime::IMAGE_JPEG, image, None).await?; /// /// println!("Cat URI: {}", response.content_uri); /// # anyhow::Ok(()) }; /// ``` - pub fn upload(&self, content_type: &Mime, data: Vec) -> SendUploadRequest { - let timeout = std::cmp::max( - Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED), - MIN_UPLOAD_REQUEST_TIMEOUT, - ); + pub fn upload( + &self, + content_type: &Mime, + data: Vec, + request_config: Option, + ) -> SendUploadRequest { + let request_config = request_config.unwrap_or_else(|| { + self.client.request_config().timeout(Self::reasonable_upload_timeout(&data)) + }); let request = assign!(media::create_content::v3::Request::new(data), { content_type: Some(content_type.essence_str().to_owned()), }); - let request_config = self.client.request_config().timeout(timeout); self.client.send(request, Some(request_config)) } + /// Returns a reasonable upload timeout for an upload, based on the size of + /// the data to be uploaded. + pub(crate) fn reasonable_upload_timeout(data: &[u8]) -> Duration { + std::cmp::max( + Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED), + MIN_UPLOAD_REQUEST_TIMEOUT, + ) + } + /// Preallocates an MXC URI for a media that will be uploaded soon. /// /// This preallocates an URI *before* any content is uploaded to the server. @@ -630,7 +647,7 @@ impl Media { let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone()); let upload_attachment = async move { - self.upload(content_type, data) + self.upload(content_type, data, None) .with_send_progress_observable(send_progress) .await .map_err(Error::from) @@ -653,7 +670,7 @@ impl Media { }; let response = self - .upload(&thumbnail.content_type, thumbnail.data) + .upload(&thumbnail.content_type, thumbnail.data, None) .with_send_progress_observable(send_progress) .await?; let url = response.content_uri; diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 87f1ca8866a..3a6eb8072a2 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2263,7 +2263,7 @@ impl Room { ) -> Result { self.ensure_room_joined()?; - let upload_response = self.client.media().upload(mime, data).await?; + let upload_response = self.client.media().upload(mime, data, None).await?; let mut info = info.unwrap_or_default(); info.blurhash = upload_response.blurhash; info.mimetype = Some(mime.to_string()); From 982c6eab54201c1310fc8a72b47b10e3fb2a6543 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 7 Nov 2024 13:39:42 +0100 Subject: [PATCH 501/979] feat(send queue): retry uploads if they've failed with transient errors --- crates/matrix-sdk/src/send_queue.rs | 19 ++-- crates/matrix-sdk/src/test_utils/mocks.rs | 14 +++ .../tests/integration/send_queue.rs | 88 +++++++++++++++++++ 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 474b415bc3f..a9322dc239b 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -171,7 +171,7 @@ use crate::{ config::RequestConfig, error::RetryKind, room::{edit::EditedContent, WeakRoom}, - Client, Room, + Client, Media, Room, }; mod upload; @@ -709,18 +709,27 @@ impl RoomSendQueue { let media_source = if room.is_encrypted().await? { trace!("upload will be encrypted (encrypted room)"); let mut cursor = std::io::Cursor::new(data); - let encrypted_file = - room.client().upload_encrypted_file(&mime, &mut cursor).await?; + let encrypted_file = room + .client() + .upload_encrypted_file(&mime, &mut cursor) + .with_request_config(RequestConfig::short_retry()) + .await?; MediaSource::Encrypted(Box::new(encrypted_file)) } else { trace!("upload will be in clear text (room without encryption)"); - let res = room.client().media().upload(&mime, data).await?; + let request_config = RequestConfig::short_retry() + .timeout(Media::reasonable_upload_timeout(&data)); + let res = + room.client().media().upload(&mime, data, Some(request_config)).await?; MediaSource::Plain(res.content_uri) }; #[cfg(not(feature = "e2e-encryption"))] let media_source = { - let res = room.client().media().upload(&mime, data).await?; + let request_config = RequestConfig::short_retry() + .timeout(Media::reasonable_upload_timeout(&data)); + let res = + room.client().media().upload(&mime, data, Some(request_config)).await?; MediaSource::Plain(res.content_uri) }; diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 35f6970e2af..874568eef3f 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -293,6 +293,13 @@ impl<'a> MatrixMock<'a> { Self { mock: self.mock.up_to_n_times(1).expect(1), ..self } } + /// Specify an upper limit to the number of times you would like this + /// [`MatrixMock`] to respond to incoming requests that satisfy the + /// conditions imposed by your matchers. + pub fn up_to_n_times(self, num: u64) -> Self { + Self { mock: self.mock.up_to_n_times(num), ..self } + } + /// Mount a [`MatrixMock`] on the attached server. /// /// The [`MatrixMock`] will remain active until the [`MatrixMockServer`] is @@ -518,6 +525,13 @@ impl<'a> MockUpload<'a> { MatrixMock { server: self.server, mock } } + /// Returns a send endpoint that emulates a transient failure, i.e responds + /// with error 500. + pub fn error500(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(500)); + MatrixMock { server: self.server, mock } + } + /// Specify how to respond to a query (viz., like /// [`MockBuilder::respond_with`] does), when other predefined responses /// aren't sufficient. diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index e1d138b28f0..6cb96670d2a 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1933,3 +1933,91 @@ async fn test_media_uploads() { // That's all, folks! assert!(watch.is_empty()); } + +#[async_test] +async fn test_media_upload_retry() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.make_client().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Create the media to send (no thumbnails). + let filename = "surprise.jpeg.exe"; + let content_type = mime::IMAGE_JPEG; + let data = b"hello world".to_vec(); + + let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(13)), + width: Some(uint!(37)), + size: Some(uint!(42)), + blurhash: None, + })); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + // Fail for the first three attempts. + mock.mock_upload() + .expect_mime_type("image/jpeg") + .error500() + .up_to_n_times(3) + .expect(3) + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + q.send_attachment(filename, content_type, data, config) + .await + .expect("queuing the attachment works"); + + // Observe the local echo. + let (event_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.body, filename); + + // Let the upload stumble and the queue disable itself. + let error = assert_update!(watch => error { recoverable=true, txn=event_txn }); + let error = error.as_client_api_error().unwrap(); + assert_eq!(error.status_code, 500); + assert!(q.is_enabled().not()); + + // Mount the mock for the upload and sending the event. + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://sdk.rs/media")) + .mock_once() + .mount() + .await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + + // Restart the send queue. + q.set_enabled(true); + + assert_update!(watch => uploaded { + related_to = event_txn, + mxc = mxc_uri!("mxc://sdk.rs/media") + }); + + let edit_msg = assert_update!(watch => edit local echo { + txn = event_txn + }); + assert_let!(MessageType::Image(new_content) = edit_msg.msgtype); + assert_let!(MediaSource::Plain(new_uri) = &new_content.source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/media")); + + // The event is sent, at some point. + assert_update!(watch => sent { + txn = event_txn, + event_id = event_id!("$1") + }); + + // That's all, folks! + assert!(watch.is_empty()); +} From 6f60eea9cecd4edb0c1a6b02d73e25ed96f138c6 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 7 Nov 2024 16:13:00 +0100 Subject: [PATCH 502/979] task(tests): refactor mock system to use generic endpoints and avoid code duplication --- crates/matrix-sdk/src/test_utils/mocks.rs | 209 +++++++++------------- 1 file changed, 87 insertions(+), 122 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 874568eef3f..3cd2af0123f 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -44,14 +44,14 @@ use crate::{Client, Room}; /// It works like this: /// /// - start by saying which endpoint you'd like to mock, e.g. -/// [`Self::mock_room_send()`]. This returns a specialized `MockSomething` +/// [`Self::mock_room_send()`]. This returns a specialized [`MockEndpoint`] /// data structure, with its own impl. For this example, it's -/// [`MockRoomSend`]. +/// `MockEndpoint`. /// - configure the response on the endpoint-specific mock data structure. For /// instance, if you want the sending to result in a transient failure, call -/// [`MockRoomSend::error500`]; if you want it to succeed and return the event -/// `$42`, call [`MockRoomSend::ok`]. It's still possible to call -/// [`MockRoomSend::respond_with()`], as we do with wiremock MockBuilder, for +/// [`MockEndpoint::error500`]; if you want it to succeed and return the event +/// `$42`, call [`MockEndpoint::ok()`]. It's still possible to call +/// [`MockEndpoint::respond_with()`], as we do with wiremock MockBuilder, for /// maximum flexibility when the helpers aren't sufficient. /// - once the endpoint's response is configured, for any mock builder, you get /// a [`MatrixMock`]; this is a plain [`wiremock::Mock`] with the server @@ -139,81 +139,85 @@ impl MatrixMockServer { // Specific mount endpoints. impl MatrixMockServer { /// Mocks a sync endpoint. - pub fn mock_sync(&self) -> MockSync<'_> { + pub fn mock_sync(&self) -> MockEndpoint<'_, SyncEndpoint> { let mock = Mock::given(method("GET")) .and(path("/_matrix/client/r0/sync")) .and(header("authorization", "Bearer 1234")); - MockSync { + MockEndpoint { mock, server: &self.server, - sync_response_builder: self.sync_response_builder.clone(), + endpoint: SyncEndpoint { sync_response_builder: self.sync_response_builder.clone() }, } } /// Creates a prebuilt mock for sending an event in a room. /// /// Note: works with *any* room. - pub fn mock_room_send(&self) -> MockRoomSend<'_> { + pub fn mock_room_send(&self) -> MockEndpoint<'_, RoomSendEndpoint> { let mock = Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) .and(header("authorization", "Bearer 1234")); - MockRoomSend { mock, server: &self.server } + MockEndpoint { mock, server: &self.server, endpoint: RoomSendEndpoint } } /// Creates a prebuilt mock for asking whether *a* room is encrypted or not. /// /// Note: Applies to all rooms. - pub fn mock_room_state_encryption(&self) -> MockEncryptionState<'_> { + pub fn mock_room_state_encryption(&self) -> MockEndpoint<'_, EncryptionStateEndpoint> { let mock = Mock::given(method("GET")) .and(header("authorization", "Bearer 1234")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); - MockEncryptionState { mock, server: &self.server } + MockEndpoint { mock, server: &self.server, endpoint: EncryptionStateEndpoint } } /// Creates a prebuilt mock for setting the room encryption state. /// /// Note: Applies to all rooms. - pub fn mock_set_room_state_encryption(&self) -> MockSetEncryptionState<'_> { + pub fn mock_set_room_state_encryption(&self) -> MockEndpoint<'_, SetEncryptionStateEndpoint> { let mock = Mock::given(method("PUT")) .and(header("authorization", "Bearer 1234")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); - MockSetEncryptionState { mock, server: &self.server } + MockEndpoint { mock, server: &self.server, endpoint: SetEncryptionStateEndpoint } } /// Creates a prebuilt mock for the room redact endpoint. - pub fn mock_room_redact(&self) -> MockRoomRedact<'_> { + pub fn mock_room_redact(&self) -> MockEndpoint<'_, RoomRedactEndpoint> { let mock = Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) .and(header("authorization", "Bearer 1234")); - MockRoomRedact { mock, server: &self.server } + MockEndpoint { mock, server: &self.server, endpoint: RoomRedactEndpoint } } /// Creates a prebuilt mock for retrieving an event with /room/.../event. - pub fn mock_room_event(&self) -> MockRoomEvent<'_> { + pub fn mock_room_event(&self) -> MockEndpoint<'_, RoomEventEndpoint> { let mock = Mock::given(method("GET")).and(header("authorization", "Bearer 1234")); - MockRoomEvent { mock, server: &self.server, room: None, match_event_id: false } + MockEndpoint { + mock, + server: &self.server, + endpoint: RoomEventEndpoint { room: None, match_event_id: false }, + } } /// Create a prebuilt mock for uploading media. - pub fn mock_upload(&self) -> MockUpload<'_> { + pub fn mock_upload(&self) -> MockEndpoint<'_, UploadEndpoint> { let mock = Mock::given(method("POST")) .and(path("/_matrix/media/r0/upload")) .and(header("authorization", "Bearer 1234")); - MockUpload { mock, server: &self.server } + MockEndpoint { mock, server: &self.server, endpoint: UploadEndpoint } } /// Create a prebuilt mock for resolving room aliases. - pub fn mock_room_directory_resolve_alias(&self) -> MockResolveRoomAlias<'_> { + pub fn mock_room_directory_resolve_alias(&self) -> MockEndpoint<'_, ResolveRoomAliasEndpoint> { let mock = Mock::given(method("GET")).and(path_regex(r"/_matrix/client/r0/directory/room/.*")); - MockResolveRoomAlias { mock, server: &self.server } + MockEndpoint { mock, server: &self.server, endpoint: ResolveRoomAliasEndpoint } } /// Create a prebuilt mock for creating room aliases. - pub fn mock_create_room_alias(&self) -> MockCreateRoomAlias<'_> { + pub fn mock_create_room_alias(&self) -> MockEndpoint<'_, CreateRoomAliasEndpoint> { let mock = Mock::given(method("PUT")).and(path_regex(r"/_matrix/client/r0/directory/room/.*")); - MockCreateRoomAlias { mock, server: &self.server } + MockEndpoint { mock, server: &self.server, endpoint: CreateRoomAliasEndpoint } } } @@ -325,13 +329,41 @@ impl<'a> MatrixMock<'a> { } } -/// A prebuilt mock for sending events to a room. -pub struct MockRoomSend<'a> { +/// Generic mocked endpoint, with useful common helpers. +pub struct MockEndpoint<'a, T> { server: &'a MockServer, mock: MockBuilder, + endpoint: T, +} + +impl<'a, T> MockEndpoint<'a, T> { + /// Specify how to respond to a query (viz., like + /// [`MockBuilder::respond_with`] does), when other predefined responses + /// aren't sufficient. + pub fn respond_with(self, func: R) -> MatrixMock<'a> { + MatrixMock { mock: self.mock.respond_with(func), server: self.server } + } + + /// Returns a send endpoint that emulates a transient failure, i.e responds + /// with error 500. + pub fn error500(self) -> MatrixMock<'a> { + MatrixMock { mock: self.mock.respond_with(ResponseTemplate::new(500)), server: self.server } + } + + /// Internal helper to return an `{ event_id }` JSON struct along with a 200 + /// ok response. + fn ok_with_event_id(self, event_id: OwnedEventId) -> MatrixMock<'a> { + let mock = self.mock.respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id })), + ); + MatrixMock { server: self.server, mock } + } } -impl<'a> MockRoomSend<'a> { +/// A prebuilt mock for sending an event in a room. +pub struct RoomSendEndpoint; + +impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// Ensures that the body of the request is a superset of the provided /// `body` parameter. pub fn body_matches_partial_json(self, body: serde_json::Value) -> Self { @@ -341,19 +373,7 @@ impl<'a> MockRoomSend<'a> { /// Returns a send endpoint that emulates success, i.e. the event has been /// sent with the given event id. pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { - let returned_event_id = returned_event_id.into(); - MatrixMock { - mock: self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "event_id": returned_event_id - }))), - server: self.server, - } - } - - /// Returns a send endpoint that emulates a transient failure, i.e responds - /// with error 500. - pub fn error500(self) -> MatrixMock<'a> { - MatrixMock { mock: self.mock.respond_with(ResponseTemplate::new(500)), server: self.server } + self.ok_with_event_id(returned_event_id.into()) } /// Returns a send endpoint that emulates a permanent failure (event is too @@ -367,30 +387,21 @@ impl<'a> MockRoomSend<'a> { server: self.server, } } - - /// Specify how to respond to a query (viz., like - /// [`MockBuilder::respond_with`] does), when other predefined responses - /// aren't sufficient. - pub fn respond_with(self, func: R) -> MatrixMock<'a> { - MatrixMock { mock: self.mock.respond_with(func), server: self.server } - } } /// A prebuilt mock for running sync v2. -pub struct MockSync<'a> { - mock: MockBuilder, - server: &'a MockServer, +pub struct SyncEndpoint { sync_response_builder: Arc>, } -impl<'a> MockSync<'a> { +impl<'a> MockEndpoint<'a, SyncEndpoint> { /// Temporarily mocks the sync with the given endpoint and runs a client /// sync with it. /// /// After calling this function, the sync endpoint isn't mocked anymore. pub async fn ok_and_run(self, client: &Client, func: F) { let json_response = { - let mut builder = self.sync_response_builder.lock().unwrap(); + let mut builder = self.endpoint.sync_response_builder.lock().unwrap(); func(&mut builder); builder.build_json_sync_response() }; @@ -406,12 +417,9 @@ impl<'a> MockSync<'a> { } /// A prebuilt mock for reading the encryption state of a room. -pub struct MockEncryptionState<'a> { - server: &'a MockServer, - mock: MockBuilder, -} +pub struct EncryptionStateEndpoint; -impl<'a> MockEncryptionState<'a> { +impl<'a> MockEndpoint<'a, EncryptionStateEndpoint> { /// Marks the room as encrypted. pub fn encrypted(self) -> MatrixMock<'a> { let mock = self.mock.respond_with( @@ -430,63 +438,49 @@ impl<'a> MockEncryptionState<'a> { } /// A prebuilt mock for setting the encryption state of a room. -pub struct MockSetEncryptionState<'a> { - server: &'a MockServer, - mock: MockBuilder, -} +pub struct SetEncryptionStateEndpoint; -impl<'a> MockSetEncryptionState<'a> { +impl<'a> MockEndpoint<'a, SetEncryptionStateEndpoint> { /// Returns a mock for a successful setting of the encryption state event. pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { - let event_id = returned_event_id.into(); - let mock = self.mock.respond_with( - ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id })), - ); - MatrixMock { server: self.server, mock } + self.ok_with_event_id(returned_event_id.into()) } } /// A prebuilt mock for redacting an event in a room. -pub struct MockRoomRedact<'a> { - server: &'a MockServer, - mock: MockBuilder, -} +pub struct RoomRedactEndpoint; -impl<'a> MockRoomRedact<'a> { +impl<'a> MockEndpoint<'a, RoomRedactEndpoint> { /// Returns a redact endpoint that emulates success, i.e. the redaction /// event has been sent with the given event id. pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { - let event_id = returned_event_id.into(); - let mock = self.mock.respond_with( - ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id })), - ); - MatrixMock { server: self.server, mock } + self.ok_with_event_id(returned_event_id.into()) } } /// A prebuilt mock for getting a single event in a room. -pub struct MockRoomEvent<'a> { +pub struct RoomEventEndpoint { room: Option, match_event_id: bool, - server: &'a MockServer, - mock: MockBuilder, } -impl<'a> MockRoomEvent<'a> { +impl<'a> MockEndpoint<'a, RoomEventEndpoint> { /// Limits the scope of this mock to a specific room. - pub fn room(self, room: impl Into) -> Self { - Self { room: Some(room.into()), ..self } + pub fn room(mut self, room: impl Into) -> Self { + self.endpoint.room = Some(room.into()); + self } /// Whether the mock checks for the event id from the event. - pub fn match_event_id(self) -> Self { - Self { match_event_id: true, ..self } + pub fn match_event_id(mut self) -> Self { + self.endpoint.match_event_id = true; + self } /// Returns a redact endpoint that emulates success, i.e. the redaction /// event has been sent with the given event id. pub fn ok(self, event: TimelineEvent) -> MatrixMock<'a> { - let event_path = if self.match_event_id { + let event_path = if self.endpoint.match_event_id { let event_id = event.kind.event_id().expect("an event id is required"); event_id.to_string() } else { @@ -494,7 +488,7 @@ impl<'a> MockRoomEvent<'a> { "".to_owned() }; - let room_path = self.room.map_or_else(|| ".*".to_owned(), |room| room.to_string()); + let room_path = self.endpoint.room.map_or_else(|| ".*".to_owned(), |room| room.to_string()); let mock = self .mock @@ -505,12 +499,9 @@ impl<'a> MockRoomEvent<'a> { } /// A prebuilt mock for uploading media. -pub struct MockUpload<'a> { - server: &'a MockServer, - mock: MockBuilder, -} +pub struct UploadEndpoint; -impl<'a> MockUpload<'a> { +impl<'a> MockEndpoint<'a, UploadEndpoint> { /// Expect that the content type matches what's given here. pub fn expect_mime_type(self, content_type: &str) -> Self { Self { mock: self.mock.and(header("content-type", content_type)), ..self } @@ -524,29 +515,12 @@ impl<'a> MockUpload<'a> { }))); MatrixMock { server: self.server, mock } } - - /// Returns a send endpoint that emulates a transient failure, i.e responds - /// with error 500. - pub fn error500(self) -> MatrixMock<'a> { - let mock = self.mock.respond_with(ResponseTemplate::new(500)); - MatrixMock { server: self.server, mock } - } - - /// Specify how to respond to a query (viz., like - /// [`MockBuilder::respond_with`] does), when other predefined responses - /// aren't sufficient. - pub fn respond_with(self, func: R) -> MatrixMock<'a> { - MatrixMock { mock: self.mock.respond_with(func), server: self.server } - } } /// A prebuilt mock for resolving a room alias. -pub struct MockResolveRoomAlias<'a> { - server: &'a MockServer, - mock: MockBuilder, -} +pub struct ResolveRoomAliasEndpoint; -impl<'a> MockResolveRoomAlias<'a> { +impl<'a> MockEndpoint<'a, ResolveRoomAliasEndpoint> { /// Returns a data endpoint with a resolved room alias. pub fn ok(self, room_id: &str, servers: Vec) -> MatrixMock<'a> { let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ @@ -564,21 +538,12 @@ impl<'a> MockResolveRoomAlias<'a> { }))); MatrixMock { server: self.server, mock } } - - /// Returns a data endpoint with a server error. - pub fn error500(self) -> MatrixMock<'a> { - let mock = self.mock.respond_with(ResponseTemplate::new(500)); - MatrixMock { server: self.server, mock } - } } /// A prebuilt mock for creating a room alias. -pub struct MockCreateRoomAlias<'a> { - server: &'a MockServer, - mock: MockBuilder, -} +pub struct CreateRoomAliasEndpoint; -impl<'a> MockCreateRoomAlias<'a> { +impl<'a> MockEndpoint<'a, CreateRoomAliasEndpoint> { /// Returns a data endpoint for creating a room alias. pub fn ok(self) -> MatrixMock<'a> { let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); From 5957232e5414138756f6e0bc80dff45cfcdf9fc0 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 7 Nov 2024 16:31:46 +0100 Subject: [PATCH 503/979] task(tests): add a `MockClientBuilder` to help with creating `Client`s connected to a `MatrixMockServer` --- crates/matrix-sdk/src/client/mod.rs | 8 +- crates/matrix-sdk/src/test_utils/mocks.rs | 74 +++++++++++++++-- .../tests/integration/room/attachment/mod.rs | 10 +-- .../tests/integration/room/joined.rs | 4 +- .../tests/integration/send_queue.rs | 81 ++++++++----------- crates/matrix-sdk/tests/integration/widget.rs | 2 +- 6 files changed, 113 insertions(+), 66 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index e8a6f98fb79..ca0e34c3b42 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2989,7 +2989,7 @@ pub(crate) mod tests { #[async_test] async fn test_is_room_alias_available_if_alias_is_not_resolved() { let server = MatrixMockServer::new().await; - let client = logged_in_client(Some(server.server().uri())).await; + let client = server.client_builder().build().await; server.mock_room_directory_resolve_alias().not_found().expect(1).mount().await; @@ -3000,7 +3000,7 @@ pub(crate) mod tests { #[async_test] async fn test_is_room_alias_available_if_alias_is_resolved() { let server = MatrixMockServer::new().await; - let client = logged_in_client(Some(server.server().uri())).await; + let client = server.client_builder().build().await; server .mock_room_directory_resolve_alias() @@ -3016,7 +3016,7 @@ pub(crate) mod tests { #[async_test] async fn test_is_room_alias_available_if_error_found() { let server = MatrixMockServer::new().await; - let client = logged_in_client(Some(server.server().uri())).await; + let client = server.client_builder().build().await; server.mock_room_directory_resolve_alias().error500().expect(1).mount().await; @@ -3027,7 +3027,7 @@ pub(crate) mod tests { #[async_test] async fn test_create_room_alias() { let server = MatrixMockServer::new().await; - let client = logged_in_client(Some(server.server().uri())).await; + let client = server.client_builder().build().await; server.mock_create_room_alias().ok().expect(1).mount().await; diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 3cd2af0123f..7e5d3820678 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -19,20 +19,23 @@ use std::sync::{Arc, Mutex}; -use matrix_sdk_base::deserialized_responses::TimelineEvent; +use matrix_sdk_base::{deserialized_responses::TimelineEvent, store::StoreConfig, SessionMeta}; use matrix_sdk_test::{ test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, }; -use ruma::{MxcUri, OwnedEventId, OwnedRoomId, RoomId}; +use ruma::{api::MatrixVersion, device_id, user_id, MxcUri, OwnedEventId, OwnedRoomId, RoomId}; use serde_json::json; use wiremock::{ matchers::{body_partial_json, header, method, path, path_regex}, Mock, MockBuilder, MockGuard, MockServer, Respond, ResponseTemplate, Times, }; -use super::logged_in_client; -use crate::{Client, Room}; +use crate::{ + config::RequestConfig, + matrix_auth::{MatrixSession, MatrixSessionTokens}, + Client, ClientBuilder, Room, +}; /// A `wiremock` [`MockServer`] along with useful methods to help mocking Matrix /// client-server API endpoints easily. @@ -80,10 +83,10 @@ impl MatrixMockServer { Self { server, sync_response_builder: Default::default() } } - /// Creates a new [`Client`] configured to use this server, preconfigured - /// with a session expected by the server endpoints. - pub async fn make_client(&self) -> Client { - logged_in_client(Some(self.server.uri().to_string())).await + /// Creates a new [`MockClientBuilder`] configured to use this server, + /// preconfigured with a session expected by the server endpoints. + pub fn client_builder(&self) -> MockClientBuilder { + MockClientBuilder::new(self.server.uri()) } /// Return the underlying server. @@ -550,3 +553,58 @@ impl<'a> MockEndpoint<'a, CreateRoomAliasEndpoint> { MatrixMock { server: self.server, mock } } } + +/// An augmented [`ClientBuilder`] that also allows for handling session login. +pub struct MockClientBuilder { + builder: ClientBuilder, + logged_in: bool, +} + +impl MockClientBuilder { + fn new(homeserver: String) -> Self { + let default_builder = Client::builder() + .homeserver_url(homeserver) + .server_versions([MatrixVersion::V1_0]) + .request_config(RequestConfig::new().disable_retry()); + + Self { builder: default_builder, logged_in: true } + } + + /// Doesn't log-in a user. + /// + /// Authenticated requests will fail if this is called. + pub fn unlogged(mut self) -> Self { + self.logged_in = false; + self + } + + /// Provides another [`StoreConfig`] for the underlying [`ClientBuilder`]. + pub fn store_config(mut self, store_config: StoreConfig) -> Self { + self.builder = self.builder.store_config(store_config); + self + } + + /// Finish building the client into the final [`Client`] instance. + pub async fn build(self) -> Client { + let client = self.builder.build().await.expect("building client failed"); + + if self.logged_in { + client + .matrix_auth() + .restore_session(MatrixSession { + meta: SessionMeta { + user_id: user_id!("@example:localhost").to_owned(), + device_id: device_id!("DEVICEID").to_owned(), + }, + tokens: MatrixSessionTokens { + access_token: "1234".to_owned(), + refresh_token: None, + }, + }) + .await + .unwrap(); + } + + client + } +} diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 22c8a356a4f..f81a6f09c67 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -40,7 +40,7 @@ async fn test_room_attachment_send() { .mount() .await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; @@ -82,7 +82,7 @@ async fn test_room_attachment_send_info() { .mount() .await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; @@ -132,7 +132,7 @@ async fn test_room_attachment_send_wrong_info() { .mount() .await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; @@ -191,7 +191,7 @@ async fn test_room_attachment_send_info_thumbnail() { // Second request: return the media MXC. mock.mock_upload().expect_mime_type("image/jpeg").ok(&media_mxc).mock_once().mount().await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; @@ -286,7 +286,7 @@ async fn test_room_attachment_send_mentions() { .mount() .await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; mock.mock_room_state_encryption().plain().mount().await; diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index d8b247685fc..2c81f1caaca 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -721,7 +721,7 @@ async fn test_make_reply_event_doesnt_require_event_cache() { // /event query to get details on an event. let mock = MatrixMockServer::new().await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let user_id = client.user_id().unwrap().to_owned(); let room_id = room_id!("!galette:saucisse.bzh"); @@ -745,7 +745,7 @@ async fn test_make_reply_event_doesnt_require_event_cache() { #[async_test] async fn test_enable_encryption_doesnt_stay_unencrypted() { let mock = MatrixMockServer::new().await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; mock.mock_room_state_encryption().plain().mount().await; mock.mock_set_room_state_encryption().ok(event_id!("$1")).mount().await; diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 6cb96670d2a..51529c9b122 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -3,7 +3,7 @@ use std::{ops::Not as _, sync::Arc, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, Thumbnail}, - config::{RequestConfig, StoreConfig}, + config::StoreConfig, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, send_queue::{ LocalEcho, LocalEchoContent, RoomSendQueueError, RoomSendQueueStorageError, @@ -11,15 +11,12 @@ use matrix_sdk::{ }, test_utils::{ events::EventFactory, - logged_in_client, mocks::{MatrixMock, MatrixMockServer}, - set_client_session, }, - Client, MemoryStore, + MemoryStore, }; use matrix_sdk_test::{async_test, InvitedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder}; use ruma::{ - api::MatrixVersion, event_id, events::{ poll::unstable_start::{ @@ -210,7 +207,7 @@ async fn test_cant_send_invited_room() { // When I'm invited to a room, let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_room(&client, room_id, InvitedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. @@ -226,7 +223,7 @@ async fn test_cant_send_left_room() { // When I've left a room, let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_room(&client, room_id, LeftRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. @@ -244,7 +241,7 @@ async fn test_cant_send_knocked_room() { // When I've knocked into a room, let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_room(&client, room_id, KnockedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. @@ -262,7 +259,7 @@ async fn test_nothing_sent_when_disabled() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; // When I disable the send queue, @@ -294,7 +291,7 @@ async fn test_smoke() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -359,7 +356,7 @@ async fn test_smoke_raw() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -402,7 +399,7 @@ async fn test_smoke_raw() { async fn test_error_then_locally_reenabling() { let mock = MatrixMockServer::new().await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -504,7 +501,7 @@ async fn test_error_then_locally_reenabling() { async fn test_error_then_globally_reenabling() { let mock = MatrixMockServer::new().await; - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -571,7 +568,7 @@ async fn test_reenabling_queue() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let errors = client.send_queue().subscribe_errors(); @@ -644,7 +641,7 @@ async fn test_disjoint_enabled_status() { let room_id1 = room_id!("!a:b.c"); let room_id2 = room_id!("!b:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room1 = mock.sync_joined_room(&client, room_id1).await; let room2 = mock.sync_joined_room(&client, room_id2).await; @@ -679,7 +676,7 @@ async fn test_cancellation() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -796,7 +793,7 @@ async fn test_edit() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -900,7 +897,7 @@ async fn test_edit_with_poll_start() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1028,7 +1025,7 @@ async fn test_edit_while_being_sent_and_fails() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1109,7 +1106,7 @@ async fn test_edit_wakes_the_sending_task() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1159,7 +1156,7 @@ async fn test_abort_after_disable() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let mut errors = client.send_queue().subscribe_errors(); @@ -1217,7 +1214,7 @@ async fn test_abort_or_edit_after_send() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; // Start with an enabled sending queue. @@ -1260,7 +1257,7 @@ async fn test_abort_while_being_sent_and_fails() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1329,7 +1326,7 @@ async fn test_unrecoverable_errors() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let mut errors = client.send_queue().subscribe_errors(); @@ -1391,7 +1388,7 @@ async fn test_unwedge_unrecoverable_errors() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let mut errors = client.send_queue().subscribe_errors(); @@ -1457,8 +1454,8 @@ async fn test_no_network_access_error_is_recoverable() { // server in a static. Using the line below will create a "bare" server, // which is effectively dropped upon `drop()`. let server = wiremock::MockServer::builder().start().await; - let client = logged_in_client(Some(server.uri().to_string())).await; let mock = MatrixMockServer::from_server(server); + let client = mock.client_builder().build().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); @@ -1512,20 +1509,16 @@ async fn test_reloading_rooms_with_unsent_events() { let room_id2 = room_id!("!d:e.f"); let server = wiremock::MockServer::start().await; - let client = Client::builder() - .homeserver_url(server.uri()) - .server_versions([MatrixVersion::V1_0]) + let mock = MatrixMockServer::from_server(server); + + let client = mock + .client_builder() .store_config( StoreConfig::new("cross-process-store-locks-holder-name".to_owned()) .state_store(store.clone()), ) - .request_config(RequestConfig::new().disable_retry()) .build() - .await - .unwrap(); - set_client_session(&client).await; - - let mock = MatrixMockServer::from_server(server); + .await; // Mark two rooms as joined. let room = mock.sync_joined_room(&client, room_id).await; @@ -1570,19 +1563,15 @@ async fn test_reloading_rooms_with_unsent_events() { mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; mock.mock_room_send().ok(event_id!("$2")).mock_once().mount().await; - let client = Client::builder() - .homeserver_url(mock.server().uri()) - .server_versions([MatrixVersion::V1_0]) + let new_client = mock + .client_builder() .store_config( StoreConfig::new("cross-process-store-locks-holder-name".to_owned()).state_store(store), ) - .request_config(RequestConfig::new().disable_retry()) .build() - .await - .unwrap(); - set_client_session(&client).await; + .await; - client.send_queue().respawn_tasks_for_rooms_with_unsent_requests().await; + new_client.send_queue().respawn_tasks_for_rooms_with_unsent_requests().await; // Let the sending queues process events. sleep(Duration::from_secs(1)).await; @@ -1597,7 +1586,7 @@ async fn test_reactions() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1720,7 +1709,7 @@ async fn test_media_uploads() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); @@ -1940,7 +1929,7 @@ async fn test_media_upload_retry() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let client = mock.make_client().await; + let client = mock.client_builder().build().await; let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index 909880dcd40..e03b73a9a62 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -74,7 +74,7 @@ async fn run_test_driver( } } let mock_server = MatrixMockServer::new().await; - let client = mock_server.make_client().await; + let client = mock_server.client_builder().build().await; let room = mock_server.sync_joined_room(&client, &ROOM_ID).await; mock_server.mock_room_state_encryption().plain().mount().await; From 36b96ccef2899767e7d8f7b421647a4e0fa304cd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 7 Nov 2024 16:36:49 +0100 Subject: [PATCH 504/979] task(tests): have the test clients use Matrix v1.12 --- crates/matrix-sdk/src/test_utils/mocks.rs | 20 +-- crates/matrix-sdk/tests/integration/widget.rs | 115 ++++++++---------- 2 files changed, 63 insertions(+), 72 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 7e5d3820678..a4bd0bf716f 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -144,7 +144,7 @@ impl MatrixMockServer { /// Mocks a sync endpoint. pub fn mock_sync(&self) -> MockEndpoint<'_, SyncEndpoint> { let mock = Mock::given(method("GET")) - .and(path("/_matrix/client/r0/sync")) + .and(path("/_matrix/client/v3/sync")) .and(header("authorization", "Bearer 1234")); MockEndpoint { mock, @@ -158,7 +158,7 @@ impl MatrixMockServer { /// Note: works with *any* room. pub fn mock_room_send(&self) -> MockEndpoint<'_, RoomSendEndpoint> { let mock = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/.*")) .and(header("authorization", "Bearer 1234")); MockEndpoint { mock, server: &self.server, endpoint: RoomSendEndpoint } } @@ -169,7 +169,7 @@ impl MatrixMockServer { pub fn mock_room_state_encryption(&self) -> MockEndpoint<'_, EncryptionStateEndpoint> { let mock = Mock::given(method("GET")) .and(header("authorization", "Bearer 1234")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.*room.*encryption.?")); MockEndpoint { mock, server: &self.server, endpoint: EncryptionStateEndpoint } } @@ -179,14 +179,14 @@ impl MatrixMockServer { pub fn mock_set_room_state_encryption(&self) -> MockEndpoint<'_, SetEncryptionStateEndpoint> { let mock = Mock::given(method("PUT")) .and(header("authorization", "Bearer 1234")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.*room.*encryption.?")); MockEndpoint { mock, server: &self.server, endpoint: SetEncryptionStateEndpoint } } /// Creates a prebuilt mock for the room redact endpoint. pub fn mock_room_redact(&self) -> MockEndpoint<'_, RoomRedactEndpoint> { let mock = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/redact/.*?/.*?")) .and(header("authorization", "Bearer 1234")); MockEndpoint { mock, server: &self.server, endpoint: RoomRedactEndpoint } } @@ -204,7 +204,7 @@ impl MatrixMockServer { /// Create a prebuilt mock for uploading media. pub fn mock_upload(&self) -> MockEndpoint<'_, UploadEndpoint> { let mock = Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) + .and(path("/_matrix/media/v3/upload")) .and(header("authorization", "Bearer 1234")); MockEndpoint { mock, server: &self.server, endpoint: UploadEndpoint } } @@ -212,14 +212,14 @@ impl MatrixMockServer { /// Create a prebuilt mock for resolving room aliases. pub fn mock_room_directory_resolve_alias(&self) -> MockEndpoint<'_, ResolveRoomAliasEndpoint> { let mock = - Mock::given(method("GET")).and(path_regex(r"/_matrix/client/r0/directory/room/.*")); + Mock::given(method("GET")).and(path_regex(r"/_matrix/client/v3/directory/room/.*")); MockEndpoint { mock, server: &self.server, endpoint: ResolveRoomAliasEndpoint } } /// Create a prebuilt mock for creating room aliases. pub fn mock_create_room_alias(&self) -> MockEndpoint<'_, CreateRoomAliasEndpoint> { let mock = - Mock::given(method("PUT")).and(path_regex(r"/_matrix/client/r0/directory/room/.*")); + Mock::given(method("PUT")).and(path_regex(r"/_matrix/client/v3/directory/room/.*")); MockEndpoint { mock, server: &self.server, endpoint: CreateRoomAliasEndpoint } } } @@ -495,7 +495,7 @@ impl<'a> MockEndpoint<'a, RoomEventEndpoint> { let mock = self .mock - .and(path_regex(format!("^/_matrix/client/r0/rooms/{room_path}/event/{event_path}"))) + .and(path_regex(format!("^/_matrix/client/v3/rooms/{room_path}/event/{event_path}"))) .respond_with(ResponseTemplate::new(200).set_body_json(event.into_raw().json())); MatrixMock { server: self.server, mock } } @@ -564,7 +564,7 @@ impl MockClientBuilder { fn new(homeserver: String) -> Self { let default_builder = Client::builder() .homeserver_url(homeserver) - .server_versions([MatrixVersion::V1_0]) + .server_versions([MatrixVersion::V1_12]) .request_config(RequestConfig::new().disable_retry()); Self { builder: default_builder, logged_in: true } diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index e03b73a9a62..ecd00353937 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -18,7 +18,6 @@ use assert_matches::assert_matches; use async_trait::async_trait; use futures_util::FutureExt; use matrix_sdk::{ - config::SyncSettings, test_utils::mocks::MatrixMockServer, widget::{ Capabilities, CapabilitiesProvider, WidgetDriver, WidgetDriverHandle, WidgetSettings, @@ -26,9 +25,7 @@ use matrix_sdk::{ Client, }; use matrix_sdk_common::{executor::spawn, timeout::timeout}; -use matrix_sdk_test::{ - async_test, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, -}; +use matrix_sdk_test::{async_test, EventBuilder, JoinedRoomBuilder, ALICE, BOB}; use once_cell::sync::Lazy; use ruma::{ event_id, @@ -50,8 +47,6 @@ use wiremock::{ Mock, ResponseTemplate, }; -use crate::mock_sync; - /// Create a JSON string from a [`json!`][serde_json::json] "literal". #[macro_export] macro_rules! json_string { @@ -246,7 +241,7 @@ async fn test_read_messages() { "start": "t392-516_47314_0_7_1_1_1_11444_1" }); Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/messages$")) .and(header("authorization", "Bearer 1234")) .and(query_param("limit", "2")) .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) @@ -341,7 +336,7 @@ async fn test_read_messages_with_msgtype_capabilities() { "start": "t392-516_47314_0_7_1_1_1_11444_1" }); Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/messages$")) .and(header("authorization", "Bearer 1234")) .and(query_param("limit", "3")) .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) @@ -429,58 +424,54 @@ async fn test_receive_live_events() { // No messages from the driver yet assert_matches!(recv_message(&driver_handle).now_or_never(), None); - let mut sync_builder = SyncResponseBuilder::new(); - // bump the internal batch counter, otherwise the response will be seen as - // identical to the one done in `run_test_driver` - sync_builder.build_json_sync_response(); - - let event_builder = EventBuilder::new(); - sync_builder.add_joined_room( - JoinedRoomBuilder::new(&ROOM_ID) - // text message from alice - matches filter #2 - .add_timeline_event(event_builder.make_sync_message_event( - &ALICE, - RoomMessageEventContent::text_plain("simple text message"), - )) - // emote from alice - doesn't match - .add_timeline_event(event_builder.make_sync_message_event( - &ALICE, - RoomMessageEventContent::emote_plain("emote message"), - )) - // pointless member event - matches filter #4 - .add_timeline_event(event_builder.make_sync_state_event( - user_id!("@example:localhost"), - "@example:localhost", - RoomMemberEventContent::new(MembershipState::Join), - Some(RoomMemberEventContent::new(MembershipState::Join)), - )) - // kick alice - doesn't match because the `#@example:localhost` bit - // is about the state_key, not the sender - .add_timeline_event(event_builder.make_sync_state_event( - user_id!("@example:localhost"), - ALICE.as_str(), - RoomMemberEventContent::new(MembershipState::Ban), - Some(RoomMemberEventContent::new(MembershipState::Join)), - )) - // set room tpoic - doesn't match - .add_timeline_event(event_builder.make_sync_state_event( - &BOB, - "", - RoomTopicEventContent::new("new room topic".to_owned()), - None, - )) - // set room name - matches filter #3 - .add_timeline_event(event_builder.make_sync_state_event( - &BOB, - "", - RoomNameEventContent::new("New Room Name".to_owned()), - None, - )), - ); - - mock_sync(mock_server.server(), sync_builder.build_json_sync_response(), None).await; - let _response = - client.sync_once(SyncSettings::new().timeout(Duration::from_millis(3000))).await.unwrap(); + mock_server + .mock_sync() + .ok_and_run(&client, |sync_builder| { + let event_builder = EventBuilder::new(); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(&ROOM_ID) + // text message from alice - matches filter #2 + .add_timeline_event(event_builder.make_sync_message_event( + &ALICE, + RoomMessageEventContent::text_plain("simple text message"), + )) + // emote from alice - doesn't match + .add_timeline_event(event_builder.make_sync_message_event( + &ALICE, + RoomMessageEventContent::emote_plain("emote message"), + )) + // pointless member event - matches filter #4 + .add_timeline_event(event_builder.make_sync_state_event( + user_id!("@example:localhost"), + "@example:localhost", + RoomMemberEventContent::new(MembershipState::Join), + Some(RoomMemberEventContent::new(MembershipState::Join)), + )) + // kick alice - doesn't match because the `#@example:localhost` bit + // is about the state_key, not the sender + .add_timeline_event(event_builder.make_sync_state_event( + user_id!("@example:localhost"), + ALICE.as_str(), + RoomMemberEventContent::new(MembershipState::Ban), + Some(RoomMemberEventContent::new(MembershipState::Join)), + )) + // set room tpoic - doesn't match + .add_timeline_event(event_builder.make_sync_state_event( + &BOB, + "", + RoomTopicEventContent::new("new room topic".to_owned()), + None, + )) + // set room name - matches filter #3 + .add_timeline_event(event_builder.make_sync_state_event( + &BOB, + "", + RoomNameEventContent::new("New Room Name".to_owned()), + None, + )), + ); + }) + .await; let msg = recv_message(&driver_handle).await; assert_eq!(msg["api"], "toWidget"); @@ -518,7 +509,7 @@ async fn test_send_room_message() { .await; Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/m.room.message/.*$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*$")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) .expect(1) .mount(mock_server.server()) @@ -559,7 +550,7 @@ async fn test_send_room_name() { .await; Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.name/?$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/?$")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) .expect(1) .mount(mock_server.server()) From 8f0f0fa4d4a689a57d748120e143eb4fae662262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 8 Nov 2024 15:56:03 +0100 Subject: [PATCH 505/979] chore(test): Don't require two room IDs in the mock_sync_room method --- crates/matrix-sdk/src/test_utils/mocks.rs | 24 ++++++++++++------- .../tests/integration/send_queue.rs | 6 ++--- .../src/sync_builder/invited_room.rs | 5 ++++ .../src/sync_builder/joined_room.rs | 5 ++++ .../src/sync_builder/knocked_room.rs | 5 ++++ .../src/sync_builder/left_room.rs | 5 ++++ 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index a4bd0bf716f..15ee9080cb4 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -97,13 +97,9 @@ impl MatrixMockServer { /// Overrides the sync/ endpoint with knowledge that the given /// invited/joined/knocked/left room exists, runs a sync and returns the /// given room. - pub async fn sync_room( - &self, - client: &Client, - room_id: &RoomId, - room_data: impl Into, - ) -> Room { + pub async fn sync_room(&self, client: &Client, room_data: impl Into) -> Room { let any_room = room_data.into(); + let room_id = any_room.room_id().to_owned(); self.mock_sync() .ok_and_run(client, move |builder| match any_room { @@ -122,13 +118,13 @@ impl MatrixMockServer { }) .await; - client.get_room(room_id).expect("look at me, the room is known now") + client.get_room(&room_id).expect("look at me, the room is known now") } /// Overrides the sync/ endpoint with knowledge that the given room exists /// in the joined state, runs a sync and returns the given room. pub async fn sync_joined_room(&self, client: &Client, room_id: &RoomId) -> Room { - self.sync_room(client, room_id, JoinedRoomBuilder::new(room_id)).await + self.sync_room(client, JoinedRoomBuilder::new(room_id)).await } /// Verify that the previous mocks expected number of requests match @@ -236,6 +232,18 @@ pub enum AnyRoomBuilder { Knocked(KnockedRoomBuilder), } +impl AnyRoomBuilder { + /// Get the [`RoomId`] of the room this [`AnyRoomBuilder`] will create. + fn room_id(&self) -> &RoomId { + match self { + AnyRoomBuilder::Invited(r) => r.room_id(), + AnyRoomBuilder::Joined(r) => r.room_id(), + AnyRoomBuilder::Left(r) => r.room_id(), + AnyRoomBuilder::Knocked(r) => r.room_id(), + } + } +} + impl From for AnyRoomBuilder { fn from(val: InvitedRoomBuilder) -> AnyRoomBuilder { AnyRoomBuilder::Invited(val) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 51529c9b122..1a9dce61f7c 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -208,7 +208,7 @@ async fn test_cant_send_invited_room() { // When I'm invited to a room, let room_id = room_id!("!a:b.c"); let client = mock.client_builder().build().await; - let room = mock.sync_room(&client, room_id, InvitedRoomBuilder::new(room_id)).await; + let room = mock.sync_room(&client, InvitedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -224,7 +224,7 @@ async fn test_cant_send_left_room() { // When I've left a room, let room_id = room_id!("!a:b.c"); let client = mock.client_builder().build().await; - let room = mock.sync_room(&client, room_id, LeftRoomBuilder::new(room_id)).await; + let room = mock.sync_room(&client, LeftRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -242,7 +242,7 @@ async fn test_cant_send_knocked_room() { // When I've knocked into a room, let room_id = room_id!("!a:b.c"); let client = mock.client_builder().build().await; - let room = mock.sync_room(&client, room_id, KnockedRoomBuilder::new(room_id)).await; + let room = mock.sync_room(&client, KnockedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( diff --git a/testing/matrix-sdk-test/src/sync_builder/invited_room.rs b/testing/matrix-sdk-test/src/sync_builder/invited_room.rs index a5a985e7502..7af107446ae 100644 --- a/testing/matrix-sdk-test/src/sync_builder/invited_room.rs +++ b/testing/matrix-sdk-test/src/sync_builder/invited_room.rs @@ -20,6 +20,11 @@ impl InvitedRoomBuilder { Self { room_id: room_id.to_owned(), inner: Default::default() } } + /// Get the room ID of this [`InvitedRoomBuilder`]. + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + /// Add an event to the state. pub fn add_state_event(mut self, event: StrippedStateTestEvent) -> Self { self.inner.invite_state.events.push(event.into_raw_event()); diff --git a/testing/matrix-sdk-test/src/sync_builder/joined_room.rs b/testing/matrix-sdk-test/src/sync_builder/joined_room.rs index bd93c2461df..079bc04cc41 100644 --- a/testing/matrix-sdk-test/src/sync_builder/joined_room.rs +++ b/testing/matrix-sdk-test/src/sync_builder/joined_room.rs @@ -25,6 +25,11 @@ impl JoinedRoomBuilder { Self { room_id: room_id.to_owned(), inner: Default::default() } } + /// Get the room ID of this [`JoinedRoomBuilder`]. + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + /// Add an event to the timeline. /// /// The raw event can be created with the diff --git a/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs b/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs index 0df9a4e73a6..5838298efea 100644 --- a/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs +++ b/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs @@ -20,6 +20,11 @@ impl KnockedRoomBuilder { Self { room_id: room_id.to_owned(), inner: Default::default() } } + /// Get the room ID of this [`KnockedRoomBuilder`]. + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + /// Add an event to the state. pub fn add_state_event(mut self, event: StrippedStateTestEvent) -> Self { self.inner.knock_state.events.push(event.into_raw_event()); diff --git a/testing/matrix-sdk-test/src/sync_builder/left_room.rs b/testing/matrix-sdk-test/src/sync_builder/left_room.rs index b18ef3df00f..acf271d6682 100644 --- a/testing/matrix-sdk-test/src/sync_builder/left_room.rs +++ b/testing/matrix-sdk-test/src/sync_builder/left_room.rs @@ -22,6 +22,11 @@ impl LeftRoomBuilder { Self { room_id: room_id.to_owned(), inner: Default::default() } } + /// Get the room ID of this [`LeftRoomBuilder`]. + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + /// Add an event to the timeline. /// /// The raw event can be created with the From d446eb933eacc872413a04767903e29832c6a224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 8 Nov 2024 15:59:47 +0100 Subject: [PATCH 506/979] test: Add a bunch examples to the MatrixMockServer docs --- Cargo.lock | 14 + crates/matrix-sdk/Cargo.toml | 1 + crates/matrix-sdk/src/test_utils/mocks.rs | 489 +++++++++++++++++++++- 3 files changed, 495 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be3ee36dc0b..4eecb52161f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2930,6 +2930,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tokio-test", "tokio-util", "tower", "tracing", @@ -5621,6 +5622,19 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.11" diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 27f91ed7ef4..f4b38643154 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -144,6 +144,7 @@ serde_urlencoded = "0.7.1" similar-asserts = { workspace = true } stream_assert = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +tokio-test = "0.4.4" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.33" diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 15ee9080cb4..8d2c3ed9199 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -37,8 +37,8 @@ use crate::{ Client, ClientBuilder, Room, }; -/// A `wiremock` [`MockServer`] along with useful methods to help mocking Matrix -/// client-server API endpoints easily. +/// A [`wiremock`] [`MockServer`] along with useful methods to help mocking +/// Matrix client-server API endpoints easily. /// /// It implements mock endpoints, limiting the shared code as much as possible, /// so the mocks are still flexible to use as scoped/unscoped mounts, named, and @@ -46,21 +46,60 @@ use crate::{ /// /// It works like this: /// -/// - start by saying which endpoint you'd like to mock, e.g. +/// * start by saying which endpoint you'd like to mock, e.g. /// [`Self::mock_room_send()`]. This returns a specialized [`MockEndpoint`] /// data structure, with its own impl. For this example, it's /// `MockEndpoint`. -/// - configure the response on the endpoint-specific mock data structure. For +/// * configure the response on the endpoint-specific mock data structure. For /// instance, if you want the sending to result in a transient failure, call /// [`MockEndpoint::error500`]; if you want it to succeed and return the event /// `$42`, call [`MockEndpoint::ok()`]. It's still possible to call /// [`MockEndpoint::respond_with()`], as we do with wiremock MockBuilder, for /// maximum flexibility when the helpers aren't sufficient. -/// - once the endpoint's response is configured, for any mock builder, you get +/// * once the endpoint's response is configured, for any mock builder, you get /// a [`MatrixMock`]; this is a plain [`wiremock::Mock`] with the server /// curried, so one doesn't have to pass it around when calling /// [`MatrixMock::mount()`] or [`MatrixMock::mount_as_scoped()`]. As such, it /// mostly defers its implementations to [`wiremock::Mock`] under the hood. +/// +/// # Examples +/// +/// ``` +/// # tokio_test::block_on(async { +/// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; +/// use serde_json::json; +/// +/// // First create the mock server and client pair. +/// let mock_server = MatrixMockServer::new().await; +/// let client = mock_server.client_builder().build().await; +/// +/// // Let's say that our rooms are not encrypted. +/// mock_server.mock_room_state_encryption().plain().mount().await; +/// +/// // Let us get a room where we will send an event. +/// let room = mock_server +/// .sync_joined_room(&client, room_id!("!room_id:localhost")) +/// .await; +/// +/// // Now we mock the endpoint so we can actually send the event. +/// let event_id = event_id!("$some_id"); +/// let send_guard = mock_server +/// .mock_room_send() +/// .ok(event_id) +/// .expect(1) +/// .mount_as_scoped() +/// .await; +/// +/// // And we send it out. +/// let response = room.send_raw("m.room.message", json!({ "body": "Hello world" })).await?; +/// +/// assert_eq!( +/// event_id, +/// response.event_id, +/// "The event ID we mocked should match the one we received when we sent the event" +/// ); +/// # anyhow::Ok(()) }); +/// ``` pub struct MatrixMockServer { server: MockServer, @@ -71,14 +110,13 @@ pub struct MatrixMockServer { } impl MatrixMockServer { - /// Create a new `wiremock` server specialized for Matrix usage. + /// Create a new [`wiremock`] server specialized for Matrix usage. pub async fn new() -> Self { let server = MockServer::start().await; Self { server, sync_response_builder: Default::default() } } - /// Creates a new [`MatrixMockServer`] when both parts have been already - /// created. + /// Creates a new [`MatrixMockServer`] from a [`wiremock`] server. pub fn from_server(server: MockServer) -> Self { Self { server, sync_response_builder: Default::default() } } @@ -89,7 +127,7 @@ impl MatrixMockServer { MockClientBuilder::new(self.server.uri()) } - /// Return the underlying server. + /// Return the underlying [`wiremock`] server. pub fn server(&self) -> &MockServer { &self.server } @@ -97,6 +135,21 @@ impl MatrixMockServer { /// Overrides the sync/ endpoint with knowledge that the given /// invited/joined/knocked/left room exists, runs a sync and returns the /// given room. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use matrix_sdk_test::LeftRoomBuilder; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// let left_room = mock_server + /// .sync_room(&client, LeftRoomBuilder::new(room_id!("!room_id:localhost"))) + /// .await; + /// # anyhow::Ok(()) }); pub async fn sync_room(&self, client: &Client, room_data: impl Into) -> Room { let any_room = room_data.into(); let room_id = any_room.room_id().to_owned(); @@ -123,12 +176,56 @@ impl MatrixMockServer { /// Overrides the sync/ endpoint with knowledge that the given room exists /// in the joined state, runs a sync and returns the given room. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// # anyhow::Ok(()) }); pub async fn sync_joined_room(&self, client: &Client, room_id: &RoomId) -> Room { self.sync_room(client, JoinedRoomBuilder::new(room_id)).await } /// Verify that the previous mocks expected number of requests match /// reality, and then cancels all active mocks. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// mock_server.mock_room_send().ok(event_id!("$some_id")).mount().await; + /// + /// // This will succeed. + /// let response = room.send_raw("m.room.message", json!({ "body": "Hello world" })).await?; + /// + /// // Now we reset the mocks. + /// mock_server.verify_and_reset().await; + /// + /// // And we can't send anymore. + /// let response = room + /// .send_raw("m.room.message", json!({ "body": "Hello world" })) + /// .await + /// .expect_err("We removed the mock so sending should now fail"); + /// # anyhow::Ok(()) }); + /// ``` pub async fn verify_and_reset(&self) { self.server.verify().await; self.server.reset().await; @@ -138,6 +235,32 @@ impl MatrixMockServer { // Specific mount endpoints. impl MatrixMockServer { /// Mocks a sync endpoint. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// use matrix_sdk_test::JoinedRoomBuilder; + /// + /// // First create the mock server and client pair. + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// let room_id = room_id!("!room_id:localhost"); + /// + /// // Let's emulate what `MatrixMockServer::sync_joined_room()` does. + /// mock_server + /// .mock_sync() + /// .ok_and_run(&client, |builder| { + /// builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + /// }) + /// .await; + /// + /// let room = client + /// .get_room(room_id) + /// .expect("The room should be available after we mocked the sync"); + /// # anyhow::Ok(()) }); + /// ``` pub fn mock_sync(&self) -> MockEndpoint<'_, SyncEndpoint> { let mock = Mock::given(method("GET")) .and(path("/_matrix/client/v3/sync")) @@ -152,6 +275,40 @@ impl MatrixMockServer { /// Creates a prebuilt mock for sending an event in a room. /// /// Note: works with *any* room. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send() + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response = room.send_raw("m.room.message", json!({ "body": "Hello world" })).await?; + /// + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` pub fn mock_room_send(&self) -> MockEndpoint<'_, RoomSendEndpoint> { let mock = Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/.*")) @@ -162,6 +319,28 @@ impl MatrixMockServer { /// Creates a prebuilt mock for asking whether *a* room is encrypted or not. /// /// Note: Applies to all rooms. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().encrypted().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// assert!( + /// room.is_encrypted().await?, + /// "The room should be marked as encrypted." + /// ); + /// # anyhow::Ok(()) }); + /// ``` pub fn mock_room_state_encryption(&self) -> MockEndpoint<'_, EncryptionStateEndpoint> { let mock = Mock::given(method("GET")) .and(header("authorization", "Bearer 1234")) @@ -172,6 +351,36 @@ impl MatrixMockServer { /// Creates a prebuilt mock for setting the room encryption state. /// /// Note: Applies to all rooms. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{event_id, room_id}, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// mock_server + /// .mock_set_room_state_encryption() + /// .ok(event_id!("$id")) + /// .mock_once() + /// .mount() + /// .await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// room.enable_encryption() + /// .await + /// .expect("We should be able to enable encryption in the room"); + /// # anyhow::Ok(()) }); + /// ``` pub fn mock_set_room_state_encryption(&self) -> MockEndpoint<'_, SetEncryptionStateEndpoint> { let mock = Mock::given(method("PUT")) .and(header("authorization", "Bearer 1234")) @@ -180,6 +389,31 @@ impl MatrixMockServer { } /// Creates a prebuilt mock for the room redact endpoint. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{event_id, room_id}, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// let event_id = event_id!("$id"); + /// + /// mock_server.mock_room_redact().ok(event_id).mock_once().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// room.redact(event_id, None, None) + /// .await + /// .expect("We should be able to redact events in the room"); + /// # anyhow::Ok(()) }); + /// ``` pub fn mock_room_redact(&self) -> MockEndpoint<'_, RoomRedactEndpoint> { let mock = Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/v3/rooms/.*/redact/.*?/.*?")) @@ -351,12 +585,78 @@ impl<'a, T> MockEndpoint<'a, T> { /// Specify how to respond to a query (viz., like /// [`MockBuilder::respond_with`] does), when other predefined responses /// aren't sufficient. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// use wiremock::ResponseTemplate; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send() + /// .respond_with( + /// ResponseTemplate::new(429) + /// .insert_header("Retry-After", "100") + /// .set_body_json(json!({ + /// "errcode": "M_LIMIT_EXCEEDED", + /// "custom_field": "with custom data", + /// }))) + /// .expect(1) + /// .mount() + /// .await; + /// + /// room + /// .send_raw("m.room.message", json!({ "body": "Hello world" })) + /// .await + /// .expect_err("The sending of the event should fail"); + /// # anyhow::Ok(()) }); + /// ``` pub fn respond_with(self, func: R) -> MatrixMock<'a> { MatrixMock { mock: self.mock.respond_with(func), server: self.server } } /// Returns a send endpoint that emulates a transient failure, i.e responds /// with error 500. + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// mock_server + /// .mock_room_send() + /// .error500() + /// .expect(1) + /// .mount() + /// .await; + /// + /// room + /// .send_raw("m.room.message", json!({ "body": "Hello world" })) + /// .await.expect_err("The sending of the event should have failed"); + /// # anyhow::Ok(()) }); + /// ``` pub fn error500(self) -> MatrixMock<'a> { MatrixMock { mock: self.mock.respond_with(ResponseTemplate::new(500)), server: self.server } } @@ -377,18 +677,119 @@ pub struct RoomSendEndpoint; impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// Ensures that the body of the request is a superset of the provided /// `body` parameter. + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{room_id, event_id, events::room::message::RoomMessageEventContent}, + /// test_utils::mocks::MatrixMockServer + /// }; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// let send_guard = mock_server + /// .mock_room_send() + /// .body_matches_partial_json(json!({ + /// "body": "Hello world", + /// })) + /// .ok(event_id) + /// .expect(1) + /// .mount_as_scoped() + /// .await; + /// + /// let content = RoomMessageEventContent::text_plain("Hello world"); + /// let response = room.send(content).await?; + /// + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` pub fn body_matches_partial_json(self, body: serde_json::Value) -> Self { Self { mock: self.mock.and(body_partial_json(body)), ..self } } /// Returns a send endpoint that emulates success, i.e. the event has been /// sent with the given event id. + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// let send_guard = mock_server + /// .mock_room_send() + /// .ok(event_id) + /// .expect(1) + /// .mount_as_scoped() + /// .await; + /// + /// let response = room.send_raw("m.room.message", json!({ "body": "Hello world" })).await?; + /// + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { self.ok_with_event_id(returned_event_id.into()) } /// Returns a send endpoint that emulates a permanent failure (event is too /// large). + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// mock_server + /// .mock_room_send() + /// .error500() + /// .expect(1) + /// .mount() + /// .await; + /// + /// room + /// .send_raw("m.room.message", json!({ "body": "Hello world" })) + /// .await.expect_err("The sending of the event should have failed"); + /// # anyhow::Ok(()) }); + /// ``` pub fn error_too_large(self) -> MatrixMock<'a> { MatrixMock { mock: self.mock.respond_with(ResponseTemplate::new(413).set_body_json(json!({ @@ -410,6 +811,32 @@ impl<'a> MockEndpoint<'a, SyncEndpoint> { /// sync with it. /// /// After calling this function, the sync endpoint isn't mocked anymore. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// use matrix_sdk_test::JoinedRoomBuilder; + /// + /// // First create the mock server and client pair. + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// let room_id = room_id!("!room_id:localhost"); + /// + /// // Let's emulate what `MatrixMockServer::sync_joined_room()` does. + /// mock_server + /// .mock_sync() + /// .ok_and_run(&client, |builder| { + /// builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + /// }) + /// .await; + /// + /// let room = client + /// .get_room(room_id) + /// .expect("The room should be available after we mocked the sync"); + /// # anyhow::Ok(()) }); + /// ``` pub async fn ok_and_run(self, client: &Client, func: F) { let json_response = { let mut builder = self.endpoint.sync_response_builder.lock().unwrap(); @@ -432,6 +859,28 @@ pub struct EncryptionStateEndpoint; impl<'a> MockEndpoint<'a, EncryptionStateEndpoint> { /// Marks the room as encrypted. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().encrypted().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// assert!( + /// room.is_encrypted().await?, + /// "The room should be marked as encrypted." + /// ); + /// # anyhow::Ok(()) }); + /// ``` pub fn encrypted(self) -> MatrixMock<'a> { let mock = self.mock.respond_with( ResponseTemplate::new(200).set_body_json(&*test_json::sync_events::ENCRYPTION_CONTENT), @@ -440,6 +889,28 @@ impl<'a> MockEndpoint<'a, EncryptionStateEndpoint> { } /// Marks the room as not encrypted. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// assert!( + /// !room.is_encrypted().await?, + /// "The room should not be marked as encrypted." + /// ); + /// # anyhow::Ok(()) }); + /// ``` pub fn plain(self) -> MatrixMock<'a> { let mock = self .mock From f341dc41316e4a987dbed8336e0df286634d5891 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 12 Nov 2024 15:00:41 +0100 Subject: [PATCH 507/979] refactor(ffi): remove duplicated fields in media event contents The caption and filenames were weirdly duplicated in each media content, when the expected behavior is well defined: - if there's both a caption and a filename, body := caption, filename is its own field. - if there's only a filename, body := filename. We can remove all duplicated fields, knowing this, and reconstruct the body based on that information. This should make it clearer to FFI users which is what, and provide a clearer API when creating the caption and so on. --- bindings/matrix-sdk-ffi/src/ruma.rs | 93 ++++++++++------------------- crates/matrix-sdk/src/room/mod.rs | 2 +- 2 files changed, 34 insertions(+), 61 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index fa4bdbb95b5..195fae191e9 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -262,6 +262,23 @@ pub enum MessageType { Other { msgtype: String, body: String }, } +/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md +/// If the filename field is present in a media message, clients should treat +/// body as a caption instead of a file name. Otherwise, the body is the +/// file name. +/// +/// So: +/// - if a media has a filename and a caption, the body is the caption, filename +/// is its own field. +/// - if a media only has a filename, then body is the filename. +fn get_body_and_filename(filename: String, caption: Option) -> (String, Option) { + if let Some(caption) = caption { + (caption, Some(filename)) + } else { + (filename, None) + } +} + impl TryFrom for RumaMessageType { type Error = serde_json::Error; @@ -273,35 +290,39 @@ impl TryFrom for RumaMessageType { })) } MessageType::Image { content } => { + let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaImageMessageEventContent::new(content.body, (*content.source).clone()) + RumaImageMessageEventContent::new(body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.raw_filename; + event_content.formatted = content.formatted_caption.map(Into::into); + event_content.filename = filename; Self::Image(event_content) } MessageType::Audio { content } => { + let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaAudioMessageEventContent::new(content.body, (*content.source).clone()) + RumaAudioMessageEventContent::new(body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.raw_filename; + event_content.formatted = content.formatted_caption.map(Into::into); + event_content.filename = filename; Self::Audio(event_content) } MessageType::Video { content } => { + let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaVideoMessageEventContent::new(content.body, (*content.source).clone()) + RumaVideoMessageEventContent::new(body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.raw_filename; + event_content.formatted = content.formatted_caption.map(Into::into); + event_content.filename = filename; Self::Video(event_content) } MessageType::File { content } => { + let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaFileMessageEventContent::new(content.body, (*content.source).clone()) + RumaFileMessageEventContent::new(body, (*content.source).clone()) .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted.map(Into::into); - event_content.filename = content.raw_filename; + event_content.formatted = content.formatted_caption.map(Into::into); + event_content.filename = filename; Self::File(event_content) } MessageType::Notice { content } => { @@ -335,9 +356,6 @@ impl From for MessageType { }, RumaMessageType::Image(c) => MessageType::Image { content: ImageMessageContent { - body: c.body.clone(), - formatted: c.formatted.as_ref().map(Into::into), - raw_filename: c.filename.clone(), filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), @@ -347,9 +365,6 @@ impl From for MessageType { }, RumaMessageType::Audio(c) => MessageType::Audio { content: AudioMessageContent { - body: c.body.clone(), - formatted: c.formatted.as_ref().map(Into::into), - raw_filename: c.filename.clone(), filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), @@ -361,9 +376,6 @@ impl From for MessageType { }, RumaMessageType::Video(c) => MessageType::Video { content: VideoMessageContent { - body: c.body.clone(), - formatted: c.formatted.as_ref().map(Into::into), - raw_filename: c.filename.clone(), filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), @@ -373,9 +385,6 @@ impl From for MessageType { }, RumaMessageType::File(c) => MessageType::File { content: FileMessageContent { - body: c.body.clone(), - formatted: c.formatted.as_ref().map(Into::into), - raw_filename: c.filename.clone(), filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), @@ -452,15 +461,6 @@ pub struct EmoteMessageContent { #[derive(Clone, uniffi::Record)] pub struct ImageMessageContent { - /// The original body field, deserialized from the event. Prefer the use of - /// `filename` and `caption` over this. - pub body: String, - /// The original formatted body field, deserialized from the event. Prefer - /// the use of `filename` and `formatted_caption` over this. - pub formatted: Option, - /// The original filename field, deserialized from the event. Prefer the use - /// of `filename` over this. - pub raw_filename: Option, /// The computed filename, for use in a client. pub filename: String, pub caption: Option, @@ -471,15 +471,6 @@ pub struct ImageMessageContent { #[derive(Clone, uniffi::Record)] pub struct AudioMessageContent { - /// The original body field, deserialized from the event. Prefer the use of - /// `filename` and `caption` over this. - pub body: String, - /// The original formatted body field, deserialized from the event. Prefer - /// the use of `filename` and `formatted_caption` over this. - pub formatted: Option, - /// The original filename field, deserialized from the event. Prefer the use - /// of `filename` over this. - pub raw_filename: Option, /// The computed filename, for use in a client. pub filename: String, pub caption: Option, @@ -492,15 +483,6 @@ pub struct AudioMessageContent { #[derive(Clone, uniffi::Record)] pub struct VideoMessageContent { - /// The original body field, deserialized from the event. Prefer the use of - /// `filename` and `caption` over this. - pub body: String, - /// The original formatted body field, deserialized from the event. Prefer - /// the use of `filename` and `formatted_caption` over this. - pub formatted: Option, - /// The original filename field, deserialized from the event. Prefer the use - /// of `filename` over this. - pub raw_filename: Option, /// The computed filename, for use in a client. pub filename: String, pub caption: Option, @@ -511,15 +493,6 @@ pub struct VideoMessageContent { #[derive(Clone, uniffi::Record)] pub struct FileMessageContent { - /// The original body field, deserialized from the event. Prefer the use of - /// `filename` and `caption` over this. - pub body: String, - /// The original formatted body field, deserialized from the event. Prefer - /// the use of `filename` and `formatted_caption` over this. - pub formatted: Option, - /// The original filename field, deserialized from the event. Prefer the use - /// of `filename` over this. - pub raw_filename: Option, /// The computed filename, for use in a client. pub filename: String, pub caption: Option, diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 3a6eb8072a2..00484d31457 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2052,7 +2052,7 @@ impl Room { ) -> MessageType { // If caption is set, use it as body, and filename as the file name; otherwise, // body is the filename, and the filename is not set. - // https://github.com/tulir/matrix-spec-proposals/blob/body-as-caption/proposals/2530-body-as-caption.md + // https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md let (body, filename) = match caption { Some(caption) => (caption, Some(filename.to_owned())), None => (filename.to_owned(), None), From 9dd2d5ee3cfd2158ae76c01f3964e482d4454162 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 12 Nov 2024 16:08:19 +0100 Subject: [PATCH 508/979] task(architecture): address typo in architecture.md file about `EncryptionSyncService` --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1510aea38a5..c3c10f1e68f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -76,7 +76,7 @@ implement encryption at rest can use those primitives. Very high-level primitives implementing the best practices and cutting-edge Matrix tech: -- `EncryptionService`: a specialized service running simplified sliding sync (MSC4186) for +- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for everything related to crypto and E2EE for the current `Client`. - `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for retrieving the list of current rooms, and exposing its entries. From a920c3fdec314d1094e05b98c3c429d077b59c25 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 13 Nov 2024 09:13:23 +0100 Subject: [PATCH 509/979] fix(ui): Disable `share_pos()` inside `RoomListService`. This patch disables the call to `share_pos()` inside the `RoomListService` because it creates slowness we need to investigate. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 7 ++++--- .../matrix-sdk-ui/tests/integration/room_list_service.rs | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index cde34078ce9..0ac74d6c79a 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -136,9 +136,10 @@ impl RoomListService { })) .with_typing_extension(assign!(http::request::Typing::default(), { enabled: Some(true), - })) - // We don't deal with encryption device messages here so this is safe - .share_pos(); + })); + // TODO: Re-enable once we know it creates slowness. + // // We don't deal with encryption device messages here so this is safe + // .share_pos(); let sliding_sync = builder .add_cached_list( 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 c8b150f7b51..6dd85150f76 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -591,6 +591,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { } #[async_test] +#[ignore] // `share_pos()` has been disabled in the room list, see there to learn more. async fn test_sync_resumes_from_previous_state_after_restart() -> Result<(), Error> { let tmp_dir = TempDir::new().unwrap(); let store_path = tmp_dir.path(); From af84c79e69a2eaa7f24e0c75412f1b7ad283e410 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 13 Nov 2024 08:22:39 +0100 Subject: [PATCH 510/979] feat(base): Make `ObservableMap::stream` works on `wasm32-unknown-unknown`. This patch updates `eyeball-im` and `eyeball-im-util` to integrate https://github.com/jplatte/eyeball/pull/63/. With this new feature, we can have a single implementation of `ObservableMap` (instead of 2: one for all targets, one for `wasm32-u-u`). It makes it possible to get `Client::rooms_stream` available on all targets now. --- Cargo.lock | 5 +- crates/matrix-sdk-base/src/client.rs | 3 - crates/matrix-sdk-base/src/store/mod.rs | 3 - .../src/store/observable_map.rs | 288 +++++++----------- 4 files changed, 108 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4eecb52161f..bd690d3fb3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,14 +1642,13 @@ dependencies = [ [[package]] name = "eyeball-im" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae8c5165c9770f3ec7cccce12f4c5d70f01fa8bf84cf30cfbfd5a1c6f8901d5" +checksum = "a1c02432230060cae0621e15803e073976d22974e0f013c9cb28a4ea1b484629" dependencies = [ "futures-core", "imbl", "tokio", - "tokio-util", "tracing", ] diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index f36178b56fa..8fec8251daf 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -22,9 +22,7 @@ use std::{ }; use eyeball::{SharedObservable, Subscriber}; -#[cfg(not(target_arch = "wasm32"))] use eyeball_im::{Vector, VectorDiff}; -#[cfg(not(target_arch = "wasm32"))] use futures_util::Stream; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::{ @@ -236,7 +234,6 @@ impl BaseClient { /// Get a stream of all the rooms changes, in addition to the existing /// rooms. - #[cfg(not(target_arch = "wasm32"))] pub fn rooms_stream(&self) -> (Vector, impl Stream>>) { self.store.rooms_stream() } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index cfea522adda..1bb92c27c3c 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -29,9 +29,7 @@ use std::{ sync::{Arc, RwLock as StdRwLock}, }; -#[cfg(not(target_arch = "wasm32"))] use eyeball_im::{Vector, VectorDiff}; -#[cfg(not(target_arch = "wasm32"))] use futures_util::Stream; use once_cell::sync::OnceCell; @@ -267,7 +265,6 @@ impl Store { /// Get a stream of all the rooms changes, in addition to the existing /// rooms. - #[cfg(not(target_arch = "wasm32"))] pub fn rooms_stream(&self) -> (Vector, impl Stream>>) { self.rooms.read().unwrap().stream() } diff --git a/crates/matrix-sdk-base/src/store/observable_map.rs b/crates/matrix-sdk-base/src/store/observable_map.rs index 1650d08a160..3392dd6510f 100644 --- a/crates/matrix-sdk-base/src/store/observable_map.rs +++ b/crates/matrix-sdk-base/src/store/observable_map.rs @@ -14,212 +14,137 @@ //! An [`ObservableMap`] implementation. -#[cfg(not(target_arch = "wasm32"))] -mod impl_non_wasm32 { - use std::{borrow::Borrow, collections::HashMap, hash::Hash}; - - use eyeball_im::{ObservableVector, Vector, VectorDiff}; - use futures_util::Stream; - - /// An observable map. - /// - /// This is an “observable map” naive implementation. Just like regular - /// hashmap, we have a redirection from a key to a position, and from a - /// position to a value. The (key, position) tuples are stored in an - /// [`HashMap`]. The (position, value) tuples are stored in an - /// [`ObservableVector`]. The (key, position) tuple is only provided for - /// fast _reading_ implementations, like `Self::get` and - /// `Self::get_or_create`. The (position, value) tuples are observable, - /// this is what interests us the most here. - /// - /// Why not implementing a new `ObservableMap` type in `eyeball-im` instead - /// of this custom implementation? Because we want to continue providing - /// `VectorDiff` when observing the changes, so that the rest of the API in - /// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must - /// produce `MapDiff`, which would be quite different. - /// Plus, we would like to re-use all our existing code, test, stream - /// adapters and so on. - /// - /// This is a trade-off. This implementation is simple enough for the - /// moment, and basically does the job. - #[derive(Debug)] - pub(crate) struct ObservableMap - where - V: Clone + Send + Sync + 'static, - { - /// The (key, position) tuples. - mapping: HashMap, +use std::{borrow::Borrow, collections::HashMap, hash::Hash}; + +use eyeball_im::{ObservableVector, Vector, VectorDiff}; +use futures_util::Stream; + +/// An observable map. +/// +/// This is an “observable map” naive implementation. Just like regular +/// hashmap, we have a redirection from a key to a position, and from a +/// position to a value. The (key, position) tuples are stored in an +/// [`HashMap`]. The (position, value) tuples are stored in an +/// [`ObservableVector`]. The (key, position) tuple is only provided for +/// fast _reading_ implementations, like `Self::get` and +/// `Self::get_or_create`. The (position, value) tuples are observable, +/// this is what interests us the most here. +/// +/// Why not implementing a new `ObservableMap` type in `eyeball-im` instead +/// of this custom implementation? Because we want to continue providing +/// `VectorDiff` when observing the changes, so that the rest of the API in +/// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must +/// produce `MapDiff`, which would be quite different. +/// Plus, we would like to re-use all our existing code, test, stream +/// adapters and so on. +/// +/// This is a trade-off. This implementation is simple enough for the +/// moment, and basically does the job. +#[derive(Debug)] +pub(crate) struct ObservableMap +where + V: Clone + 'static, +{ + /// The (key, position) tuples. + mapping: HashMap, + + /// The values where the indices are the `position` part of + /// `Self::mapping`. + values: ObservableVector, +} - /// The values where the indices are the `position` part of - /// `Self::mapping`. - values: ObservableVector, +impl ObservableMap +where + K: Hash + Eq, + V: Clone + 'static, +{ + /// Create a new `Self`. + pub(crate) fn new() -> Self { + Self { mapping: HashMap::new(), values: ObservableVector::new() } } - impl ObservableMap - where - K: Hash + Eq, - V: Clone + Send + Sync + 'static, - { - /// Create a new `Self`. - pub(crate) fn new() -> Self { - Self { mapping: HashMap::new(), values: ObservableVector::new() } - } - - /// Insert a new `V` in the collection. - /// - /// If the `V` value already exists, it will be updated to the new one. - pub(crate) fn insert(&mut self, key: K, value: V) -> usize { - match self.mapping.get(&key) { - Some(position) => { - self.values.set(*position, value); - - *position - } - None => { - let position = self.values.len(); - - self.values.push_back(value); - self.mapping.insert(key, position); + /// Insert a new `V` in the collection. + /// + /// If the `V` value already exists, it will be updated to the new one. + pub(crate) fn insert(&mut self, key: K, value: V) -> usize { + match self.mapping.get(&key) { + Some(position) => { + self.values.set(*position, value); - position - } + *position } - } + None => { + let position = self.values.len(); - /// Reading one `V` value based on their ID, if it exists. - pub(crate) fn get(&self, key: &L) -> Option<&V> - where - K: Borrow, - L: Hash + Eq + ?Sized, - { - self.mapping.get(key).and_then(|position| self.values.get(*position)) - } + self.values.push_back(value); + self.mapping.insert(key, position); - /// Reading one `V` value based on their ID, or create a new one (by - /// using `default`). - pub(crate) fn get_or_create(&mut self, key: &L, default: F) -> &V - where - K: Borrow, - L: Hash + Eq + ?Sized + ToOwned, - F: FnOnce() -> V, - { - let position = match self.mapping.get(key) { - Some(position) => *position, - None => { - let value = default(); - let position = self.values.len(); - - self.values.push_back(value); - self.mapping.insert(key.to_owned(), position); - - position - } - }; - - self.values - .get(position) - .expect("Value should be present or has just been inserted, but it's missing") - } - - /// Return an iterator over the existing values. - pub(crate) fn iter(&self) -> impl Iterator { - self.values.iter() - } - - /// Get a [`Stream`] of the values. - pub(crate) fn stream(&self) -> (Vector, impl Stream>>) { - self.values.subscribe().into_values_and_batched_stream() - } - - /// Remove a `V` value based on their ID, if it exists. - /// - /// Returns the removed value. - pub(crate) fn remove(&mut self, key: &L) -> Option - where - K: Borrow, - L: Hash + Eq + ?Sized, - { - let position = self.mapping.remove(key)?; - Some(self.values.remove(position)) + position + } } } -} -#[cfg(target_arch = "wasm32")] -mod impl_wasm32 { - use std::{borrow::Borrow, collections::BTreeMap, hash::Hash}; - - /// An observable map for Wasm. It's a simple wrapper around `BTreeMap`. - #[derive(Debug)] - pub(crate) struct ObservableMap(BTreeMap) + /// Reading one `V` value based on their ID, if it exists. + pub(crate) fn get(&self, key: &L) -> Option<&V> where - V: Clone + 'static; + K: Borrow, + L: Hash + Eq + ?Sized, + { + self.mapping.get(key).and_then(|position| self.values.get(*position)) + } - impl ObservableMap + /// Reading one `V` value based on their ID, or create a new one (by + /// using `default`). + pub(crate) fn get_or_create(&mut self, key: &L, default: F) -> &V where - K: Hash + Eq + Ord, - V: Clone + 'static, + K: Borrow, + L: Hash + Eq + ?Sized + ToOwned, + F: FnOnce() -> V, { - /// Create a new `Self`. - pub(crate) fn new() -> Self { - Self(BTreeMap::new()) - } + let position = match self.mapping.get(key) { + Some(position) => *position, + None => { + let value = default(); + let position = self.values.len(); - /// Insert a new `V` in the collection. - /// - /// If the `V` value already exists, it will be updated to the new one. - pub(crate) fn insert(&mut self, key: K, value: V) { - self.0.insert(key, value); - } + self.values.push_back(value); + self.mapping.insert(key.to_owned(), position); - /// Reading one `V` value based on their ID, if it exists. - pub(crate) fn get(&self, key: &L) -> Option<&V> - where - K: Borrow, - L: Hash + Eq + Ord + ?Sized, - { - self.0.get(key) - } + position + } + }; - /// Reading one `V` value based on their ID, or create a new one (by - /// using `default`). - pub(crate) fn get_or_create(&mut self, key: &L, default: F) -> &V - where - K: Borrow, - L: Hash + Eq + ?Sized + ToOwned, - F: FnOnce() -> V, - { - self.0.entry(key.to_owned()).or_insert_with(default) - } + self.values + .get(position) + .expect("Value should be present or has just been inserted, but it's missing") + } - /// Return an iterator over the existing values. - pub(crate) fn iter(&self) -> impl Iterator { - self.0.values() - } + /// Return an iterator over the existing values. + pub(crate) fn iter(&self) -> impl Iterator { + self.values.iter() + } - /// Remove a `V` value based on their ID, if it exists. - /// - /// Returns the removed value. - pub(crate) fn remove(&mut self, key: &L) -> Option - where - K: Borrow, - L: Hash + Eq + Ord + ?Sized, - { - self.0.remove(key) - } + /// Get a [`Stream`] of the values. + pub(crate) fn stream(&self) -> (Vector, impl Stream>>) { + self.values.subscribe().into_values_and_batched_stream() } -} -#[cfg(not(target_arch = "wasm32"))] -pub(crate) use impl_non_wasm32::ObservableMap; -#[cfg(target_arch = "wasm32")] -pub(crate) use impl_wasm32::ObservableMap; + /// Remove a `V` value based on their ID, if it exists. + /// + /// Returns the removed value. + pub(crate) fn remove(&mut self, key: &L) -> Option + where + K: Borrow, + L: Hash + Eq + ?Sized, + { + let position = self.mapping.remove(key)?; + Some(self.values.remove(position)) + } +} #[cfg(test)] mod tests { - #[cfg(not(target_arch = "wasm32"))] use eyeball_im::VectorDiff; - #[cfg(not(target_arch = "wasm32"))] use stream_assert::{assert_closed, assert_next_eq, assert_pending}; use super::ObservableMap; @@ -314,7 +239,6 @@ mod tests { ); } - #[cfg(not(target_arch = "wasm32"))] #[test] fn test_stream() { let mut map = ObservableMap::::new(); From 8f8aad6f4d800cf100727e70096eabec19d042a0 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 13 Nov 2024 11:02:13 +0100 Subject: [PATCH 511/979] chore(cargo): Update `eyeball-im-util` to 0.7.0. --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd690d3fb3f..04ded3f4267 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1654,9 +1654,9 @@ dependencies = [ [[package]] name = "eyeball-im-util" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b6b037e2cdce928a432ecc2880c944e5436d8a38c827974b882ad373f60037" +checksum = "f63a70e454238b5f66a0a0544c3e6a38be765cb01f34da9b94a2f3ecd8777cf8" dependencies = [ "arrayvec", "eyeball-im", diff --git a/Cargo.toml b/Cargo.toml index e05f04ac6b6..1a06ef22aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,8 @@ as_variant = "1.2.0" base64 = "0.22.0" byteorder = "1.4.3" eyeball = { version = "0.8.8", features = ["tracing"] } -eyeball-im = { version = "0.5.0", features = ["tracing"] } -eyeball-im-util = "0.6.0" +eyeball-im = { version = "0.5.1", features = ["tracing"] } +eyeball-im-util = "0.7.0" futures-core = "0.3.28" futures-executor = "0.3.21" futures-util = "0.3.26" From e798a517094dd4bebddb140e18349e7f4db45463 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 12 Nov 2024 15:39:00 +0100 Subject: [PATCH 512/979] feat(sdk): Implement `EventHandlerContext` for tuples. This patch implements `EventHandlerContext` for tuples where each part implements `EventHandlerContext` itself. --- .../matrix-sdk/src/event_handler/context.rs | 35 +++++++++++++++++++ crates/matrix-sdk/src/event_handler/mod.rs | 14 ++++++++ 2 files changed, 49 insertions(+) diff --git a/crates/matrix-sdk/src/event_handler/context.rs b/crates/matrix-sdk/src/event_handler/context.rs index 46213a0a96b..2b921f61fb1 100644 --- a/crates/matrix-sdk/src/event_handler/context.rs +++ b/crates/matrix-sdk/src/event_handler/context.rs @@ -107,3 +107,38 @@ impl Deref for Ctx { &self.0 } } + +// `EventHandlerContext` for tuples. + +impl EventHandlerContext for () { + fn from_data(_data: &EventHandlerData<'_>) -> Option { + Some(()) + } +} + +macro_rules! impl_context_for_tuple { + ( $( $ty:ident ),* $(,)? ) => { + #[allow(non_snake_case)] + impl< $( $ty ),* > EventHandlerContext for ( $( $ty ),* , ) + where + $( $ty : EventHandlerContext, )* + { + fn from_data(data: &EventHandlerData<'_>) -> Option { + $( + let $ty = $ty ::from_data(data)?; + )* + + Some(( $( $ty ),* , )) + } + } + }; +} + +impl_context_for_tuple!(A); +impl_context_for_tuple!(A, B); +impl_context_for_tuple!(A, B, C); +impl_context_for_tuple!(A, B, C, D); +impl_context_for_tuple!(A, B, C, D, E); +impl_context_for_tuple!(A, B, C, D, E, F); +impl_context_for_tuple!(A, B, C, D, E, F, G); +impl_context_for_tuple!(A, B, C, D, E, F, G, H); diff --git a/crates/matrix-sdk/src/event_handler/mod.rs b/crates/matrix-sdk/src/event_handler/mod.rs index c375619811d..0c63f33ee21 100644 --- a/crates/matrix-sdk/src/event_handler/mod.rs +++ b/crates/matrix-sdk/src/event_handler/mod.rs @@ -753,6 +753,20 @@ mod tests { Ok(()) } + #[async_test] + #[allow(dependency_on_unit_never_type_fallback)] + async fn test_add_event_handler_with_tuples() -> crate::Result<()> { + let client = logged_in_client(None).await; + + client.add_event_handler( + |_ev: OriginalSyncRoomMemberEvent, (_room, _client): (Room, Client)| future::ready(()), + ); + + // If it compiles, it works. No need to assert anything. + + Ok(()) + } + #[async_test] #[allow(dependency_on_unit_never_type_fallback)] async fn test_remove_event_handler() -> crate::Result<()> { From 6cef7f20c598cb0b22589dd2e2d784bfeaf2f78f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 12 Nov 2024 18:01:49 +0100 Subject: [PATCH 513/979] feat(sdk): Implement `Client::observe_events` and `Client::observe_room_events`. Changelog: This patch introduces a mechanism similar to `Client::add_event_handler` and `Client::add_room_event_handler` but with a reactive programming pattern. This patch adds `Client::observe_events` and `Client::observe_room_events`. ```rust // Get an observer. let observer = client.observe_events::)>(); // Subscribe to the observer. let mut subscriber = observer.subscribe(); // Use the subscriber as a `Stream`. let (message_event, (room, push_actions)) = subscriber.next().await.unwrap(); ``` When calling `observe_events`, one has to specify the type of event (in the example, `SyncRoomMessageEvent`) and a context (in the example, `(Room, Vec)`, respectively for the room and the push actions). --- Cargo.lock | 1 + crates/matrix-sdk/Cargo.toml | 1 + crates/matrix-sdk/src/client/mod.rs | 93 ++++++- crates/matrix-sdk/src/event_handler/mod.rs | 284 ++++++++++++++++++++- 4 files changed, 373 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04ded3f4267..72c2836a96c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2914,6 +2914,7 @@ dependencies = [ "mime2ext", "once_cell", "openidconnect", + "pin-project-lite", "proptest", "rand", "reqwest", diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index f4b38643154..59b11de6b8c 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -97,6 +97,7 @@ matrix-sdk-sqlite = { workspace = true, optional = true } matrix-sdk-test = { workspace = true, optional = true } mime = "0.3.16" mime2ext = "0.1.52" +pin-project-lite = { workspace = true } rand = { workspace = true , optional = true } ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965", "unstable-msc3930", "unstable-msc3245-v1-compat", "unstable-msc2867"] } serde = { workspace = true } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index ca0e34c3b42..a56c9ace993 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -17,7 +17,7 @@ use std::{ collections::{btree_map, BTreeMap}, fmt::{self, Debug}, - future::Future, + future::{ready, Future}, pin::Pin, sync::{Arc, Mutex as StdMutex, RwLock as StdRwLock, Weak}, }; @@ -88,7 +88,8 @@ use crate::{ error::{HttpError, HttpResult}, event_cache::EventCache, event_handler::{ - EventHandler, EventHandlerDropGuard, EventHandlerHandle, EventHandlerStore, SyncEvent, + EventHandler, EventHandlerContext, EventHandlerDropGuard, EventHandlerHandle, + EventHandlerStore, ObservableEventHandler, SyncEvent, }, http_client::HttpClient, matrix_auth::MatrixAuth, @@ -776,7 +777,7 @@ impl Client { /// ``` pub fn add_event_handler(&self, handler: H) -> EventHandlerHandle where - Ev: SyncEvent + DeserializeOwned + Send + 'static, + Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static, H: EventHandler, { self.add_event_handler_impl(handler, None) @@ -798,12 +799,96 @@ impl Client { handler: H, ) -> EventHandlerHandle where - Ev: SyncEvent + DeserializeOwned + Send + 'static, + Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static, H: EventHandler, { self.add_event_handler_impl(handler, Some(room_id.to_owned())) } + /// Observe a specific event type. + /// + /// `Ev` represents the kind of event that will be observed. `Ctx` + /// represents the context that will come with the event. It relies on the + /// same mechanism as [`Self::add_event_handler`]. The main difference is + /// that it returns an [`ObservableEventHandler`] and doesn't require a + /// user-defined closure. It is possible to subscribe to the + /// [`ObservableEventHandler`] to get an [`EventHandlerSubscriber`], which + /// implements a [`Stream`]. The `Stream::Item` will be of type `(Ev, + /// Ctx)`. + /// + /// # Example + /// + /// ``` + /// use futures_util::StreamExt as _; + /// use matrix_sdk::{ + /// ruma::{events::room::message::SyncRoomMessageEvent, push::Action}, + /// Client, Room, + /// }; + /// + /// # async fn example(client: Client) { + /// let observer = + /// client.observe_events::)>(); + /// + /// let mut subscriber = observer.subscribe(); + /// + /// let (message_event, (room, push_actions)) = + /// subscriber.next().await.unwrap(); + /// # } + /// ``` + /// + /// [`EventHandlerSubscriber`]: crate::event_handler::EventHandlerSubscriber + pub fn observe_events(&self) -> ObservableEventHandler<(Ev, Ctx)> + where + Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static, + Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static, + { + self.observe_room_events_impl(None) + } + + /// Observe a specific room, and event type. + /// + /// This method works the same way as + /// [`observe_events`][Self::observe_events], except that the observability + /// will only be applied for events in the room with the specified ID. + /// See that method for more details. + pub fn observe_room_events( + &self, + room_id: &RoomId, + ) -> ObservableEventHandler<(Ev, Ctx)> + where + Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static, + Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static, + { + self.observe_room_events_impl(Some(room_id.to_owned())) + } + + /// Shared implementation for `Self::observe_events` and + /// `Self::observe_room_events`. + fn observe_room_events_impl( + &self, + room_id: Option, + ) -> ObservableEventHandler<(Ev, Ctx)> + where + Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static, + Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static, + { + // The default value is `None`. It becomes `Some((Ev, Ctx))` once it has a + // new value. + let shared_observable = SharedObservable::new(None); + + ObservableEventHandler::new( + shared_observable.clone(), + self.event_handler_drop_guard(self.add_event_handler_impl( + move |event: Ev, context: Ctx| { + shared_observable.set(Some((event, context))); + + ready(()) + }, + room_id, + )), + ) + } + /// Remove the event handler associated with the handle. /// /// Note that you **must not** call `remove_event_handler` from the diff --git a/crates/matrix-sdk/src/event_handler/mod.rs b/crates/matrix-sdk/src/event_handler/mod.rs index 0c63f33ee21..83bffe2559f 100644 --- a/crates/matrix-sdk/src/event_handler/mod.rs +++ b/crates/matrix-sdk/src/event_handler/mod.rs @@ -40,16 +40,20 @@ use std::{ pin::Pin, sync::{ atomic::{AtomicU64, Ordering::SeqCst}, - RwLock, + Arc, RwLock, Weak, }, + task::{Context, Poll}, }; use anymap2::any::CloneAnySendSync; +use eyeball::{SharedObservable, Subscriber}; +use futures_core::Stream; use futures_util::stream::{FuturesUnordered, StreamExt}; use matrix_sdk_base::{ deserialized_responses::{EncryptionInfo, SyncTimelineEvent}, SendOutsideWasm, SyncOutsideWasm, }; +use pin_project_lite::pin_project; use ruma::{events::AnySyncStateEvent, push::Action, serde::Raw, OwnedRoomId}; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::value::RawValue as RawJsonValue; @@ -287,7 +291,7 @@ impl Client { room_id: Option, ) -> EventHandlerHandle where - Ev: SyncEvent + DeserializeOwned + Send + 'static, + Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static, H: EventHandler, { let handler_fn: Box = Box::new(move |data| { @@ -535,11 +539,139 @@ impl_event_handler!(A, B, C, D, E, F); impl_event_handler!(A, B, C, D, E, F, G); impl_event_handler!(A, B, C, D, E, F, G, H); +/// An observer of events (may be tailored to a room). +/// +/// To create such observer, use [`Client::observe_events`] or +/// [`Client::observe_room_events`]. +#[derive(Debug)] +pub struct ObservableEventHandler { + /// This type is actually nothing more than a thin glue layer between the + /// [`EventHandler`] mechanism and the reactive programming types from + /// [`eyeball`]. Here, we use a [`SharedObservable`] that is updated by the + /// [`EventHandler`]. + shared_observable: SharedObservable>, + + /// This type owns the [`EventHandlerDropGuard`]. As soon as this type goes + /// out of scope, the event handler is unregistered/removed. + /// + /// [`EventHandlerSubscriber`] holds a weak, non-owning reference, to this + /// guard. It is useful to detect when to close the [`Stream`]: as soon as + /// this type goes out of scope, the subscriber will close itself on poll. + event_handler_guard: Arc, +} + +impl ObservableEventHandler { + pub(crate) fn new( + shared_observable: SharedObservable>, + event_handler_guard: EventHandlerDropGuard, + ) -> Self { + Self { shared_observable, event_handler_guard: Arc::new(event_handler_guard) } + } + + /// Subscribe to this observer. + /// + /// It returns an [`EventHandlerSubscriber`], which implements [`Stream`]. + /// See its documentation to learn more. + pub fn subscribe(&self) -> EventHandlerSubscriber { + EventHandlerSubscriber::new( + self.shared_observable.subscribe(), + // The subscriber holds a weak non-owning reference to the event handler guard, so that + // it can detect when this observer is dropped, and can close the subscriber's stream. + Arc::downgrade(&self.event_handler_guard), + ) + } +} + +pin_project! { + /// The subscriber of an [`ObservableEventHandler`]. + /// + /// To create such subscriber, use [`ObservableEventHandler::subscribe`]. + /// + /// This type implements [`Stream`], which means it is possible to poll the + /// next value asynchronously. In other terms, polling this type will return + /// the new event as soon as they are synced. See [`Client::observe_events`] + /// to learn more. + #[derive(Debug)] + pub struct EventHandlerSubscriber { + // The `Subscriber` associated to the `SharedObservable` inside + // `ObservableEventHandle`. + // + // Keep in mind all this API is just a thin glue layer between + // `EventHandle` and `SharedObservable`, that's… maagiic! + #[pin] + subscriber: Subscriber>, + + // A weak non-owning reference to the event handler guard from + // `ObservableEventHandler`. When this type is polled (via its `Stream` + // implementation), it is possible to detect whether the observable has + // been dropped by upgrading this weak reference, and close the `Stream` + // if it needs to. + event_handler_guard: Weak, + } +} + +impl EventHandlerSubscriber { + fn new( + subscriber: Subscriber>, + event_handler_handle: Weak, + ) -> Self { + Self { subscriber, event_handler_guard: event_handler_handle } + } +} + +impl Stream for EventHandlerSubscriber +where + T: Clone, +{ + type Item = T; + + fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + + let Some(_) = this.event_handler_guard.upgrade() else { + // The `EventHandlerHandle` has been dropped via `EventHandlerDropGuard`. It + // means the `ObservableEventHandler` has been dropped. It's time to + // close this stream. + return Poll::Ready(None); + }; + + // First off, the subscriber is of type `Subscriber>` because the + // `SharedObservable` starts with a `None` value to indicate it has no yet + // received any update. We want the `Stream` to return `T`, not `Option`. We + // then filter out all `None` value. + // + // Second, when a `None` value is met, we want to poll again (hence the `loop`). + // At best, there is a new value to return. At worst, the subscriber will return + // `Poll::Pending` and will register the wakers accordingly. + + loop { + match this.subscriber.as_mut().poll_next(context) { + // Stream has been closed somehow. + Poll::Ready(None) => return Poll::Ready(None), + + // The initial value (of the `SharedObservable` behind `self.subscriber`) has been + // polled. We want to filter it out. + Poll::Ready(Some(None)) => { + // Loop over. + continue; + } + + // We have a new value! + Poll::Ready(Some(Some(value))) => return Poll::Ready(Some(value)), + + // Classical pending. + Poll::Pending => return Poll::Pending, + } + } + } +} + #[cfg(test)] mod tests { use matrix_sdk_test::{ async_test, InvitedRoomBuilder, JoinedRoomBuilder, DEFAULT_TEST_ROOM_ID, }; + use stream_assert::{assert_closed, assert_pending, assert_ready}; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); use std::{ @@ -884,4 +1016,152 @@ mod tests { assert_eq!(counter.load(SeqCst), 1); Ok(()) } + + #[async_test] + #[allow(dependency_on_unit_never_type_fallback)] + async fn test_observe_events() -> crate::Result<()> { + let client = logged_in_client(None).await; + + let room_id_0 = room_id!("!r0.matrix.org"); + let room_id_1 = room_id!("!r1.matrix.org"); + + let observable = client.observe_events::(); + + let mut subscriber = observable.subscribe(); + + assert_pending!(subscriber); + + let mut response_builder = SyncResponseBuilder::new(); + let response = response_builder + .add_joined_room(JoinedRoomBuilder::new(room_id_0).add_state_event( + StateTestEvent::Custom(json!({ + "content": { + "name": "Name 0" + }, + "event_id": "$ev0", + "origin_server_ts": 1, + "sender": "@mnt_io:matrix.org", + "state_key": "", + "type": "m.room.name", + "unsigned": { + "age": 1, + } + })), + )) + .build_sync_response(); + client.process_sync(response).await?; + + let (room_name, room) = assert_ready!(subscriber); + + assert_eq!(room_name.event_id.as_str(), "$ev0"); + assert_eq!(room.room_id(), room_id_0); + assert_eq!(room.name().unwrap(), "Name 0"); + + assert_pending!(subscriber); + + let response = response_builder + .add_joined_room(JoinedRoomBuilder::new(room_id_1).add_state_event( + StateTestEvent::Custom(json!({ + "content": { + "name": "Name 1" + }, + "event_id": "$ev1", + "origin_server_ts": 2, + "sender": "@mnt_io:matrix.org", + "state_key": "", + "type": "m.room.name", + "unsigned": { + "age": 2, + } + })), + )) + .build_sync_response(); + client.process_sync(response).await?; + + let (room_name, room) = assert_ready!(subscriber); + + assert_eq!(room_name.event_id.as_str(), "$ev1"); + assert_eq!(room.room_id(), room_id_1); + assert_eq!(room.name().unwrap(), "Name 1"); + + assert_pending!(subscriber); + + drop(observable); + assert_closed!(subscriber); + + Ok(()) + } + + #[async_test] + #[allow(dependency_on_unit_never_type_fallback)] + async fn test_observe_room_events() -> crate::Result<()> { + let client = logged_in_client(None).await; + + let room_id = room_id!("!r0.matrix.org"); + + let observable_for_room = + client.observe_room_events::(room_id); + + let mut subscriber_for_room = observable_for_room.subscribe(); + + assert_pending!(subscriber_for_room); + + let mut response_builder = SyncResponseBuilder::new(); + let response = response_builder + .add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event( + StateTestEvent::Custom(json!({ + "content": { + "name": "Name 0" + }, + "event_id": "$ev0", + "origin_server_ts": 1, + "sender": "@mnt_io:matrix.org", + "state_key": "", + "type": "m.room.name", + "unsigned": { + "age": 1, + } + })), + )) + .build_sync_response(); + client.process_sync(response).await?; + + let (room_name, (room, _client)) = assert_ready!(subscriber_for_room); + + assert_eq!(room_name.event_id.as_str(), "$ev0"); + assert_eq!(room.name().unwrap(), "Name 0"); + + assert_pending!(subscriber_for_room); + + let response = response_builder + .add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event( + StateTestEvent::Custom(json!({ + "content": { + "name": "Name 1" + }, + "event_id": "$ev1", + "origin_server_ts": 2, + "sender": "@mnt_io:matrix.org", + "state_key": "", + "type": "m.room.name", + "unsigned": { + "age": 2, + } + })), + )) + .build_sync_response(); + client.process_sync(response).await?; + + let (room_name, (room, _client)) = assert_ready!(subscriber_for_room); + + assert_eq!(room_name.event_id.as_str(), "$ev1"); + assert_eq!(room.name().unwrap(), "Name 1"); + + assert_pending!(subscriber_for_room); + + drop(observable_for_room); + assert_closed!(subscriber_for_room); + + Ok(()) + } } From 0509236cf8be06b836e3e3a32e40b8a1e6500d5b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 13 Nov 2024 10:47:40 +0100 Subject: [PATCH 514/979] doc(sdk): Improve documentation of `Client::observe_events`. --- crates/matrix-sdk/src/client/mod.rs | 70 ++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index a56c9ace993..b707691fb85 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -682,8 +682,6 @@ impl Client { /// # Examples /// /// ```no_run - /// # use url::Url; - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); /// use matrix_sdk::{ /// deserialized_responses::EncryptionInfo, /// event_handler::Ctx, @@ -700,14 +698,7 @@ impl Client { /// }; /// use serde::{Deserialize, Serialize}; /// - /// # futures_executor::block_on(async { - /// # let client = matrix_sdk::Client::builder() - /// # .homeserver_url(homeserver) - /// # .server_versions([ruma::api::MatrixVersion::V1_0]) - /// # .build() - /// # .await - /// # .unwrap(); - /// # + /// # async fn example(client: Client) { /// client.add_event_handler( /// |ev: SyncRoomMessageEvent, room: Room, client: Client| async move { /// // Common usage: Room event plus room and client. @@ -773,7 +764,7 @@ impl Client { /// client.add_event_handler(move |ev: SyncRoomMessageEvent | async move { /// println!("Calling the handler with identifier {data}"); /// }); - /// # }); + /// # } /// ``` pub fn add_event_handler(&self, handler: H) -> EventHandlerHandle where @@ -809,7 +800,7 @@ impl Client { /// /// `Ev` represents the kind of event that will be observed. `Ctx` /// represents the context that will come with the event. It relies on the - /// same mechanism as [`Self::add_event_handler`]. The main difference is + /// same mechanism as [`Client::add_event_handler`]. The main difference is /// that it returns an [`ObservableEventHandler`] and doesn't require a /// user-defined closure. It is possible to subscribe to the /// [`ObservableEventHandler`] to get an [`EventHandlerSubscriber`], which @@ -818,6 +809,8 @@ impl Client { /// /// # Example /// + /// Let's see a classical usage: + /// /// ``` /// use futures_util::StreamExt as _; /// use matrix_sdk::{ @@ -825,14 +818,50 @@ impl Client { /// Client, Room, /// }; /// - /// # async fn example(client: Client) { + /// # async fn example(client: Client) -> Option<()> { /// let observer = /// client.observe_events::)>(); /// /// let mut subscriber = observer.subscribe(); /// - /// let (message_event, (room, push_actions)) = - /// subscriber.next().await.unwrap(); + /// let (event, (room, push_actions)) = subscriber.next().await?; + /// # Some(()) + /// # } + /// ``` + /// + /// Now let's see how to get several contexts that can be useful for you: + /// + /// ``` + /// use matrix_sdk::{ + /// deserialized_responses::EncryptionInfo, + /// ruma::{ + /// events::room::{ + /// message::SyncRoomMessageEvent, topic::SyncRoomTopicEvent, + /// }, + /// push::Action, + /// }, + /// Client, Room, + /// }; + /// + /// # async fn example(client: Client) { + /// // Observe `SyncRoomMessageEvent` and fetch `Room` + `Client`. + /// let _ = client.observe_events::(); + /// + /// // Observe `SyncRoomMessageEvent` and fetch `Room` + `EncryptionInfo` + /// // to distinguish between unencrypted events and events that were decrypted + /// // by the SDK. + /// let _ = client + /// .observe_events::)>( + /// ); + /// + /// // Observe `SyncRoomMessageEvent` and fetch `Room` + push actions. + /// // For example, an event with `Action::SetTweak(Tweak::Highlight(true))` + /// // should be highlighted in the timeline. + /// let _ = + /// client.observe_events::)>(); + /// + /// // Observe `SyncRoomTopicEvent` and fetch nothing else. + /// let _ = client.observe_events::(); /// # } /// ``` /// @@ -847,10 +876,9 @@ impl Client { /// Observe a specific room, and event type. /// - /// This method works the same way as - /// [`observe_events`][Self::observe_events], except that the observability - /// will only be applied for events in the room with the specified ID. - /// See that method for more details. + /// This method works the same way as [`Client::observe_events`], except + /// that the observability will only be applied for events in the room with + /// the specified ID. See that method for more details. pub fn observe_room_events( &self, room_id: &RoomId, @@ -862,8 +890,8 @@ impl Client { self.observe_room_events_impl(Some(room_id.to_owned())) } - /// Shared implementation for `Self::observe_events` and - /// `Self::observe_room_events`. + /// Shared implementation for `Client::observe_events` and + /// `Client::observe_room_events`. fn observe_room_events_impl( &self, room_id: Option, From 0541ec7e3fcc08393fbc643a5c3238f37116f060 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 12 Nov 2024 16:48:49 +0100 Subject: [PATCH 515/979] refactor(send queue): use `SendHandle` for media uploads too --- crates/matrix-sdk/src/send_queue.rs | 61 +++++++++++----------- crates/matrix-sdk/src/send_queue/upload.rs | 27 +++++----- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index a9322dc239b..8339a1f068f 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -449,7 +449,7 @@ impl RoomSendQueue { let send_handle = SendHandle { room: self.clone(), transaction_id: transaction_id.clone(), - is_upload: false, + media_handles: None, }; let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { @@ -1161,7 +1161,7 @@ impl QueueStorage { send_handle: SendHandle { room: room.clone(), transaction_id: queued.transaction_id, - is_upload: false, + media_handles: None, }, send_error: queued.error, }, @@ -1205,8 +1205,8 @@ impl QueueStorage { DependentQueuedRequestKind::FinishUpload { local_echo, - file_upload: _, - thumbnail_info: _, + file_upload, + thumbnail_info, } => { // Materialize as an event local echo. Some(LocalEcho { @@ -1214,11 +1214,13 @@ impl QueueStorage { content: LocalEchoContent::Event { serialized_event: SerializableEventContent::new(&local_echo.into()) .ok()?, - // TODO this should be a `SendAttachmentHandle`! send_handle: SendHandle { room: room.clone(), transaction_id: dep.own_transaction_id.into(), - is_upload: true, + media_handles: Some(MediaHandles { + upload_thumbnail_txn: thumbnail_info.map(|info| info.txn), + upload_file_txn: file_upload, + }), }, send_error: None, }, @@ -1695,20 +1697,37 @@ pub enum RoomSendQueueStorageError { OperationNotImplementedYet, } +/// Extra transaction IDs useful during an upload. +#[derive(Clone, Debug)] +struct MediaHandles { + /// Transaction id used when uploading the thumbnail. + /// + /// Optional because a media can be uploaded without a thumbnail. + upload_thumbnail_txn: Option, + + /// Transaction id used when uploading the media itself. + upload_file_txn: OwnedTransactionId, +} + /// A handle to manipulate an event that was scheduled to be sent to a room. -// TODO (bnjbvr): consider renaming `SendEventHandle`, unless we can reuse it for medias too. #[derive(Clone, Debug)] pub struct SendHandle { + /// Link to the send queue used to send this request. room: RoomSendQueue, + + /// Transaction id used for the sent request. + /// + /// If this is a media upload, this is the "main" transaction id, i.e. the + /// one used to send the event, and that will be seen by observers. transaction_id: OwnedTransactionId, - // TODO(bnjbvr): remove this, once we have settled the `SendHandle` vs `SendAttachmentHandle` - // situation. - is_upload: bool, + + /// Additional handles for a media upload. + media_handles: Option, } impl SendHandle { fn nyi_for_uploads(&self) -> Result<(), RoomSendQueueStorageError> { - if self.is_upload { + if self.media_handles.is_some() { Err(RoomSendQueueStorageError::OperationNotImplementedYet) } else { Ok(()) @@ -1882,7 +1901,7 @@ impl SendReactionHandle { let handle = SendHandle { room: self.room.clone(), transaction_id: self.transaction_id.clone().into(), - is_upload: false, + media_handles: None, }; handle.abort().await @@ -1894,24 +1913,6 @@ impl SendReactionHandle { } } -/// A handle to execute actions while sending an attachment. -/// -/// In the future, this may support cancellation, subscribing to progress, etc. -#[derive(Clone, Debug)] -pub struct SendAttachmentHandle { - /// Reference to the send queue for the room where this attachment was sent. - _room: RoomSendQueue, - - /// Transaction id for the sending of the event itself. - _transaction_id: OwnedTransactionId, - - /// Transaction id for the file upload. - _file_upload: OwnedTransactionId, - - /// Transaction id for the thumbnail upload. - _thumbnail_transaction_id: Option, -} - /// From a given source of [`DependentQueuedRequest`], return only the most /// meaningful, i.e. the ones that wouldn't be overridden after applying the /// others. diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 4b6fd240373..16a3f7fb657 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -33,11 +33,12 @@ use ruma::{ }; use tracing::{debug, error, instrument, trace, warn, Span}; -use super::{QueueStorage, RoomSendQueue, RoomSendQueueError, SendAttachmentHandle}; +use super::{QueueStorage, RoomSendQueue, RoomSendQueueError}; use crate::{ attachment::AttachmentConfig, send_queue::{ - LocalEcho, LocalEchoContent, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle, + LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate, + SendHandle, }, Client, Room, }; @@ -141,7 +142,7 @@ impl RoomSendQueue { content_type: Mime, data: Vec, mut config: AttachmentConfig, - ) -> Result { + ) -> Result { let Some(room) = self.inner.room.get() else { return Err(RoomSendQueueError::RoomDisappeared); }; @@ -249,27 +250,23 @@ impl RoomSendQueue { self.inner.notifier.notify_one(); + let send_handle = SendHandle { + room: self.clone(), + transaction_id: send_event_txn.clone().into(), + media_handles: Some(MediaHandles { upload_thumbnail_txn, upload_file_txn }), + }; + let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { transaction_id: send_event_txn.clone().into(), content: LocalEchoContent::Event { serialized_event: SerializableEventContent::new(&event_content.into()) .map_err(RoomSendQueueStorageError::JsonSerialization)?, - // TODO: this should be a `SendAttachmentHandle`! - send_handle: SendHandle { - room: self.clone(), - transaction_id: send_event_txn.clone().into(), - is_upload: true, - }, + send_handle: send_handle.clone(), send_error: None, }, })); - Ok(SendAttachmentHandle { - _room: self.clone(), - _transaction_id: send_event_txn.into(), - _file_upload: upload_file_txn, - _thumbnail_transaction_id: upload_thumbnail_txn, - }) + Ok(send_handle) } } From 371e7bc0520e8e49667611148b81d570b040d133 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 12 Nov 2024 17:01:54 +0100 Subject: [PATCH 516/979] task(tests): move `error_too_large` to the generic endpoint So it can be reused in more contexts than just the sending of an event, but also for uploads. --- crates/matrix-sdk/src/test_utils/mocks.rs | 80 +++++++++++------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 8d2c3ed9199..6f923eb73c2 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -669,6 +669,46 @@ impl<'a, T> MockEndpoint<'a, T> { ); MatrixMock { server: self.server, mock } } + + /// Returns an endpoint that emulates a permanent failure error (e.g. event + /// is too large). + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// mock_server + /// .mock_room_send() + /// .error_too_large() + /// .expect(1) + /// .mount() + /// .await; + /// + /// room + /// .send_raw("m.room.message", json!({ "body": "Hello world" })) + /// .await.expect_err("The sending of the event should have failed"); + /// # anyhow::Ok(()) }); + /// ``` + pub fn error_too_large(self) -> MatrixMock<'a> { + MatrixMock { + mock: self.mock.respond_with(ResponseTemplate::new(413).set_body_json(json!({ + // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response + "errcode": "M_TOO_LARGE", + }))), + server: self.server, + } + } } /// A prebuilt mock for sending an event in a room. @@ -759,46 +799,6 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { self.ok_with_event_id(returned_event_id.into()) } - - /// Returns a send endpoint that emulates a permanent failure (event is too - /// large). - /// - /// # Examples - /// ``` - /// # tokio_test::block_on(async { - /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; - /// use serde_json::json; - /// - /// let mock_server = MatrixMockServer::new().await; - /// let client = mock_server.client_builder().build().await; - /// - /// mock_server.mock_room_state_encryption().plain().mount().await; - /// - /// let room = mock_server - /// .sync_joined_room(&client, room_id!("!room_id:localhost")) - /// .await; - /// - /// mock_server - /// .mock_room_send() - /// .error500() - /// .expect(1) - /// .mount() - /// .await; - /// - /// room - /// .send_raw("m.room.message", json!({ "body": "Hello world" })) - /// .await.expect_err("The sending of the event should have failed"); - /// # anyhow::Ok(()) }); - /// ``` - pub fn error_too_large(self) -> MatrixMock<'a> { - MatrixMock { - mock: self.mock.respond_with(ResponseTemplate::new(413).set_body_json(json!({ - // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response - "errcode": "M_TOO_LARGE", - }))), - server: self.server, - } - } } /// A prebuilt mock for running sync v2. From 99b9c50548c1401e592c99a21f33ca36bc0c16e3 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 12 Nov 2024 17:10:55 +0100 Subject: [PATCH 517/979] feat(send queue): implement unwedging for media uploads --- crates/matrix-sdk/src/send_queue.rs | 17 +++++ .../tests/integration/send_queue.rs | 75 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 8339a1f068f..196854d7a7f 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -1816,6 +1816,23 @@ impl SendHandle { .await .map_err(RoomSendQueueError::StorageError)?; + // If we have media handles, also try to unwedge them. + // + // It's fine to always do it to *all* the transaction IDs at once, because only + // one of the three requests will be active at the same time, i.e. only + // one entry will be updated in the store. The other two are either + // done, or dependent requests. + if let Some(handles) = &self.media_handles { + room.queue + .mark_as_unwedged(&handles.upload_file_txn) + .await + .map_err(RoomSendQueueError::StorageError)?; + + if let Some(txn) = &handles.upload_thumbnail_txn { + room.queue.mark_as_unwedged(txn).await.map_err(RoomSendQueueError::StorageError)?; + } + } + // Wake up the queue, in case the room was asleep before unwedging the request. room.notifier.notify_one(); diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 1a9dce61f7c..7097f5ae4e9 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -2010,3 +2010,78 @@ async fn test_media_upload_retry() { // That's all, folks! assert!(watch.is_empty()); } + +#[async_test] +async fn test_unwedging_media_upload() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Create the media to send (no thumbnails). + let filename = "rickroll.gif"; + let content_type = mime::IMAGE_JPEG; + let data = b"Never gonna give you up".to_vec(); + + let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(13)), + width: Some(uint!(37)), + size: Some(uint!(42)), + blurhash: None, + })); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + // Fail for the first attempt with an error indicating the media's too large, + // wedging the upload. + mock.mock_upload().error_too_large().mock_once().mount().await; + + // Send the media. + assert!(watch.is_empty()); + q.send_attachment(filename, content_type, data, config) + .await + .expect("queuing the attachment works"); + + // Observe the local echo. + let (event_txn, send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.body, filename); + + // Although the actual error happens on the file upload transaction id, it must + // be reported with the *event* transaction id. + let error = assert_update!(watch => error { recoverable=false, txn=event_txn }); + let error = error.as_client_api_error().unwrap(); + assert_eq!(error.status_code, 413); + assert!(q.is_enabled()); + + // Mount the mock for the upload and sending the event. + mock.mock_upload().ok(mxc_uri!("mxc://sdk.rs/media")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + + // Unwedge the upload. + send_handle.unwedge().await.unwrap(); + + // Observe the notification for the retry itself. + assert_update!(watch => retry { txn = event_txn }); + + // Observe the upload succeeding at some point. + assert_update!(watch => uploaded { related_to = event_txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + let edit_msg = assert_update!(watch => edit local echo { txn = event_txn }); + assert_let!(MessageType::Image(new_content) = edit_msg.msgtype); + assert_let!(MediaSource::Plain(new_uri) = &new_content.source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/media")); + + // The event is sent, at some point. + assert_update!(watch => sent { txn = event_txn, event_id = event_id!("$1") }); + + // That's all, folks! + assert!(watch.is_empty()); +} From 949cd78d9472bf7b5ca410081a515810301b3483 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Nov 2024 16:51:03 +0100 Subject: [PATCH 518/979] refactor: Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`. --- crates/matrix-sdk-base/src/client.rs | 2 +- crates/matrix-sdk-base/src/event_cache/mod.rs | 15 +++++++++++++++ .../store}/integration_tests.rs | 8 +++++--- .../store}/memory_store.rs | 0 .../store}/mod.rs | 0 .../store}/traits.rs | 0 crates/matrix-sdk-base/src/lib.rs | 2 +- crates/matrix-sdk-base/src/store/mod.rs | 2 +- crates/matrix-sdk-sqlite/src/error.rs | 2 +- crates/matrix-sdk-sqlite/src/event_cache_store.rs | 6 +++--- crates/matrix-sdk/src/client/builder/mod.rs | 2 +- crates/matrix-sdk/src/client/mod.rs | 2 +- crates/matrix-sdk/src/error.rs | 2 +- crates/matrix-sdk/src/send_queue.rs | 4 ++-- 14 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 crates/matrix-sdk-base/src/event_cache/mod.rs rename crates/matrix-sdk-base/src/{event_cache_store => event_cache/store}/integration_tests.rs (97%) rename crates/matrix-sdk-base/src/{event_cache_store => event_cache/store}/memory_store.rs (100%) rename crates/matrix-sdk-base/src/{event_cache_store => event_cache/store}/mod.rs (100%) rename crates/matrix-sdk-base/src/{event_cache_store => event_cache/store}/traits.rs (100%) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 8fec8251daf..a65d3e34ee7 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -70,7 +70,7 @@ use crate::RoomMemberships; use crate::{ deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent}, error::{Error, Result}, - event_cache_store::EventCacheStoreLock, + event_cache::store::EventCacheStoreLock, response_processors::AccountDataProcessor, rooms::{ normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons}, diff --git a/crates/matrix-sdk-base/src/event_cache/mod.rs b/crates/matrix-sdk-base/src/event_cache/mod.rs new file mode 100644 index 00000000000..355d613dd0e --- /dev/null +++ b/crates/matrix-sdk-base/src/event_cache/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod store; diff --git a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs similarity index 97% rename from crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs rename to crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index 6b774a081a9..e1ea613cf28 100644 --- a/crates/matrix-sdk-base/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -193,7 +193,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { /// /// ## Usage Example: /// ```no_run -/// # use matrix_sdk_base::event_cache_store::{ +/// # use matrix_sdk_base::event_cache::store::{ /// # EventCacheStore, /// # MemoryStore as MyStore, /// # Result as EventCacheStoreResult, @@ -217,7 +217,9 @@ macro_rules! event_cache_store_integration_tests { () => { mod event_cache_store_integration_tests { use matrix_sdk_test::async_test; - use $crate::event_cache_store::{EventCacheStoreIntegrationTests, IntoEventCacheStore}; + use $crate::event_cache::store::{ + EventCacheStoreIntegrationTests, IntoEventCacheStore, + }; use super::get_event_cache_store; @@ -249,7 +251,7 @@ macro_rules! event_cache_store_integration_tests_time { use std::time::Duration; use matrix_sdk_test::async_test; - use $crate::event_cache_store::IntoEventCacheStore; + use $crate::event_cache::store::IntoEventCacheStore; use super::get_event_cache_store; diff --git a/crates/matrix-sdk-base/src/event_cache_store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs similarity index 100% rename from crates/matrix-sdk-base/src/event_cache_store/memory_store.rs rename to crates/matrix-sdk-base/src/event_cache/store/memory_store.rs diff --git a/crates/matrix-sdk-base/src/event_cache_store/mod.rs b/crates/matrix-sdk-base/src/event_cache/store/mod.rs similarity index 100% rename from crates/matrix-sdk-base/src/event_cache_store/mod.rs rename to crates/matrix-sdk-base/src/event_cache/store/mod.rs diff --git a/crates/matrix-sdk-base/src/event_cache_store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs similarity index 100% rename from crates/matrix-sdk-base/src/event_cache_store/traits.rs rename to crates/matrix-sdk-base/src/event_cache/store/traits.rs diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index f884448c7a0..0c4f394fd7e 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -28,7 +28,7 @@ mod client; pub mod debug; pub mod deserialized_responses; mod error; -pub mod event_cache_store; +pub mod event_cache; pub mod latest_event; pub mod media; pub mod notification_settings; diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 1bb92c27c3c..c33e3259b23 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -58,7 +58,7 @@ use tokio::sync::{broadcast, Mutex, RwLock}; use tracing::warn; use crate::{ - event_cache_store, + event_cache::store as event_cache_store, rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState}, MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta, }; diff --git a/crates/matrix-sdk-sqlite/src/error.rs b/crates/matrix-sdk-sqlite/src/error.rs index df6a64413f3..4a1eb5be8d5 100644 --- a/crates/matrix-sdk-sqlite/src/error.rs +++ b/crates/matrix-sdk-sqlite/src/error.rs @@ -14,7 +14,7 @@ use deadpool_sqlite::{CreatePoolError, PoolError}; #[cfg(feature = "event-cache")] -use matrix_sdk_base::event_cache_store::EventCacheStoreError; +use matrix_sdk_base::event_cache::store::EventCacheStoreError; #[cfg(feature = "state-store")] use matrix_sdk_base::store::StoreError as StateStoreError; #[cfg(feature = "crypto-store")] diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 87a81657901..c5a21fb0a6b 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc}; use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ - event_cache_store::EventCacheStore, + event_cache::store::EventCacheStore, media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; @@ -279,7 +279,7 @@ mod tests { }; use matrix_sdk_base::{ - event_cache_store::{EventCacheStore, EventCacheStoreError}, + event_cache::store::{EventCacheStore, EventCacheStoreError}, event_cache_store_integration_tests, event_cache_store_integration_tests_time, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, }; @@ -387,7 +387,7 @@ mod encrypted_tests { use std::sync::atomic::{AtomicU32, Ordering::SeqCst}; use matrix_sdk_base::{ - event_cache_store::EventCacheStoreError, event_cache_store_integration_tests, + event_cache::store::EventCacheStoreError, event_cache_store_integration_tests, event_cache_store_integration_tests_time, }; use once_cell::sync::Lazy; diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index a498df9f013..f64ca277730 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -643,7 +643,7 @@ async fn build_indexeddb_store_config( let store_config = { tracing::warn!("The IndexedDB backend does not implement an event cache store, falling back to the in-memory event cache store…"); - store_config.event_cache_store(matrix_sdk_base::event_cache_store::MemoryStore::new()) + store_config.event_cache_store(matrix_sdk_base::event_cache::store::MemoryStore::new()) }; Ok(store_config) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index b707691fb85..9be1fcd7b5c 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -33,7 +33,7 @@ use imbl::Vector; #[cfg(feature = "e2e-encryption")] use matrix_sdk_base::crypto::store::LockableCryptoStore; use matrix_sdk_base::{ - event_cache_store::EventCacheStoreLock, + event_cache::store::EventCacheStoreLock, store::{DynStateStore, ServerCapabilities}, sync::{Notification, RoomUpdates}, BaseClient, RoomInfoNotableUpdate, RoomState, RoomStateFilter, SendOutsideWasm, SessionMeta, diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index fb08d32b4ce..82fe040b845 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -25,7 +25,7 @@ use matrix_sdk_base::crypto::{ CryptoStoreError, DecryptorError, KeyExportError, MegolmError, OlmError, }; use matrix_sdk_base::{ - event_cache_store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState, + event_cache::store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState, StoreError, }; use reqwest::Error as ReqwestError; diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 196854d7a7f..fc419b7b4a1 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -77,7 +77,7 @@ //! no thumbnails): //! //! - The file's content is immediately cached in the -//! [`matrix_sdk_base::event_cache_store::EventCacheStore`], using an MXC ID +//! [`matrix_sdk_base::event_cache::store::EventCacheStore`], using an MXC ID //! that is temporary and designates a local URI without any possible doubt. //! - An initial media event is created and uses this temporary MXC ID, and //! propagated as a local echo for an event. @@ -139,7 +139,7 @@ use std::{ use as_variant::as_variant; use matrix_sdk_base::{ - event_cache_store::EventCacheStoreError, + event_cache::store::EventCacheStoreError, media::MediaRequestParameters, store::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, From c3e28f7e3375931f2fe346977aaf0e47edfb9e0a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 13 Nov 2024 11:19:35 +0100 Subject: [PATCH 519/979] refactor: Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`. --- Cargo.lock | 2 ++ crates/matrix-sdk-base/src/event_cache/mod.rs | 2 ++ crates/matrix-sdk-common/Cargo.toml | 3 +++ crates/matrix-sdk-common/src/lib.rs | 1 + .../src}/linked_chunk/as_vector.rs | 0 .../src}/linked_chunk/mod.rs | 9 +++++---- .../src}/linked_chunk/updates.rs | 0 crates/matrix-sdk/src/event_cache/mod.rs | 1 - crates/matrix-sdk/src/event_cache/pagination.rs | 2 +- crates/matrix-sdk/src/event_cache/room/events.rs | 8 ++++---- 10 files changed, 18 insertions(+), 10 deletions(-) rename crates/{matrix-sdk/src/event_cache => matrix-sdk-common/src}/linked_chunk/as_vector.rs (100%) rename crates/{matrix-sdk/src/event_cache => matrix-sdk-common/src}/linked_chunk/mod.rs (99%) rename crates/{matrix-sdk/src/event_cache => matrix-sdk-common/src}/linked_chunk/updates.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 72c2836a96c..80ca196886a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2984,9 +2984,11 @@ version = "0.7.0" dependencies = [ "assert_matches", "async-trait", + "eyeball-im", "futures-core", "futures-util", "gloo-timers", + "imbl", "js-sys", "matrix-sdk-test", "proptest", diff --git a/crates/matrix-sdk-base/src/event_cache/mod.rs b/crates/matrix-sdk-base/src/event_cache/mod.rs index 355d613dd0e..70bd71760a6 100644 --- a/crates/matrix-sdk-base/src/event_cache/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/mod.rs @@ -12,4 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Event cache store and common types shared with `matrix_sdk::event_cache`. + pub mod store; diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 87d52a1f3fe..e5e0995c083 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -21,7 +21,10 @@ uniffi = ["dep:uniffi"] [dependencies] async-trait = { workspace = true } +eyeball-im = { workspace = true } futures-core = { workspace = true } +futures-util = { workspace = true } +imbl = { workspace = true } ruma = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/matrix-sdk-common/src/lib.rs b/crates/matrix-sdk-common/src/lib.rs index 451a86fa16c..29c6709ec3d 100644 --- a/crates/matrix-sdk-common/src/lib.rs +++ b/crates/matrix-sdk-common/src/lib.rs @@ -25,6 +25,7 @@ pub mod debug; pub mod deserialized_responses; pub mod executor; pub mod failures_cache; +pub mod linked_chunk; pub mod ring_buffer; pub mod store_locks; pub mod timeout; diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs similarity index 100% rename from crates/matrix-sdk/src/event_cache/linked_chunk/as_vector.rs rename to crates/matrix-sdk-common/src/linked_chunk/as_vector.rs diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs similarity index 99% rename from crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs rename to crates/matrix-sdk-common/src/linked_chunk/mod.rs index c3c69a77719..b2894f86469 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. #![allow(dead_code)] +#![allow(rustdoc::private_intra_doc_links)] //! A linked chunk is the underlying data structure that holds all events. @@ -56,7 +57,7 @@ macro_rules! assert_items_eq { let chunk = $iterator .next().expect("next chunk (expect items)"); assert!(chunk.is_items(), "chunk should contain items"); - let $crate::event_cache::linked_chunk::ChunkContent::Items(items) = chunk.content() else { + let $crate::linked_chunk::ChunkContent::Items(items) = chunk.content() else { unreachable!() }; @@ -934,7 +935,6 @@ impl ChunkIdentifierGenerator { #[repr(transparent)] pub struct ChunkIdentifier(u64); -#[cfg(test)] impl PartialEq for ChunkIdentifier { fn eq(&self, other: &u64) -> bool { self.0 == *other @@ -963,7 +963,7 @@ impl Position { /// # Panic /// /// This method will panic if it will underflow, i.e. if the index is 0. - pub(super) fn decrement_index(&mut self) { + pub fn decrement_index(&mut self) { self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0"); } } @@ -1346,7 +1346,8 @@ where } /// A type representing what to do when the system has to handle an empty chunk. -pub(crate) enum EmptyChunk { +#[derive(Debug)] +pub enum EmptyChunk { /// Keep the empty chunk. Keep, diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs b/crates/matrix-sdk-common/src/linked_chunk/updates.rs similarity index 100% rename from crates/matrix-sdk/src/event_cache/linked_chunk/updates.rs rename to crates/matrix-sdk-common/src/linked_chunk/updates.rs diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 7aa31485386..8f0c2f19212 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -54,7 +54,6 @@ use self::paginator::PaginatorError; use crate::{client::WeakClient, Client}; mod deduplicator; -mod linked_chunk; mod pagination; mod room; diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 7eb5d62a84e..c5b605ab6e0 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -18,11 +18,11 @@ use std::{future::Future, ops::ControlFlow, sync::Arc, time::Duration}; use eyeball::Subscriber; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_common::linked_chunk::ChunkContent; use tokio::time::timeout; use tracing::{debug, instrument, trace}; use super::{ - linked_chunk::ChunkContent, paginator::{PaginationResult, PaginatorState}, room::{ events::{Gap, RoomEvents}, diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 0f714e9498b..90d998923ed 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -15,13 +15,13 @@ use std::cmp::Ordering; use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_common::linked_chunk::{ + Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position, +}; use ruma::OwnedEventId; use tracing::{debug, error, warn}; -use super::super::{ - deduplicator::{Decoration, Deduplicator}, - linked_chunk::{Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position}, -}; +use super::super::deduplicator::{Decoration, Deduplicator}; /// An alias for the real event type. pub(crate) type Event = SyncTimelineEvent; From aca83fb4ed7447effd1fe5eb2f9f0cd87bb86c18 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Nov 2024 17:17:09 +0100 Subject: [PATCH 520/979] refactor: Move `Event` and `Gap` into `matrix_sdk_base::event_cache`. --- crates/matrix-sdk-base/src/event_cache/mod.rs | 13 +++++++++++++ crates/matrix-sdk/src/event_cache/room/events.rs | 12 +----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/mod.rs b/crates/matrix-sdk-base/src/event_cache/mod.rs index 70bd71760a6..0b4a80a4d95 100644 --- a/crates/matrix-sdk-base/src/event_cache/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/mod.rs @@ -14,4 +14,17 @@ //! Event cache store and common types shared with `matrix_sdk::event_cache`. +use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + pub mod store; + +/// The kind of event the event storage holds. +pub type Event = SyncTimelineEvent; + +/// The kind of gap the event storage holds. +#[derive(Clone, Debug)] +pub struct Gap { + /// The token to use in the query, extracted from a previous "from" / + /// "end" field of a `/messages` response. + pub prev_token: String, +} diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 90d998923ed..d9168bdca63 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -14,7 +14,7 @@ use std::cmp::Ordering; -use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +pub use matrix_sdk_base::event_cache::{Event, Gap}; use matrix_sdk_common::linked_chunk::{ Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position, }; @@ -23,16 +23,6 @@ use tracing::{debug, error, warn}; use super::super::deduplicator::{Decoration, Deduplicator}; -/// An alias for the real event type. -pub(crate) type Event = SyncTimelineEvent; - -#[derive(Clone, Debug)] -pub struct Gap { - /// The token to use in the query, extracted from a previous "from" / - /// "end" field of a `/messages` response. - pub prev_token: String, -} - const DEFAULT_CHUNK_CAPACITY: usize = 128; /// This type represents all events of a single room. From afaecdc457605ffa4a5dfb2c403705b3e31e3d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 13 Nov 2024 16:12:31 +0100 Subject: [PATCH 521/979] feat(ffi): generate formatted captions for `send_*` media fns Changelog: For `Timeline::send_*` fns, treat the passed `caption` parameter as markdown and use the HTML generated from it as the `formatted_caption` if there is none. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 28 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 3b1ace9d3d2..4162af56582 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -48,8 +48,8 @@ use ruma::{ }, receipt::ReceiptThread, room::message::{ - ForwardThread, LocationMessageEventContent, MessageType, - RoomMessageEventContentWithoutRelation, + FormattedBody as RumaFormattedBody, ForwardThread, LocationMessageEventContent, + MessageType, RoomMessageEventContentWithoutRelation, }, AnyMessageLikeEventContent, }, @@ -289,6 +289,7 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { + let formatted_caption = formatted_caption_from(&caption, &formatted_caption); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_image_info = BaseImageInfo::try_from(&image_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -297,7 +298,7 @@ impl Timeline { let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)? .info(attachment_info) .caption(caption) - .formatted_caption(formatted_caption.map(Into::into)); + .formatted_caption(formatted_caption); self.send_attachment( url, @@ -321,6 +322,7 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { + let formatted_caption = formatted_caption_from(&caption, &formatted_caption); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -351,6 +353,7 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { + let formatted_caption = formatted_caption_from(&caption, &formatted_caption); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -383,6 +386,7 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { + let formatted_caption = formatted_caption_from(&caption, &formatted_caption); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -710,6 +714,24 @@ impl Timeline { } } +/// Given a pair of optional `caption` and `formatted_caption` parameters, +/// return a formatted caption: +/// +/// - If a `formatted_caption` exists, return it. +/// - If it doesn't exist but there is a `caption`, parse it as markdown and +/// return the result. +/// - Return `None` if there are no `caption` or `formatted_caption` parameters. +fn formatted_caption_from( + caption: &Option, + formatted_caption: &Option, +) -> Option { + match (&caption, formatted_caption) { + (None, None) => None, + (Some(body), None) => RumaFormattedBody::markdown(body), + (_, Some(formatted_body)) => Some(formatted_body.clone().into()), + } +} + /// A handle to perform actions onto a local echo. #[derive(uniffi::Object)] pub struct SendHandle { From d614878436a163999592fef2b026d817243918fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 14 Nov 2024 09:38:56 +0100 Subject: [PATCH 522/979] refactor(sdk): move `formatted_caption_from` to the SDK, rename it Add the `markdown` feature to the SDK crate, otherwise we can't use `FormattedBody::markdown`. Refactor the pattern matching into an if, add tests to check its behaviour. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 35 ++++------ crates/matrix-sdk/src/utils.rs | 72 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 4162af56582..99b9032d499 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -48,8 +48,8 @@ use ruma::{ }, receipt::ReceiptThread, room::message::{ - FormattedBody as RumaFormattedBody, ForwardThread, LocationMessageEventContent, - MessageType, RoomMessageEventContentWithoutRelation, + ForwardThread, LocationMessageEventContent, MessageType, + RoomMessageEventContentWithoutRelation, }, AnyMessageLikeEventContent, }, @@ -81,6 +81,7 @@ use crate::{ mod content; pub use content::MessageContent; +use matrix_sdk::utils::formatted_body_from; use crate::error::QueueWedgeError; @@ -289,7 +290,8 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { - let formatted_caption = formatted_caption_from(&caption, &formatted_caption); + let formatted_caption = + formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_image_info = BaseImageInfo::try_from(&image_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -322,7 +324,8 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { - let formatted_caption = formatted_caption_from(&caption, &formatted_caption); + let formatted_caption = + formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -353,7 +356,8 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { - let formatted_caption = formatted_caption_from(&caption, &formatted_caption); + let formatted_caption = + formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -386,7 +390,8 @@ impl Timeline { progress_watcher: Option>, use_send_queue: bool, ) -> Arc { - let formatted_caption = formatted_caption_from(&caption, &formatted_caption); + let formatted_caption = + formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -714,24 +719,6 @@ impl Timeline { } } -/// Given a pair of optional `caption` and `formatted_caption` parameters, -/// return a formatted caption: -/// -/// - If a `formatted_caption` exists, return it. -/// - If it doesn't exist but there is a `caption`, parse it as markdown and -/// return the result. -/// - Return `None` if there are no `caption` or `formatted_caption` parameters. -fn formatted_caption_from( - caption: &Option, - formatted_caption: &Option, -) -> Option { - match (&caption, formatted_caption) { - (None, None) => None, - (Some(body), None) => RumaFormattedBody::markdown(body), - (_, Some(formatted_body)) => Some(formatted_body.clone().into()), - } -} - /// A handle to perform actions onto a local echo. #[derive(uniffi::Object)] pub struct SendHandle { diff --git a/crates/matrix-sdk/src/utils.rs b/crates/matrix-sdk/src/utils.rs index 327c0410176..b9da26e3ae0 100644 --- a/crates/matrix-sdk/src/utils.rs +++ b/crates/matrix-sdk/src/utils.rs @@ -21,6 +21,8 @@ use std::sync::{Arc, RwLock}; use futures_core::Stream; #[cfg(feature = "e2e-encryption")] use futures_util::StreamExt; +#[cfg(feature = "markdown")] +use ruma::events::room::message::FormattedBody; use ruma::{ events::{AnyMessageLikeEventContent, AnyStateEventContent}, serde::Raw, @@ -218,8 +220,32 @@ pub fn is_room_alias_format_valid(alias: String) -> bool { has_valid_format && is_lowercase && RoomAliasId::parse(alias).is_ok() } +/// Given a pair of optional `body` and `formatted_body` parameters, +/// returns a formatted body. +/// +/// Return the formatted body if available, or interpret the `body` parameter as +/// markdown, if provided. +#[cfg(feature = "markdown")] +pub fn formatted_body_from( + body: Option<&str>, + formatted_body: Option, +) -> Option { + if formatted_body.is_some() { + formatted_body + } else { + body.and_then(FormattedBody::markdown) + } +} + #[cfg(test)] mod test { + #[cfg(feature = "markdown")] + use assert_matches2::{assert_let, assert_matches}; + #[cfg(feature = "markdown")] + use ruma::events::room::message::FormattedBody; + + #[cfg(feature = "markdown")] + use crate::utils::formatted_body_from; use crate::utils::is_room_alias_format_valid; #[cfg(feature = "e2e-encryption")] @@ -282,4 +308,50 @@ mod test { fn test_is_room_alias_format_valid_when_has_valid_format() { assert!(is_room_alias_format_valid("#alias.test:domain.org".to_owned())) } + + #[test] + #[cfg(feature = "markdown")] + fn test_formatted_body_from_nothing_returns_none() { + assert_matches!(formatted_body_from(None, None), None); + } + + #[test] + #[cfg(feature = "markdown")] + fn test_formatted_body_from_only_formatted_body_returns_the_formatted_body() { + let formatted_body = FormattedBody::html(r"

Hello!

"); + + assert_let!( + Some(result_formatted_body) = formatted_body_from(None, Some(formatted_body.clone())) + ); + + assert_eq!(formatted_body.body, result_formatted_body.body); + assert_eq!(result_formatted_body.format, result_formatted_body.format); + } + + #[test] + #[cfg(feature = "markdown")] + fn test_formatted_body_from_markdown_body_returns_a_processed_formatted_body() { + let markdown_body = Some(r"# Parsed"); + + assert_let!(Some(result_formatted_body) = formatted_body_from(markdown_body, None)); + + let expected_formatted_body = FormattedBody::html("

Parsed

\n".to_owned()); + assert_eq!(expected_formatted_body.body, result_formatted_body.body); + assert_eq!(expected_formatted_body.format, result_formatted_body.format); + } + + #[test] + #[cfg(feature = "markdown")] + fn test_formatted_body_from_body_and_formatted_body_returns_the_formatted_body() { + let markdown_body = Some(r"# Markdown"); + let formatted_body = FormattedBody::html(r"

HTML

"); + + assert_let!( + Some(result_formatted_body) = + formatted_body_from(markdown_body, Some(formatted_body.clone())) + ); + + assert_eq!(formatted_body.body, result_formatted_body.body); + assert_eq!(formatted_body.format, result_formatted_body.format); + } } From 2872af234b2a663ecf71bfffb1eacfe435383b0e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 13 Nov 2024 13:44:37 +0100 Subject: [PATCH 523/979] test(send queue): add a test for the ordering of media vs other events --- .../tests/integration/send_queue.rs | 124 +++++++++++++----- 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 7097f5ae4e9..476f79bcb58 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -6,7 +6,7 @@ use matrix_sdk::{ config::StoreConfig, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, send_queue::{ - LocalEcho, LocalEchoContent, RoomSendQueueError, RoomSendQueueStorageError, + LocalEcho, LocalEchoContent, RoomSendQueue, RoomSendQueueError, RoomSendQueueStorageError, RoomSendQueueUpdate, }, test_utils::{ @@ -40,6 +40,25 @@ use tokio::{ }; use wiremock::{Request, ResponseTemplate}; +/// Queues an attachment whenever the actual data/mime type etc. don't matter. +/// +/// Returns the filename, for sanity check purposes. +async fn queue_attachment_no_thumbnail(q: &RoomSendQueue) -> &'static str { + let filename = "surprise.jpeg.exe"; + let content_type = mime::IMAGE_JPEG; + let data = b"hello world".to_vec(); + let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(13)), + width: Some(uint!(37)), + size: Some(uint!(42)), + blurhash: None, + })); + q.send_attachment(filename, content_type, data, config) + .await + .expect("queuing the attachment works"); + filename +} + fn mock_jpeg_upload<'a>( mock: &'a MatrixMockServer, mxc: &MxcUri, @@ -1936,18 +1955,6 @@ async fn test_media_upload_retry() { let (local_echoes, mut watch) = q.subscribe().await.unwrap(); assert!(local_echoes.is_empty()); - // Create the media to send (no thumbnails). - let filename = "surprise.jpeg.exe"; - let content_type = mime::IMAGE_JPEG; - let data = b"hello world".to_vec(); - - let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(13)), - width: Some(uint!(37)), - size: Some(uint!(42)), - blurhash: None, - })); - // Prepare endpoints. mock.mock_room_state_encryption().plain().mount().await; @@ -1962,9 +1969,7 @@ async fn test_media_upload_retry() { // Send the media. assert!(watch.is_empty()); - q.send_attachment(filename, content_type, data, config) - .await - .expect("queuing the attachment works"); + let filename = queue_attachment_no_thumbnail(&q).await; // Observe the local echo. let (event_txn, _send_handle, content) = assert_update!(watch => local echo event); @@ -2024,18 +2029,6 @@ async fn test_unwedging_media_upload() { let (local_echoes, mut watch) = q.subscribe().await.unwrap(); assert!(local_echoes.is_empty()); - // Create the media to send (no thumbnails). - let filename = "rickroll.gif"; - let content_type = mime::IMAGE_JPEG; - let data = b"Never gonna give you up".to_vec(); - - let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(13)), - width: Some(uint!(37)), - size: Some(uint!(42)), - blurhash: None, - })); - // Prepare endpoints. mock.mock_room_state_encryption().plain().mount().await; @@ -2045,9 +2038,7 @@ async fn test_unwedging_media_upload() { // Send the media. assert!(watch.is_empty()); - q.send_attachment(filename, content_type, data, config) - .await - .expect("queuing the attachment works"); + let filename = queue_attachment_no_thumbnail(&q).await; // Observe the local echo. let (event_txn, send_handle, content) = assert_update!(watch => local echo event); @@ -2085,3 +2076,74 @@ async fn test_unwedging_media_upload() { // That's all, folks! assert!(watch.is_empty()); } + +#[async_test] +async fn test_media_event_is_sent_in_order() { + // Test that despite happening in multiple requests, sending a media maintains + // the ordering. + + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_upload().ok(mxc_uri!("mxc://sdk.rs/media")).mock_once().mount().await; + + assert!(watch.is_empty()); + + { + // 1. Send a text message that will get wedged. + mock.mock_room_send().error_too_large().mock_once().mount().await; + q.send(RoomMessageEventContent::text_plain("error").into()).await.unwrap(); + let (text_txn, _send_handle) = assert_update!(watch => local echo { body = "error" }); + assert_update!(watch => error { recoverable = false, txn = text_txn }); + } + + // We'll then send a media event, and then a text event with success. + mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$text")).mock_once().mount().await; + + // 2. Queue the media. + let filename = queue_attachment_no_thumbnail(&q).await; + + // 3. Queue the message. + q.send(RoomMessageEventContent::text_plain("hello world").into()).await.unwrap(); + + // Observe the local echo for the media. + let (event_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.body, filename); + + // Observe the local echo for the message. + let (text_txn, _send_handle) = assert_update!(watch => local echo { body = "hello world" }); + + // The media gets uploaded. + assert_update!(watch => uploaded { related_to = event_txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + // The media event gets updated with the final MXC IDs. + assert_update!(watch => edit local echo { txn = event_txn }); + + // This is the main thing we're testing: the media must be effectively sent + // *before* the text message, despite implementation details (the media is + // sent over multiple send queue requests). + + assert_update!(watch => sent { txn = event_txn, event_id = event_id!("$media") }); + assert_update!(watch => sent { txn = text_txn, event_id = event_id!("$text") }); + + // That's all, folks! + assert!(watch.is_empty()); + + // When reopening the send queue, we still see the wedged event. + let (local_echoes, _watch) = q.subscribe().await.unwrap(); + assert_eq!(local_echoes.len(), 1); + assert_let!(LocalEchoContent::Event { send_error, .. } = &local_echoes[0].content); + assert!(send_error.is_some()); +} From c02d8cee77cc0f0ca357c8074acf12241894f176 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 13 Nov 2024 14:20:12 +0100 Subject: [PATCH 524/979] feat!(send queue): add a priority field to maintain ordering of sending Prior to this patch, the send queue would not maintain the ordering of sending a media *then* a text, because it would push back a dependent request graduating into a queued request. The solution implemented here consists in adding a new priority column to the send queue, defaulting to 0 for existing events, and use higher priorities for the media uploads, so they're considered before other requests. A high priority is also used for aggregation events that are sent late, so they're sent as soon as possible, before other subsequent events. --- .../src/store/integration_tests.rs | 78 +++++++++++++++++-- .../matrix-sdk-base/src/store/memory_store.rs | 9 ++- .../matrix-sdk-base/src/store/send_queue.rs | 6 ++ crates/matrix-sdk-base/src/store/traits.rs | 11 ++- .../src/state_store/mod.rs | 14 +++- .../state_store/009_send_queue_priority.sql | 3 + crates/matrix-sdk-sqlite/src/state_store.rs | 23 ++++-- crates/matrix-sdk/src/send_queue.rs | 19 ++++- crates/matrix-sdk/src/send_queue/upload.rs | 9 ++- .../tests/integration/send_queue.rs | 9 ++- 10 files changed, 158 insertions(+), 23 deletions(-) create mode 100644 crates/matrix-sdk-sqlite/migrations/state_store/009_send_queue_priority.sql diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index e439965c416..28c4e90cf4b 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -85,6 +85,8 @@ pub trait StateStoreIntegrationTests { async fn test_display_names_saving(&self); /// Test operations with the send queue. async fn test_send_queue(&self); + /// Test priority of operations with the send queue. + async fn test_send_queue_priority(&self); /// Test operations related to send queue dependents. async fn test_send_queue_dependents(&self); /// Test saving/restoring server capabilities. @@ -1212,7 +1214,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into()) .unwrap(); - self.save_send_queue_request(room_id, txn0.clone(), event0.into()).await.unwrap(); + self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap(); // Reading it will work. let pending = self.load_send_queue_requests(room_id).await.unwrap(); @@ -1236,7 +1238,7 @@ impl StateStoreIntegrationTests for DynStateStore { ) .unwrap(); - self.save_send_queue_request(room_id, txn, event.into()).await.unwrap(); + self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap(); } // Reading all the events should work. @@ -1334,7 +1336,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into()) .unwrap(); - self.save_send_queue_request(room_id2, txn.clone(), event.into()).await.unwrap(); + self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap(); } // Add and remove one event for room3. @@ -1344,7 +1346,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into()) .unwrap(); - self.save_send_queue_request(room_id3, txn.clone(), event.into()).await.unwrap(); + self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap(); self.remove_send_queue_request(room_id3, &txn).await.unwrap(); } @@ -1357,6 +1359,64 @@ impl StateStoreIntegrationTests for DynStateStore { assert!(outstanding_rooms.iter().any(|room| room == room_id2)); } + async fn test_send_queue_priority(&self) { + let room_id = room_id!("!test_send_queue:localhost"); + + // No queued event in store at first. + let events = self.load_send_queue_requests(room_id).await.unwrap(); + assert!(events.is_empty()); + + // Saving one request should work. + let low0_txn = TransactionId::new(); + let ev0 = + SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into()) + .unwrap(); + self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap(); + + // Saving one request with higher priority should work. + let high_txn = TransactionId::new(); + let ev1 = + SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into()) + .unwrap(); + self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap(); + + // Saving another request with the low priority should work. + let low1_txn = TransactionId::new(); + let ev2 = + SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into()) + .unwrap(); + self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap(); + + // The requests should be ordered from higher priority to lower, and when equal, + // should use the insertion order instead. + let pending = self.load_send_queue_requests(room_id).await.unwrap(); + + assert_eq!(pending.len(), 3); + { + assert_eq!(pending[0].transaction_id, high_txn); + + let deserialized = pending[0].as_event().unwrap().deserialize().unwrap(); + assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); + assert_eq!(content.body(), "high"); + } + + { + assert_eq!(pending[1].transaction_id, low0_txn); + + let deserialized = pending[1].as_event().unwrap().deserialize().unwrap(); + assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); + assert_eq!(content.body(), "low0"); + } + + { + assert_eq!(pending[2].transaction_id, low1_txn); + + let deserialized = pending[2].as_event().unwrap().deserialize().unwrap(); + assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized); + assert_eq!(content.body(), "low1"); + } + } + async fn test_send_queue_dependents(&self) { let room_id = room_id!("!test_send_queue_dependents:localhost"); @@ -1365,7 +1425,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into()) .unwrap(); - self.save_send_queue_request(room_id, txn0.clone(), event0.into()).await.unwrap(); + self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap(); // No dependents, to start with. assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty()); @@ -1427,7 +1487,7 @@ impl StateStoreIntegrationTests for DynStateStore { let event1 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into()) .unwrap(); - self.save_send_queue_request(room_id, txn1.clone(), event1.into()).await.unwrap(); + self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap(); self.save_dependent_queued_request( room_id, @@ -1609,6 +1669,12 @@ macro_rules! statestore_integration_tests { store.test_send_queue().await; } + #[async_test] + async fn test_send_queue_priority() { + let store = get_store().await.expect("creating store failed").into_state_store(); + store.test_send_queue_priority().await; + } + #[async_test] async fn test_send_queue_dependents() { let store = get_store().await.expect("creating store failed").into_state_store(); diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 2ba91d6f200..60701a3e5f6 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -807,13 +807,14 @@ impl StateStore for MemoryStore { room_id: &RoomId, transaction_id: OwnedTransactionId, kind: QueuedRequestKind, + priority: usize, ) -> Result<(), Self::Error> { self.send_queue_events .write() .unwrap() .entry(room_id.to_owned()) .or_default() - .push(QueuedRequest { kind, transaction_id, error: None }); + .push(QueuedRequest { kind, transaction_id, error: None, priority }); Ok(()) } @@ -867,7 +868,11 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, ) -> Result, Self::Error> { - Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone()) + let mut ret = + self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone(); + // Inverted order of priority, use stable sort to keep insertion order. + ret.sort_by(|lhs, rhs| rhs.priority.cmp(&lhs.priority)); + Ok(ret) } async fn update_send_queue_request_status( diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index f5f0ccaa7e0..4d3b4a76bb8 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -125,6 +125,12 @@ pub struct QueuedRequest { /// /// `None` if the request is in the queue, waiting to be sent. pub error: Option, + + /// At which priority should this be handled? + /// + /// The bigger the value, the higher the priority at which this request + /// should be handled. + pub priority: usize, } impl QueuedRequest { diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 4dabd9eefac..f60189dd3d3 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -358,6 +358,7 @@ pub trait StateStore: AsyncTraitDeps { room_id: &RoomId, transaction_id: OwnedTransactionId, request: QueuedRequestKind, + priority: usize, ) -> Result<(), Self::Error>; /// Updates a send queue request with the given content, and resets its @@ -390,6 +391,10 @@ pub trait StateStore: AsyncTraitDeps { ) -> Result; /// Loads all the send queue requests for the given room. + /// + /// The resulting vector of queued requests should be ordered from higher + /// priority to lower priority, and respect the insertion order when + /// priorities are equal. async fn load_send_queue_requests( &self, room_id: &RoomId, @@ -641,8 +646,12 @@ impl StateStore for EraseStateStoreError { room_id: &RoomId, transaction_id: OwnedTransactionId, content: QueuedRequestKind, + priority: usize, ) -> Result<(), Self::Error> { - self.0.save_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into) + self.0 + .save_send_queue_request(room_id, transaction_id, content, priority) + .await + .map_err(Into::into) } async fn update_send_queue_request( diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 39156c70a25..372a179c9f9 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -437,6 +437,8 @@ struct PersistedQueuedRequest { pub error: Option, + priority: Option, + // Migrated fields: keep these private, they're not used anymore elsewhere in the code base. /// Deprecated (from old format), now replaced with error field. is_wedged: Option, @@ -459,7 +461,10 @@ impl PersistedQueuedRequest { _ => self.error, }; - Some(QueuedRequest { kind, transaction_id: self.transaction_id, error }) + // By default, events without a priority have a priority of 0. + let priority = self.priority.unwrap_or(0); + + Some(QueuedRequest { kind, transaction_id: self.transaction_id, error, priority }) } } @@ -1329,6 +1334,7 @@ impl_state_store!({ room_id: &RoomId, transaction_id: OwnedTransactionId, kind: QueuedRequestKind, + priority: usize, ) -> Result<()> { let encoded_key = self.encode_key(keys::ROOM_SEND_QUEUE, room_id); @@ -1357,6 +1363,7 @@ impl_state_store!({ error: None, is_wedged: None, event: None, + priority: Some(priority), }); // Save the new vector into db. @@ -1460,11 +1467,14 @@ impl_state_store!({ .get(&encoded_key)? .await?; - let prev = prev.map_or_else( + let mut prev = prev.map_or_else( || Ok(Vec::new()), |val| self.deserialize_value::>(&val), )?; + // Inverted stable ordering on priority. + prev.sort_by(|lhs, rhs| rhs.priority.unwrap_or(0).cmp(&lhs.priority.unwrap_or(0))); + Ok(prev.into_iter().filter_map(PersistedQueuedRequest::into_queued_request).collect()) } diff --git a/crates/matrix-sdk-sqlite/migrations/state_store/009_send_queue_priority.sql b/crates/matrix-sdk-sqlite/migrations/state_store/009_send_queue_priority.sql new file mode 100644 index 00000000000..48913b5381c --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/state_store/009_send_queue_priority.sql @@ -0,0 +1,3 @@ +-- Add a priority column, defaulting to 0 for all events in the send queue. +ALTER TABLE "send_queue_events" + ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0; diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 4f453577651..ab152289059 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -69,7 +69,7 @@ mod keys { /// This is used to figure whether the sqlite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in /// the [`SqliteStateStore::run_migrations`] function.. -const DATABASE_VERSION: u8 = 9; +const DATABASE_VERSION: u8 = 10; /// A sqlite based cryptostore. #[derive(Clone)] @@ -307,6 +307,17 @@ impl SqliteStateStore { .await?; } + if from < 10 && to >= 10 { + conn.with_transaction(move |txn| { + // Run the migration. + txn.execute_batch(include_str!( + "../migrations/state_store/009_send_queue_priority.sql" + ))?; + txn.set_db_version(10) + }) + .await?; + } + Ok(()) } @@ -1685,6 +1696,7 @@ impl StateStore for SqliteStateStore { room_id: &RoomId, transaction_id: OwnedTransactionId, content: QueuedRequestKind, + priority: usize, ) -> Result<(), Self::Error> { let room_id_key = self.encode_key(keys::SEND_QUEUE, room_id); let room_id_value = self.serialize_value(&room_id.to_owned())?; @@ -1699,7 +1711,7 @@ impl StateStore for SqliteStateStore { self.acquire() .await? .with_transaction(move |txn| { - txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content) VALUES (?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content))?; + txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content, priority) VALUES (?, ?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content, priority))?; Ok(()) }) .await @@ -1761,14 +1773,14 @@ impl StateStore for SqliteStateStore { // Note: ROWID is always present and is an auto-incremented integer counter. We // want to maintain the insertion order, so we can sort using it. // Note 2: transaction_id is not encoded, see why in `save_send_queue_event`. - let res: Vec<(String, Vec, Option>)> = self + let res: Vec<(String, Vec, Option>, usize)> = self .acquire() .await? .prepare( - "SELECT transaction_id, content, wedge_reason FROM send_queue_events WHERE room_id = ? ORDER BY ROWID", + "SELECT transaction_id, content, wedge_reason, priority FROM send_queue_events WHERE room_id = ? ORDER BY priority DESC, ROWID", |mut stmt| { stmt.query((room_id,))? - .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))) + .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))) .collect() }, ) @@ -1780,6 +1792,7 @@ impl StateStore for SqliteStateStore { transaction_id: entry.0.into(), kind: self.deserialize_json(&entry.1)?, error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?, + priority: entry.3, }); } diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index fc419b7b4a1..4dfba7918a0 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -837,6 +837,12 @@ struct QueueStorage { } impl QueueStorage { + /// Default priority for a queued request. + const LOW_PRIORITY: usize = 0; + + /// High priority for a queued request that must be handled before others. + const HIGH_PRIORITY: usize = 10; + /// Create a new queue for queuing requests to be sent later. fn new(client: WeakClient, room: OwnedRoomId) -> Self { Self { room_id: room, being_sent: Default::default(), client } @@ -847,7 +853,7 @@ impl QueueStorage { self.client.get().ok_or(RoomSendQueueStorageError::ClientShuttingDown) } - /// Push a new event to be sent in the queue. + /// Push a new event to be sent in the queue, with a default priority of 0. /// /// Returns the transaction id chosen to identify the request. async fn push( @@ -858,7 +864,12 @@ impl QueueStorage { self.client()? .store() - .save_send_queue_request(&self.room_id, transaction_id.clone(), request) + .save_send_queue_request( + &self.room_id, + transaction_id.clone(), + request, + Self::LOW_PRIORITY, + ) .await?; Ok(transaction_id) @@ -1058,6 +1069,7 @@ impl QueueStorage { thumbnail_source: None, // the thumbnail has no thumbnails :) related_to: send_event_txn.clone(), }, + Self::LOW_PRIORITY, ) .await?; @@ -1088,6 +1100,7 @@ impl QueueStorage { thumbnail_source: None, related_to: send_event_txn.clone(), }, + Self::LOW_PRIORITY, ) .await?; @@ -1311,6 +1324,7 @@ impl QueueStorage { &self.room_id, dependent_request.own_transaction_id.into(), serializable.into(), + Self::HIGH_PRIORITY, ) .await .map_err(RoomSendQueueStorageError::StateStoreError)?; @@ -1397,6 +1411,7 @@ impl QueueStorage { &self.room_id, dependent_request.own_transaction_id.into(), serializable.into(), + Self::HIGH_PRIORITY, ) .await .map_err(RoomSendQueueStorageError::StateStoreError)?; diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 16a3f7fb657..d1259c5e57f 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -350,7 +350,12 @@ impl QueueStorage { client .store() - .save_send_queue_request(&self.room_id, event_txn, new_content.into()) + .save_send_queue_request( + &self.room_id, + event_txn, + new_content.into(), + Self::HIGH_PRIORITY, + ) .await .map_err(RoomSendQueueStorageError::StateStoreError)?; @@ -392,7 +397,7 @@ impl QueueStorage { client .store() - .save_send_queue_request(&self.room_id, next_upload_txn, request) + .save_send_queue_request(&self.room_id, next_upload_txn, request, Self::HIGH_PRIORITY) .await .map_err(RoomSendQueueStorageError::StateStoreError)?; diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 476f79bcb58..8815a965971 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -900,13 +900,16 @@ async fn test_edit() { // Let the server process the responses. drop(lock_guard); - // Now the server will process the messages in order. + // The queue sends the first event, without the edit. assert_update!(watch => sent { txn = txn1, }); - assert_update!(watch => sent { txn = txn2, }); - // Let a bit of time to process the edit event sent to the server for txn1. + // The queue sends the edit; we can't check the transaction id because it's + // unknown. assert_update!(watch => sent {}); + // The queue sends the second event. + assert_update!(watch => sent { txn = txn2, }); + assert!(watch.is_empty()); } From 7aa930b81c892c8962d6bb8766cf29b42d0db806 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:55:25 +0100 Subject: [PATCH 525/979] feat(WidgetDriver): Send state from state sync and not from timeline to widget (#4254) --- crates/matrix-sdk/src/widget/matrix.rs | 35 ++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index e30e342d60e..db840dd673e 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -26,8 +26,9 @@ use ruma::{ }, assign, events::{ - AnyMessageLikeEventContent, AnyStateEventContent, AnySyncTimelineEvent, AnyTimelineEvent, - MessageLikeEventType, StateEventType, TimelineEventType, + AnyMessageLikeEventContent, AnyStateEventContent, AnySyncMessageLikeEvent, + AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEventType, + StateEventType, TimelineEventType, }, serde::{from_raw_json_value, Raw}, EventId, RoomId, TransactionId, @@ -177,13 +178,31 @@ impl MatrixDriver { pub(crate) fn events(&self) -> EventReceiver { let (tx, rx) = unbounded_channel(); let room_id = self.room.room_id().to_owned(); - let handle = self.room.add_event_handler(move |raw: Raw| { - let _ = tx.send(attach_room_id(&raw, &room_id)); + + // Get only message like events from the timeline section of the sync. + let _tx = tx.clone(); + let _room_id = room_id.clone(); + let handle_msg_like = + self.room.add_event_handler(move |raw: Raw| { + let _ = _tx.send(attach_room_id(raw.cast_ref(), &_room_id)); + async {} + }); + let drop_guard_msg_like = self.room.client().event_handler_drop_guard(handle_msg_like); + + // Get only all state events from the state section of the sync. + let handle_state = self.room.add_event_handler(move |raw: Raw| { + let _ = tx.send(attach_room_id(raw.cast_ref(), &room_id)); async {} }); - - let drop_guard = self.room.client().event_handler_drop_guard(handle); - EventReceiver { rx, _drop_guard: drop_guard } + let drop_guard_state = self.room.client().event_handler_drop_guard(handle_state); + + // The receiver will get a combination of state and message like events. + // The state events will come from the state section of the sync (to always + // represent current resolved state). All state events in the timeline + // section of the sync will not be forwarded to the widget. + // TODO annotate the events and send both timeline and state section state + // events. + EventReceiver { rx, _drop_guards: [drop_guard_msg_like, drop_guard_state] } } } @@ -191,7 +210,7 @@ impl MatrixDriver { /// along with the drop guard for the room event handler. pub(crate) struct EventReceiver { rx: UnboundedReceiver>, - _drop_guard: EventHandlerDropGuard, + _drop_guards: [EventHandlerDropGuard; 2], } impl EventReceiver { From 8fa07ec22d6f1e543470a7049a879f854df98ffb Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 12 Nov 2024 18:02:12 +0100 Subject: [PATCH 526/979] task(send queue): `being_sent` is an `Option`, not a set anymore There can be at most one thing being sent by the send queue, so make this super explicit. --- crates/matrix-sdk/src/send_queue.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 4dfba7918a0..240790136b2 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -129,7 +129,7 @@ //! remembered and fixed up into the media event, just before sending it. use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, + collections::{BTreeMap, HashMap}, str::FromStr as _, sync::{ atomic::{AtomicBool, Ordering}, @@ -833,7 +833,7 @@ struct QueueStorage { /// All the queued requests that are being sent at the moment. /// /// It also serves as an internal lock on the storage backend. - being_sent: Arc>>, + being_sent: Arc>>, } impl QueueStorage { @@ -887,7 +887,8 @@ impl QueueStorage { self.client()?.store().load_send_queue_requests(&self.room_id).await?; if let Some(request) = queued_requests.iter().find(|queued| !queued.is_wedged()) { - being_sent.insert(request.transaction_id.clone()); + let prev = being_sent.replace(request.transaction_id.clone()); + assert!(prev.is_none()); Ok(Some(request.clone())) } else { @@ -899,7 +900,8 @@ impl QueueStorage { /// with the given transaction id as not being sent anymore, so it can /// be removed from the queue later. async fn mark_as_not_being_sent(&self, transaction_id: &TransactionId) { - self.being_sent.write().await.remove(transaction_id); + let was_being_sent = self.being_sent.write().await.take(); + assert_eq!(was_being_sent.as_deref(), Some(transaction_id)); } /// Marks a request popped with [`Self::peek_next_to_send`] and identified @@ -912,7 +914,8 @@ impl QueueStorage { ) -> Result<(), RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; - being_sent.remove(transaction_id); + let was_being_sent = being_sent.take(); + assert_eq!(was_being_sent.as_deref(), Some(transaction_id)); Ok(self .client()? @@ -943,7 +946,8 @@ impl QueueStorage { ) -> Result<(), RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; - being_sent.remove(transaction_id); + let was_being_sent = being_sent.take(); + assert_eq!(was_being_sent.as_deref(), Some(transaction_id)); let client = self.client()?; let store = client.store(); @@ -973,7 +977,7 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let being_sent = self.being_sent.read().await; - if being_sent.contains(transaction_id) { + if being_sent.as_deref() == Some(transaction_id) { // Save the intent to redact the event. self.client()? .store() @@ -1008,7 +1012,7 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let being_sent = self.being_sent.read().await; - if being_sent.contains(transaction_id) { + if being_sent.as_deref() == Some(transaction_id) { // Save the intent to edit the associated event. self.client()? .store() From 50db563363f18088bbfb17546b7d281e1a5c12e4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 12 Nov 2024 18:06:32 +0100 Subject: [PATCH 527/979] feat(send queue): allow aborting media uploads --- .../src/event_cache/store/traits.rs | 3 + crates/matrix-sdk/src/send_queue.rs | 232 +++++++++++++----- crates/matrix-sdk/src/send_queue/upload.rs | 148 ++++++++++- .../tests/integration/send_queue.rs | 9 +- 4 files changed, 308 insertions(+), 84 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 6742851870d..e52ad8b8b2e 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -98,6 +98,9 @@ pub trait EventCacheStore: AsyncTraitDeps { /// Remove all the media files' content associated to an `MxcUri` from the /// media store. /// + /// This should not raise an error when the `uri` parameter points to an + /// unknown media, and it should return an Ok result in this case. + /// /// # Arguments /// /// * `uri` - The `MxcUri` of the media files. diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 240790136b2..9f7befe4df5 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -161,7 +161,7 @@ use ruma::{ serde::Raw, OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, }; -use tokio::sync::{broadcast, Notify, RwLock}; +use tokio::sync::{broadcast, oneshot, Notify, RwLock}; use tracing::{debug, error, info, instrument, trace, warn}; #[cfg(feature = "e2e-encryption")] @@ -542,7 +542,7 @@ impl RoomSendQueue { continue; } - let queued_request = match queue.peek_next_to_send().await { + let (queued_request, cancel_upload_rx) = match queue.peek_next_to_send().await { Ok(Some(request)) => request, Ok(None) => { @@ -571,8 +571,9 @@ impl RoomSendQueue { continue; }; - match Self::handle_request(&room, queued_request).await { - Ok(parent_key) => match queue.mark_as_sent(&txn_id, parent_key.clone()).await { + match Self::handle_request(&room, queued_request, cancel_upload_rx).await { + Ok(Some(parent_key)) => match queue.mark_as_sent(&txn_id, parent_key.clone()).await + { Ok(()) => match parent_key { SentRequestKey::Event(event_id) => { let _ = updates.send(RoomSendQueueUpdate::SentEvent { @@ -594,6 +595,10 @@ impl RoomSendQueue { } }, + Ok(None) => { + debug!("Request has been aborted while running, continuing."); + } + Err(err) => { let is_recoverable = match err { crate::Error::Http(ref http_err) => { @@ -661,11 +666,14 @@ impl RoomSendQueue { info!("exited sending task"); } - /// Handles a single request and returns the [`SentRequestKey`] on success. + /// Handles a single request and returns the [`SentRequestKey`] on success + /// (unless the request was cancelled, in which case it'll return + /// `None`). async fn handle_request( room: &Room, request: QueuedRequest, - ) -> Result { + cancel_upload_rx: Option>, + ) -> Result, crate::Error> { match request.kind { QueuedRequestKind::Event { content } => { let (event, event_type) = content.raw(); @@ -677,7 +685,7 @@ impl RoomSendQueue { .await?; trace!(txn_id = %request.transaction_id, event_id = %res.event_id, "event successfully sent"); - Ok(SentRequestKey::Event(res.event_id)) + Ok(Some(SentRequestKey::Event(res.event_id))) } QueuedRequestKind::MediaUpload { @@ -688,61 +696,83 @@ impl RoomSendQueue { } => { trace!(%relates_to, "uploading media related to event"); - let mime = Mime::from_str(&content_type).map_err(|_| { - crate::Error::SendQueueWedgeError(QueueWedgeError::InvalidMimeType { - mime_type: content_type.clone(), - }) - })?; - - let data = room - .client() - .event_cache_store() - .lock() - .await? - .get_media_content(&cache_key) - .await? - .ok_or(crate::Error::SendQueueWedgeError( - QueueWedgeError::MissingMediaContent, - ))?; - - #[cfg(feature = "e2e-encryption")] - let media_source = if room.is_encrypted().await? { - trace!("upload will be encrypted (encrypted room)"); - let mut cursor = std::io::Cursor::new(data); - let encrypted_file = room + let fut = async move { + let mime = Mime::from_str(&content_type).map_err(|_| { + crate::Error::SendQueueWedgeError(QueueWedgeError::InvalidMimeType { + mime_type: content_type.clone(), + }) + })?; + + let data = room .client() - .upload_encrypted_file(&mime, &mut cursor) - .with_request_config(RequestConfig::short_retry()) - .await?; - MediaSource::Encrypted(Box::new(encrypted_file)) - } else { - trace!("upload will be in clear text (room without encryption)"); - let request_config = RequestConfig::short_retry() - .timeout(Media::reasonable_upload_timeout(&data)); - let res = - room.client().media().upload(&mime, data, Some(request_config)).await?; - MediaSource::Plain(res.content_uri) - }; + .event_cache_store() + .lock() + .await? + .get_media_content(&cache_key) + .await? + .ok_or(crate::Error::SendQueueWedgeError( + QueueWedgeError::MissingMediaContent, + ))?; + + #[cfg(feature = "e2e-encryption")] + let media_source = if room.is_encrypted().await? { + trace!("upload will be encrypted (encrypted room)"); + let mut cursor = std::io::Cursor::new(data); + let encrypted_file = room + .client() + .upload_encrypted_file(&mime, &mut cursor) + .with_request_config(RequestConfig::short_retry()) + .await?; + MediaSource::Encrypted(Box::new(encrypted_file)) + } else { + trace!("upload will be in clear text (room without encryption)"); + let request_config = RequestConfig::short_retry() + .timeout(Media::reasonable_upload_timeout(&data)); + let res = + room.client().media().upload(&mime, data, Some(request_config)).await?; + MediaSource::Plain(res.content_uri) + }; - #[cfg(not(feature = "e2e-encryption"))] - let media_source = { - let request_config = RequestConfig::short_retry() - .timeout(Media::reasonable_upload_timeout(&data)); - let res = - room.client().media().upload(&mime, data, Some(request_config)).await?; - MediaSource::Plain(res.content_uri) + #[cfg(not(feature = "e2e-encryption"))] + let media_source = { + let request_config = RequestConfig::short_retry() + .timeout(Media::reasonable_upload_timeout(&data)); + let res = + room.client().media().upload(&mime, data, Some(request_config)).await?; + MediaSource::Plain(res.content_uri) + }; + + let uri = match &media_source { + MediaSource::Plain(uri) => uri, + MediaSource::Encrypted(encrypted_file) => &encrypted_file.url, + }; + trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded"); + + Ok(SentRequestKey::Media(SentMediaInfo { + file: media_source, + thumbnail: thumbnail_source, + })) }; - let uri = match &media_source { - MediaSource::Plain(uri) => uri, - MediaSource::Encrypted(encrypted_file) => &encrypted_file.url, + let wait_for_cancel = async move { + if let Some(rx) = cancel_upload_rx { + rx.await + } else { + std::future::pending().await + } }; - trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded"); - Ok(SentRequestKey::Media(SentMediaInfo { - file: media_source, - thumbnail: thumbnail_source, - })) + tokio::select! { + biased; + + _ = wait_for_cancel => { + Ok(None) + } + + res = fut => { + res.map(Some) + } + } } } } @@ -822,6 +852,31 @@ struct RoomSendQueueInner { _task: JoinHandle<()>, } +/// Information about a request being sent right this moment. +struct BeingSentInfo { + /// Transaction id of the thing being sent. + transaction_id: OwnedTransactionId, + + /// For an upload request, a trigger to cancel the upload before it + /// completes. + cancel_upload: Option>, +} + +impl BeingSentInfo { + /// Aborts the upload, if a trigger is available. + /// + /// Consumes the object because the sender is a oneshot and will be consumed + /// upon sending. + fn cancel_upload(self) -> bool { + if let Some(cancel_upload) = self.cancel_upload { + let _ = cancel_upload.send(()); + true + } else { + false + } + } +} + #[derive(Clone)] struct QueueStorage { /// Reference to the client, to get access to the underlying store. @@ -830,10 +885,11 @@ struct QueueStorage { /// To which room is this storage related. room_id: OwnedRoomId, - /// All the queued requests that are being sent at the moment. + /// The one queued request that is being sent at the moment, along with + /// associated data that can be useful to act upon it. /// /// It also serves as an internal lock on the storage backend. - being_sent: Arc>>, + being_sent: Arc>>, } impl QueueStorage { @@ -879,7 +935,10 @@ impl QueueStorage { /// /// It is required to call [`Self::mark_as_sent`] after it's been /// effectively sent. - async fn peek_next_to_send(&self) -> Result, RoomSendQueueStorageError> { + async fn peek_next_to_send( + &self, + ) -> Result>)>, RoomSendQueueStorageError> + { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; @@ -887,10 +946,21 @@ impl QueueStorage { self.client()?.store().load_send_queue_requests(&self.room_id).await?; if let Some(request) = queued_requests.iter().find(|queued| !queued.is_wedged()) { - let prev = being_sent.replace(request.transaction_id.clone()); + let (cancel_upload_tx, cancel_upload_rx) = + if matches!(request.kind, QueuedRequestKind::MediaUpload { .. }) { + let (tx, rx) = oneshot::channel(); + (Some(tx), Some(rx)) + } else { + Default::default() + }; + + let prev = being_sent.replace(BeingSentInfo { + transaction_id: request.transaction_id.clone(), + cancel_upload: cancel_upload_tx, + }); assert!(prev.is_none()); - Ok(Some(request.clone())) + Ok(Some((request.clone(), cancel_upload_rx))) } else { Ok(None) } @@ -901,7 +971,10 @@ impl QueueStorage { /// be removed from the queue later. async fn mark_as_not_being_sent(&self, transaction_id: &TransactionId) { let was_being_sent = self.being_sent.write().await.take(); - assert_eq!(was_being_sent.as_deref(), Some(transaction_id)); + assert_eq!( + was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()), + Some(transaction_id) + ); } /// Marks a request popped with [`Self::peek_next_to_send`] and identified @@ -915,7 +988,10 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; let was_being_sent = being_sent.take(); - assert_eq!(was_being_sent.as_deref(), Some(transaction_id)); + assert_eq!( + was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()), + Some(transaction_id) + ); Ok(self .client()? @@ -947,7 +1023,10 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; let was_being_sent = being_sent.take(); - assert_eq!(was_being_sent.as_deref(), Some(transaction_id)); + assert_eq!( + was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()), + Some(transaction_id) + ); let client = self.client()?; let store = client.store(); @@ -977,7 +1056,7 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let being_sent = self.being_sent.read().await; - if being_sent.as_deref() == Some(transaction_id) { + if being_sent.as_ref().map(|info| info.transaction_id.as_ref()) == Some(transaction_id) { // Save the intent to redact the event. self.client()? .store() @@ -1012,7 +1091,7 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let being_sent = self.being_sent.read().await; - if being_sent.as_deref() == Some(transaction_id) { + if being_sent.as_ref().map(|info| info.transaction_id.as_ref()) == Some(transaction_id) { // Save the intent to edit the associated event. self.client()? .store() @@ -1760,9 +1839,25 @@ impl SendHandle { #[instrument(skip(self), fields(room_id = %self.room.inner.room.room_id(), txn_id = %self.transaction_id))] pub async fn abort(&self) -> Result { trace!("received an abort request"); - self.nyi_for_uploads()?; - if self.room.inner.queue.cancel_event(&self.transaction_id).await? { + let queue = &self.room.inner.queue; + + if let Some(handles) = &self.media_handles { + if queue.abort_upload(&self.transaction_id, handles).await? { + // Propagate a cancelled update. + let _ = self.room.inner.updates.send(RoomSendQueueUpdate::CancelledLocalEvent { + transaction_id: self.transaction_id.clone(), + }); + + return Ok(true); + } + + // If it failed, it means the sending of the event is not a + // dependent request anymore. Fall back to the regular + // code path below, that handles aborting sending of an event. + } + + if queue.cancel_event(&self.transaction_id).await? { trace!("successful abort"); // Propagate a cancelled update too. @@ -1841,6 +1936,7 @@ impl SendHandle { // one of the three requests will be active at the same time, i.e. only // one entry will be updated in the store. The other two are either // done, or dependent requests. + if let Some(handles) = &self.media_handles { room.queue .mark_as_unwedged(&handles.upload_file_txn) diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index d1259c5e57f..9373f9798b2 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -43,11 +43,11 @@ use crate::{ Client, Room, }; -/// Create a [`MediaRequest`] for a file we want to store locally before -/// sending it. +/// Create an [`OwnedMxcUri`] for a file or thumbnail we want to store locally +/// before sending it. /// /// This uses a MXC ID that is only locally valid. -fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters { +fn make_local_uri(txn_id: &TransactionId) -> OwnedMxcUri { // This mustn't represent a potentially valid media server, otherwise it'd be // possible for an attacker to return malicious content under some // preconditions (e.g. the cache store has been cleared before the upload @@ -55,10 +55,16 @@ fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParamete // which is guaranteed to be on the local machine. As a result, the only attack // possible would be coming from the user themselves, which we consider a // non-threat. + OwnedMxcUri::from(format!("mxc://send-queue.localhost/{txn_id}")) +} + +/// Create a [`MediaRequest`] for a file we want to store locally before +/// sending it. +/// +/// This uses a MXC ID that is only locally valid. +fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters { MediaRequestParameters { - source: MediaSource::Plain(OwnedMxcUri::from(format!( - "mxc://send-queue.localhost/{txn_id}" - ))), + source: MediaSource::Plain(make_local_uri(txn_id)), format: MediaFormat::File, } } @@ -74,9 +80,7 @@ fn make_local_thumbnail_media_request( ) -> MediaRequestParameters { // See comment in [`make_local_file_media_request`]. MediaRequestParameters { - source: MediaSource::Plain(OwnedMxcUri::from(format!( - "mxc://send-queue.localhost/{txn_id}" - ))), + source: MediaSource::Plain(make_local_uri(txn_id)), format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(width, height)), } } @@ -403,4 +407,130 @@ impl QueueStorage { Ok(()) } + + /// Try to abort an upload that would be ongoing. + /// + /// Return true if any media (media itself or its thumbnail) was being + /// uploaded. In this case, the media event has also been removed from + /// the send queue. If it returns false, then the uploads already + /// happened, and the event sending *may* have started. + #[instrument(skip(self, handles))] + pub(super) async fn abort_upload( + &self, + event_txn: &TransactionId, + handles: &MediaHandles, + ) -> Result { + let client = self.client()?; + + // Keep the lock until we're done touching the storage. + let mut being_sent = self.being_sent.write().await; + debug!("trying to abort an upload"); + + let store = client.store(); + + let upload_file_as_dependent = ChildTransactionId::from(handles.upload_file_txn.clone()); + let event_as_dependent = ChildTransactionId::from(event_txn.to_owned()); + + let mut removed_dependent_upload = false; + let mut removed_dependent_event = false; + + if let Some(thumbnail_txn) = &handles.upload_thumbnail_txn { + if store.remove_send_queue_request(&self.room_id, thumbnail_txn).await? { + // The thumbnail upload existed as a request: either it was pending (something + // else was being sent), or it was actively being sent. + trace!("could remove thumbnail request, removing 2 dependent requests now"); + + // 1. Try to abort sending using the being_sent info, in case it was active. + if let Some(info) = being_sent.as_ref() { + if info.transaction_id == *thumbnail_txn { + // SAFETY: we knew it was Some(), two lines above. + let info = being_sent.take().unwrap(); + if info.cancel_upload() { + trace!("aborted ongoing thumbnail upload"); + } + } + } + + // 2. Remove the dependent requests. + removed_dependent_upload = store + .remove_dependent_queued_request(&self.room_id, &upload_file_as_dependent) + .await?; + + if !removed_dependent_upload { + warn!("unable to find the dependent file upload request"); + } + + removed_dependent_event = store + .remove_dependent_queued_request(&self.room_id, &event_as_dependent) + .await?; + + if !removed_dependent_event { + warn!("unable to find the dependent media event upload request"); + } + } + } + + // If we're here: + // - either there was no thumbnail to upload, + // - or the thumbnail request has terminated already. + // + // So the next target is the upload request itself, in both cases. + + if !removed_dependent_upload { + if store.remove_send_queue_request(&self.room_id, &handles.upload_file_txn).await? { + // The upload existed as a request: either it was pending (something else was + // being sent), or it was actively being sent. + trace!("could remove file upload request, removing 1 dependent request"); + + // 1. Try to abort sending using the being_sent info, in case it was active. + if let Some(info) = being_sent.as_ref() { + if info.transaction_id == handles.upload_file_txn { + // SAFETY: we knew it was Some(), two lines above. + let info = being_sent.take().unwrap(); + if info.cancel_upload() { + trace!("aborted ongoing file upload"); + } + } + } + + // 2. Remove the dependent request. + if !store + .remove_dependent_queued_request(&self.room_id, &event_as_dependent) + .await? + { + warn!("unable to find the dependent media event upload request"); + } + } else { + // The upload was not in the send queue, so it's completed. + // + // It means the event sending is either still queued as a dependent request, or + // it's graduated into a request. + if !removed_dependent_event + && !store + .remove_dependent_queued_request(&self.room_id, &event_as_dependent) + .await? + { + // The media event has been promoted into a request, or the promoted request + // has been sent already: we couldn't abort, let the caller decide what to do. + debug!("uploads already happened => deferring to aborting an event sending"); + return Ok(false); + } + } + } + + // At this point, all the requests and dependent requests have been cleaned up. + // Perform the final step: empty the cache from the local items. + { + let event_cache = client.event_cache_store().lock().await?; + event_cache + .remove_media_content_for_uri(&make_local_uri(&handles.upload_file_txn)) + .await?; + if let Some(txn) = &handles.upload_thumbnail_txn { + event_cache.remove_media_content_for_uri(&make_local_uri(txn)).await?; + } + } + + debug!("successfully aborted!"); + Ok(true) + } } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 8815a965971..a782d381700 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1870,13 +1870,8 @@ async fn test_media_uploads() { // ---------------------- // Send handle operations. - // Operations on the send handle haven't been implemented yet. - assert_matches!( - send_handle.abort().await, - Err(RoomSendQueueStorageError::OperationNotImplementedYet) - ); - // (and this operation would be invalid, we shouldn't turn a media into a - // message). + // This operation should be invalid, we shouldn't turn a media into a + // message. assert_matches!( send_handle.edit(RoomMessageEventContent::text_plain("hi").into()).await, Err(RoomSendQueueStorageError::OperationNotImplementedYet) From bc86027853de8493f513848fe1205602c259fadc Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 13 Nov 2024 16:52:09 +0100 Subject: [PATCH 528/979] test(send queue): add a test for cancelling a media upload before it's active --- .../tests/integration/send_queue.rs | 168 ++++++++++++++++-- 1 file changed, 156 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index a782d381700..93edd4c0d6a 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -7,13 +7,13 @@ use matrix_sdk::{ media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, send_queue::{ LocalEcho, LocalEchoContent, RoomSendQueue, RoomSendQueueError, RoomSendQueueStorageError, - RoomSendQueueUpdate, + RoomSendQueueUpdate, SendHandle, }, test_utils::{ events::EventFactory, mocks::{MatrixMock, MatrixMockServer}, }, - MemoryStore, + Client, MemoryStore, }; use matrix_sdk_test::{async_test, InvitedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder}; use ruma::{ @@ -24,18 +24,19 @@ use ruma::{ UnstablePollStartContentBlock, UnstablePollStartEventContent, }, room::{ - message::{MessageType, RoomMessageEventContent}, + message::{ImageMessageEventContent, MessageType, RoomMessageEventContent}, MediaSource, }, AnyMessageLikeEventContent, EventContent as _, Mentions, }, mxc_uri, owned_user_id, room_id, serde::Raw, - uint, MxcUri, OwnedEventId, TransactionId, + uint, MxcUri, OwnedEventId, OwnedTransactionId, TransactionId, }; use serde_json::json; use tokio::{ - sync::Mutex, + sync::{broadcast::Receiver, Mutex}, + task::yield_now, time::{sleep, timeout}, }; use wiremock::{Request, ResponseTemplate}; @@ -59,6 +60,44 @@ async fn queue_attachment_no_thumbnail(q: &RoomSendQueue) -> &'static str { filename } +/// Queues an attachment whenever the actual data/mime type etc. don't matter, +/// along with a thumbnail. +/// +/// Returns the filename, for sanity check purposes. +async fn queue_attachment_with_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'static str) { + let filename = "surprise.jpeg.exe"; + let content_type = mime::IMAGE_JPEG; + let data = b"hello world".to_vec(); + + let thumbnail = Thumbnail { + data: b"thumbnail".to_vec(), + content_type: content_type.clone(), + info: Some(BaseThumbnailInfo { + height: Some(uint!(13)), + width: Some(uint!(37)), + size: Some(uint!(42)), + }), + }; + + let config = + AttachmentConfig::with_thumbnail(thumbnail).info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(13)), + width: Some(uint!(37)), + size: Some(uint!(42)), + blurhash: None, + })); + + let handle = q + .send_attachment(filename, content_type, data, config) + .await + .expect("queuing the attachment works"); + + // Let the background task pick up the request. + yield_now().await; + + (handle, filename) +} + fn mock_jpeg_upload<'a>( mock: &'a MatrixMockServer, mxc: &MxcUri, @@ -756,7 +795,7 @@ async fn test_cancellation() { assert!(watch.is_empty()); // Let the background task start now. - tokio::task::yield_now().await; + yield_now().await; // While the first item is being sent, the system records the intent to abort // it. @@ -878,7 +917,7 @@ async fn test_edit() { assert!(watch.is_empty()); // Let the background task start now. - tokio::task::yield_now().await; + yield_now().await; // While the first item is being sent, the system remembers the intent to edit // it, and will send it later. @@ -1002,7 +1041,7 @@ async fn test_edit_with_poll_start() { assert!(watch.is_empty()); // Let the background task start now. - tokio::task::yield_now().await; + yield_now().await; // Edit the poll start event let poll_answers: UnstablePollAnswers = @@ -1089,7 +1128,7 @@ async fn test_edit_while_being_sent_and_fails() { assert!(watch.is_empty()); // Let the background task start now. - tokio::task::yield_now().await; + yield_now().await; // While the first item is being sent, the system remembers the intent to edit // it, and will send it later. @@ -1150,7 +1189,7 @@ async fn test_edit_wakes_the_sending_task() { assert!(watch.is_empty()); // Let the background task start now. - tokio::task::yield_now().await; + yield_now().await; assert_update!(watch => error { recoverable = false, txn = txn }); assert!(watch.is_empty()); @@ -1321,7 +1360,7 @@ async fn test_abort_while_being_sent_and_fails() { assert!(watch.is_empty()); // Let the background task start now. - tokio::task::yield_now().await; + yield_now().await; // While the item is being sent, the system remembers the intent to redact it // later. @@ -2075,11 +2114,57 @@ async fn test_unwedging_media_upload() { assert!(watch.is_empty()); } +/// Aborts an ongoing media upload and checks post-conditions: +/// - we could abort +/// - we get the notification about the aborted upload +/// - the medias aren't present in the cache store +async fn abort_and_verify( + client: &Client, + watch: &mut Receiver, + img_content: ImageMessageEventContent, + upload_handle: SendHandle, + upload_txn: OwnedTransactionId, +) { + let file_source = img_content.source; + let info = img_content.info.unwrap(); + let thumbnail_source = info.thumbnail_source.unwrap(); + let thumbnail_info = info.thumbnail_info.unwrap(); + + let aborted = upload_handle.abort().await.unwrap(); + assert!(aborted, "upload must have been aborted"); + + assert_update!(watch => cancelled { txn = upload_txn }); + + // The event cache doesn't contain the medias anymore. + client + .media() + .get_media_content( + &MediaRequestParameters { source: file_source, format: MediaFormat::File }, + true, + ) + .await + .unwrap_err(); + + client + .media() + .get_media_content( + &MediaRequestParameters { + source: thumbnail_source, + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( + thumbnail_info.width.unwrap(), + thumbnail_info.height.unwrap(), + )), + }, + true, + ) + .await + .unwrap_err(); +} + #[async_test] async fn test_media_event_is_sent_in_order() { // Test that despite happening in multiple requests, sending a media maintains // the ordering. - let mock = MatrixMockServer::new().await; // Mark the room as joined. @@ -2088,6 +2173,7 @@ async fn test_media_event_is_sent_in_order() { let room = mock.sync_joined_room(&client, room_id).await; let q = room.send_queue(); + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); assert!(local_echoes.is_empty()); @@ -2145,3 +2231,61 @@ async fn test_media_event_is_sent_in_order() { assert_let!(LocalEchoContent::Event { send_error, .. } = &local_echoes[0].content); assert!(send_error.is_some()); } + +#[async_test] +async fn test_cancel_upload_before_active() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + mock.mock_room_send() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(1)).set_body_json( + json!({ + "event_id": event_id!("$msg") + }), + )) + .mock_once() + .mount() + .await; + + // Send an event which sending will be "slow" (blocked by mutex). + q.send(RoomMessageEventContent::text_plain("hey").into()).await.unwrap(); + let (msg_txn, _handle) = assert_update!(watch => local echo { body = "hey" }); + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_with_thumbnail(&q).await; + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.filename(), filename); + + // Abort the upload. + abort_and_verify(&client, &mut watch, img_content, upload_handle, upload_txn).await; + + // Let the sending progress. + assert!(watch.is_empty()); + sleep(Duration::from_secs(1)).await; + + // The text event is sent, at some point. + assert_update!(watch => sent { txn = msg_txn, }); + + // Wait a bit of time for things to settle. + sleep(Duration::from_millis(500)).await; + + // That's all, folks! + assert!(watch.is_empty()); +} From b7d4be9b65012f45485e392acf2643ae95f76c43 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 13 Nov 2024 17:29:49 +0100 Subject: [PATCH 529/979] test(send queue): add a test for cancelling an upload while the thumbnail upload is active --- .../tests/integration/send_queue.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 93edd4c0d6a..6457b3687d3 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -2289,3 +2289,55 @@ async fn test_cancel_upload_before_active() { // That's all, folks! assert!(watch.is_empty()); } + +#[async_test] +async fn test_cancel_upload_with_thumbnail_active() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$msg")).mock_once().mount().await; + + // Have the thumbnail upload take forever and time out, if continued. This will + // be interrupted when aborting, so this will never have to complete. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(60)).set_body_json( + json!({ + "mxc_id": "mxc://sdk.rs/unreachable" + }), + )) + .expect(0) + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_with_thumbnail(&q).await; + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.filename(), filename); + + // Abort the upload. + abort_and_verify(&client, &mut watch, img_content, upload_handle, upload_txn).await; + + // To prove we're not waiting for the upload to finish, send a message and + // observe it's immediately sent. + q.send(RoomMessageEventContent::text_plain("hi").into()).await.unwrap(); + let (msg_txn, _handle) = assert_update!(watch => local echo { body = "hi" }); + assert_update!(watch => sent { txn = msg_txn, }); + + // That's all, folks! + assert!(watch.is_empty()); +} From 9b6de4e4367a465b7605eb931d0e4375df8f5ddd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 13 Nov 2024 18:00:16 +0100 Subject: [PATCH 530/979] test(send queue): add more tests for cancellation --- .../tests/integration/send_queue.rs | 255 +++++++++++++++++- 1 file changed, 243 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 6457b3687d3..4198a123d80 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -44,7 +44,7 @@ use wiremock::{Request, ResponseTemplate}; /// Queues an attachment whenever the actual data/mime type etc. don't matter. /// /// Returns the filename, for sanity check purposes. -async fn queue_attachment_no_thumbnail(q: &RoomSendQueue) -> &'static str { +async fn queue_attachment_no_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'static str) { let filename = "surprise.jpeg.exe"; let content_type = mime::IMAGE_JPEG; let data = b"hello world".to_vec(); @@ -54,10 +54,11 @@ async fn queue_attachment_no_thumbnail(q: &RoomSendQueue) -> &'static str { size: Some(uint!(42)), blurhash: None, })); - q.send_attachment(filename, content_type, data, config) + let handle = q + .send_attachment(filename, content_type, data, config) .await .expect("queuing the attachment works"); - filename + (handle, filename) } /// Queues an attachment whenever the actual data/mime type etc. don't matter, @@ -2006,7 +2007,7 @@ async fn test_media_upload_retry() { // Send the media. assert!(watch.is_empty()); - let filename = queue_attachment_no_thumbnail(&q).await; + let (_handle, filename) = queue_attachment_no_thumbnail(&q).await; // Observe the local echo. let (event_txn, _send_handle, content) = assert_update!(watch => local echo event); @@ -2075,7 +2076,7 @@ async fn test_unwedging_media_upload() { // Send the media. assert!(watch.is_empty()); - let filename = queue_attachment_no_thumbnail(&q).await; + let (_handle, filename) = queue_attachment_no_thumbnail(&q).await; // Observe the local echo. let (event_txn, send_handle, content) = assert_update!(watch => local echo event); @@ -2196,7 +2197,7 @@ async fn test_media_event_is_sent_in_order() { mock.mock_room_send().ok(event_id!("$text")).mock_once().mount().await; // 2. Queue the media. - let filename = queue_attachment_no_thumbnail(&q).await; + let (_handle, filename) = queue_attachment_no_thumbnail(&q).await; // 3. Queue the message. q.send(RoomMessageEventContent::text_plain("hello world").into()).await.unwrap(); @@ -2311,12 +2312,68 @@ async fn test_cancel_upload_with_thumbnail_active() { // Have the thumbnail upload take forever and time out, if continued. This will // be interrupted when aborting, so this will never have to complete. mock.mock_upload() - .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(60)).set_body_json( - json!({ - "mxc_id": "mxc://sdk.rs/unreachable" - }), - )) - .expect(0) + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(60))) + .expect(1) + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_with_thumbnail(&q).await; + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.filename(), filename); + + // Let the upload request start. + sleep(Duration::from_millis(500)).await; + + // Abort the upload. + abort_and_verify(&client, &mut watch, img_content, upload_handle, upload_txn).await; + + // To prove we're not waiting for the upload to finish, send a message and + // observe it's immediately sent. + q.send(RoomMessageEventContent::text_plain("hi").into()).await.unwrap(); + let (msg_txn, _handle) = assert_update!(watch => local echo { body = "hi" }); + assert_update!(watch => sent { txn = msg_txn, }); + + // That's all, folks! + assert!(watch.is_empty()); +} + +#[async_test] +async fn test_cancel_upload_with_uploaded_thumbnail_and_file_active() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$msg")).mock_once().named("send event").mount().await; + + // Have the thumbnail upload finish early. + mock.mock_upload() + .ok(mxc_uri!("mxc://sdk.rs/thumbnail")) + .mock_once() + .named("thumbnail upload") + .mount() + .await; + + // Have the file upload take forever and time out, if continued. This will + // be interrupted when aborting, so this will never have to complete. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(60))) + .expect(1) + .named("file upload") .mount() .await; @@ -2329,6 +2386,12 @@ async fn test_cancel_upload_with_thumbnail_active() { assert_let!(MessageType::Image(img_content) = content.msgtype); assert_eq!(img_content.filename(), filename); + // The thumbnail uploads just fine. + assert_update!(watch => uploaded { related_to = upload_txn, mxc = mxc_uri!("mxc://sdk.rs/thumbnail") }); + + // Let the file upload request start. + sleep(Duration::from_millis(500)).await; + // Abort the upload. abort_and_verify(&client, &mut watch, img_content, upload_handle, upload_txn).await; @@ -2341,3 +2404,171 @@ async fn test_cancel_upload_with_thumbnail_active() { // That's all, folks! assert!(watch.is_empty()); } + +#[async_test] +async fn test_cancel_upload_only_file_with_file_active() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$msg")).mock_once().named("send event").mount().await; + + // Have the file upload take forever and time out, if continued. This will + // be interrupted when aborting, so this will never have to complete. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(60))) + .expect(1) + .named("file upload") + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_no_thumbnail(&q).await; + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.filename(), filename); + + // Let the upload request start. + sleep(Duration::from_millis(500)).await; + + // Abort the upload. + let aborted = upload_handle.abort().await.unwrap(); + assert!(aborted, "upload must have been aborted"); + + assert_update!(watch => cancelled { txn = upload_txn }); + + // The event cache doesn't contain the medias anymore. + client + .media() + .get_media_content( + &MediaRequestParameters { source: img_content.source, format: MediaFormat::File }, + true, + ) + .await + .unwrap_err(); + + // To prove we're not waiting for the upload to finish, send a message and + // observe it's immediately sent. + q.send(RoomMessageEventContent::text_plain("hi").into()).await.unwrap(); + let (msg_txn, _handle) = assert_update!(watch => local echo { body = "hi" }); + assert_update!(watch => sent { txn = msg_txn, }); + + // That's all, folks! + assert!(watch.is_empty()); +} + +#[async_test] +async fn test_cancel_upload_while_sending_event() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + // File upload will succeed immediately. + mock.mock_upload() + .ok(mxc_uri!("mxc://sdk.rs/media")) + .mock_once() + .named("file upload") + .mount() + .await; + + // Sending of the media event will take 1 second, so we can abort it while it's + // happening. + mock.mock_room_send() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(1)).set_body_json( + json!({ + "event_id": "$media" + }), + )) + .mock_once() + .named("send event") + .mock_once() + .mount() + .await; + + // A redaction will happen because the abort happens after the event is getting + // sent. + mock.mock_room_redact().ok(event_id!("$redaction")).mock_once().mount().await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_no_thumbnail(&q).await; + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(local_content) = content.msgtype); + assert_eq!(local_content.filename(), filename); + + assert_update!(watch => uploaded { related_to = upload_txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + let edit_msg = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(remote_content) = edit_msg.msgtype); + assert_let!(MediaSource::Plain(new_uri) = &remote_content.source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/media")); + + // Let the upload request start. + sleep(Duration::from_millis(250)).await; + + // Abort the upload. + let aborted = upload_handle.abort().await.unwrap(); + assert!(aborted, "upload must have been aborted"); + + // We get a local echo for the cancelled media event… + assert_update!(watch => cancelled { txn = upload_txn }); + // …But the event is still sent, before getting redacted. + assert_update!(watch => sent { txn = upload_txn, }); + + // The event cache doesn't contain the media with the local URI. + client + .media() + .get_media_content( + &MediaRequestParameters { source: local_content.source, format: MediaFormat::File }, + true, + ) + .await + .unwrap_err(); + + // But it does contain the media with the remote URI, which hasn't been removed + // from the remote server. + client + .media() + .get_media_content( + &MediaRequestParameters { source: remote_content.source, format: MediaFormat::File }, + true, + ) + .await + .unwrap(); + + // Let things settle (and the redaction endpoint be called). + sleep(Duration::from_secs(1)).await; + + // Trying to abort after it's been sent/redacted is a no-op + let aborted = upload_handle.abort().await.unwrap(); + assert!(aborted.not()); + + // That's all, folks! + assert!(watch.is_empty()); +} From 02c7c2cdfc10a18d32c498f15d8ff40268108205 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 13 Nov 2024 14:53:44 +0100 Subject: [PATCH 531/979] test(send queue): caching a thumbnail of unknown dimensions removes it from cache after upload --- crates/matrix-sdk/src/attachment.rs | 2 +- crates/matrix-sdk/src/send_queue/upload.rs | 33 ++-- .../tests/integration/send_queue.rs | 154 ++++++++++++++++++ 3 files changed, 176 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index dbefd0d32a1..0fde6997466 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -148,7 +148,7 @@ impl From for FileInfo { } } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] /// Base metadata about a thumbnail. pub struct BaseThumbnailInfo { /// The height of the thumbnail in pixels. diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 9373f9798b2..9e885d3cc57 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -323,18 +323,27 @@ impl QueueStorage { let from_req = make_local_thumbnail_media_request(&info.txn, info.height, info.width); - trace!( from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); - - // Reuse the same format for the cached thumbnail with the final MXC ID. - let new_format = from_req.format.clone(); - - cache_store - .replace_media_key( - &from_req, - &MediaRequestParameters { source: new_source, format: new_format }, - ) - .await - .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + if info.height == uint!(0) || info.width == uint!(0) { + trace!(from = ?from_req.source, "removing thumbnail with unknown dimension from cache store"); + + cache_store + .remove_media_content(&from_req) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + } else { + trace!(from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); + + // Reuse the same format for the cached thumbnail with the final MXC ID. + let new_format = from_req.format.clone(); + + cache_store + .replace_media_key( + &from_req, + &MediaRequestParameters { source: new_source, format: new_format }, + ) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; + } } } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 4198a123d80..49fc6cd670f 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1980,6 +1980,160 @@ async fn test_media_uploads() { assert!(watch.is_empty()); } +#[async_test] +async fn test_media_uploads_no_caching_of_thumbnails_of_unknown_sizes() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // ---------------------- + // Create the media to send, with a thumbnail that has unknown dimensions. + let filename = "surprise.jpeg.exe"; + let content_type = mime::IMAGE_JPEG; + let data = b"hello world".to_vec(); + + let thumbnail = Thumbnail { + data: b"thumbnail".to_vec(), + content_type: content_type.clone(), + info: Some(BaseThumbnailInfo::default()), + }; + + let attachment_info = AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(14)), + width: Some(uint!(38)), + size: Some(uint!(43)), + blurhash: None, + }); + + let config = AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info); + + // ---------------------- + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + + mock.mock_upload().ok(mxc_uri!("mxc://sdk.rs/thumbnail")).mock_once().mount().await; + mock.mock_upload().ok(mxc_uri!("mxc://sdk.rs/media")).mock_once().mount().await; + + // ---------------------- + // Send the media. + assert!(watch.is_empty()); + q.send_attachment(filename, content_type, data, config) + .await + .expect("queuing the attachment works"); + + // ---------------------- + // Observe the local echo + let (txn, _send_handle, content) = assert_update!(watch => local echo event); + + // Sanity-check metadata. + assert_let!(MessageType::Image(img_content) = content.msgtype); + assert_eq!(img_content.filename(), filename); + + let info = img_content.info.unwrap(); + assert_eq!(info.height, Some(uint!(14))); + assert_eq!(info.width, Some(uint!(38))); + assert_eq!(info.size, Some(uint!(43))); + assert_eq!(info.mimetype.as_deref(), Some("image/jpeg")); + + // Check the data source: it should reference the send queue local storage. + let local_source = img_content.source; + assert_let!(MediaSource::Plain(mxc) = &local_source); + assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); + + // The media is immediately available from the cache. + let file_media = client + .media() + .get_media_content( + &MediaRequestParameters { source: local_source, format: MediaFormat::File }, + true, + ) + .await + .expect("media should be found"); + assert_eq!(file_media, b"hello world"); + + // ---------------------- + // Thumbnail. + + // Check metadata. + let tinfo = info.thumbnail_info.unwrap(); + assert_eq!(tinfo.height, None); + assert_eq!(tinfo.width, None); + assert_eq!(tinfo.size, None); + assert_eq!(tinfo.mimetype.as_deref(), Some("image/jpeg")); + + // Check the thumbnail source: it should reference the send queue local storage. + let local_thumbnail_source = info.thumbnail_source.unwrap(); + assert_let!(MediaSource::Plain(mxc) = &local_thumbnail_source); + assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); + + let thumbnail_media = client + .media() + .get_media_content( + &MediaRequestParameters { + source: local_thumbnail_source, + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(0), uint!(0))), + }, + true, + ) + .await + .expect("media should be found"); + assert_eq!(thumbnail_media, b"thumbnail"); + + // ---------------------- + // Let the upload progress. + + assert_update!(watch => uploaded { related_to = txn, mxc = mxc_uri!("mxc://sdk.rs/thumbnail") }); + assert_update!(watch => uploaded { related_to = txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + let edit_msg = assert_update!(watch => edit local echo { txn = txn }); + assert_let!(MessageType::Image(new_content) = edit_msg.msgtype); + assert_let!(MediaSource::Plain(new_uri) = &new_content.source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/media")); + + let file_media = client + .media() + .get_media_content( + &MediaRequestParameters { source: new_content.source, format: MediaFormat::File }, + true, + ) + .await + .expect("media should be found with its final MXC uri in the cache"); + assert_eq!(file_media, b"hello world"); + + let new_thumbnail_source = new_content.info.unwrap().thumbnail_source.unwrap(); + assert_let!(MediaSource::Plain(new_uri) = &new_thumbnail_source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/thumbnail")); + + // Retrieving the thumbnail should NOT work, since it doesn't make sense to + // cache it with a size of 0. + client + .media() + .get_media_content( + &MediaRequestParameters { + source: new_thumbnail_source, + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(0), uint!(0))), + }, + true, + ) + .await + .unwrap_err(); + + // The event is sent, at some point. + assert_update!(watch => sent { event_id = event_id!("$1") }); + + // That's all, folks! + assert!(watch.is_empty()); +} + #[async_test] async fn test_media_upload_retry() { let mock = MatrixMockServer::new().await; From 8070e3c16579ea3be588786f01ccbb7d4a35804d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 14 Nov 2024 10:54:07 +0100 Subject: [PATCH 532/979] refactor(widget): tidy up and start commenting the widget code --- crates/matrix-sdk/src/widget/filter.rs | 2 - .../src/widget/machine/from_widget.rs | 1 - crates/matrix-sdk/src/widget/machine/mod.rs | 135 +++++++++++------- crates/matrix-sdk/src/widget/mod.rs | 46 +++--- 4 files changed, 108 insertions(+), 76 deletions(-) diff --git a/crates/matrix-sdk/src/widget/filter.rs b/crates/matrix-sdk/src/widget/filter.rs index c3daf2ce062..cfdca629135 100644 --- a/crates/matrix-sdk/src/widget/filter.rs +++ b/crates/matrix-sdk/src/widget/filter.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(dead_code)] // temporary - use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType}; use serde::Deserialize; diff --git a/crates/matrix-sdk/src/widget/machine/from_widget.rs b/crates/matrix-sdk/src/widget/machine/from_widget.rs index 7dcb3c86ad7..e70d53cf7d5 100644 --- a/crates/matrix-sdk/src/widget/machine/from_widget.rs +++ b/crates/matrix-sdk/src/widget/machine/from_widget.rs @@ -124,7 +124,6 @@ pub(super) enum ReadEventRequest { event_type: StateEventType, state_key: StateKeySelector, }, - #[allow(dead_code)] ReadMessageLikeEvent { #[serde(rename = "type")] event_type: MessageLikeEventType, diff --git a/crates/matrix-sdk/src/widget/machine/mod.rs b/crates/matrix-sdk/src/widget/machine/mod.rs index eb0c521878e..d3b600c2c7f 100644 --- a/crates/matrix-sdk/src/widget/machine/mod.rs +++ b/crates/matrix-sdk/src/widget/machine/mod.rs @@ -14,8 +14,6 @@ //! No I/O logic of the [`WidgetDriver`]. -#![warn(unreachable_pub)] - use std::{fmt, iter, time::Duration}; use driver_req::UpdateDelayedEventRequest; @@ -54,7 +52,6 @@ use super::{ filter::{MatrixEventContent, MatrixEventFilterInput}, Capabilities, StateKeySelector, }; -use crate::widget::EventFilter; mod driver_req; mod from_widget; @@ -72,7 +69,7 @@ pub(crate) use self::{ }; /// Action (a command) that client (driver) must perform. -#[derive(Clone, Debug)] +#[derive(Debug)] pub(crate) enum Action { /// Send a raw message to the widget. SendToWidget(String), @@ -94,23 +91,33 @@ pub(crate) enum Action { /// Subscribe to the events in the *current* room, i.e. a room which this /// widget is instantiated with. The client is aware of the room. - #[allow(dead_code)] Subscribe, /// Unsuscribe from the events in the *current* room. Symmetrical to /// `Subscribe`. - #[allow(dead_code)] Unsubscribe, } /// No I/O state machine. /// -/// Handles interactions with the widget as well as the `MatrixDriver`. +/// Handles interactions with the widget as well as the +/// [`crate::widget::MatrixDriver`]. pub(crate) struct WidgetMachine { + /// Unique identifier for the widget. + /// + /// Allows distinguishing different widgets. widget_id: String, + + /// The room to which this widget machine is attached. room_id: OwnedRoomId, + + /// Outstanding requests sent to the widget (mapped by uuid). pending_to_widget_requests: PendingRequests, + + /// Outstanding requests sent to the matrix driver (mapped by uuid). pending_matrix_driver_requests: PendingRequests, + + /// Current negotiation state for capabilities. capabilities: CapabilitiesState, } @@ -149,9 +156,11 @@ impl WidgetMachine { match event { IncomingMessage::WidgetMessage(raw) => self.process_widget_message(&raw), + IncomingMessage::MatrixDriverResponse { request_id, response } => { self.process_matrix_driver_response(request_id, response) } + IncomingMessage::MatrixEventReceived(event) => { let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else { error!("Received matrix event before capabilities negotiation"); @@ -172,10 +181,8 @@ impl WidgetMachine { fn process_widget_message(&mut self, raw: &str) -> Vec { let message = match serde_json::from_str::(raw) { Ok(msg) => msg, - Err(e) => { - // TODO: There is a special error handling required for the invalid - // messages. Refer to the `widget-api-poc` for implementation notes. - error!("Failed to parse incoming message: {e}"); + Err(error) => { + error!("couldn't deserialize incoming widget message: {error}"); return Vec::new(); } }; @@ -203,21 +210,22 @@ impl WidgetMachine { ) -> Vec { let request = match raw_request.deserialize() { Ok(r) => r, - Err(e) => return vec![self.send_from_widget_error_response(raw_request, e)], + Err(e) => return vec![Self::send_from_widget_error_response(raw_request, e)], }; match request { FromWidgetRequest::SupportedApiVersions {} => { let response = SupportedApiVersionsResponse::new(); - vec![self.send_from_widget_response(raw_request, response)] + vec![Self::send_from_widget_response(raw_request, response)] } FromWidgetRequest::ContentLoaded {} => { - let response = vec![self.send_from_widget_response(raw_request, JsonObject::new())]; - self.capabilities - .is_unset() - .then(|| [&response, self.negotiate_capabilities().as_slice()].concat()) - .unwrap_or(response) + let mut response = + vec![Self::send_from_widget_response(raw_request, JsonObject::new())]; + if self.capabilities.is_unset() { + response.append(&mut self.negotiate_capabilities()); + } + response } FromWidgetRequest::ReadEvent(req) => self @@ -245,17 +253,20 @@ impl WidgetMachine { action.map(|a| vec![a]).unwrap_or_default() }); - let response = self.send_from_widget_response(raw_request, OpenIdResponse::Pending); + let response = + Self::send_from_widget_response(raw_request, OpenIdResponse::Pending); iter::once(response).chain(request_action).collect() } + FromWidgetRequest::DelayedEventUpdate(req) => { let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else { let text = "Received send update delayed event request before capabilities were negotiated"; - return vec![self.send_from_widget_error_response(raw_request, text)]; + return vec![Self::send_from_widget_error_response(raw_request, text)]; }; + if !capabilities.update_delayed_event { - return vec![self.send_from_widget_error_response( + return vec![Self::send_from_widget_error_response( raw_request, format!( "Not allowed: missing the {} capability.", @@ -263,19 +274,22 @@ impl WidgetMachine { ), )]; } + let (request, request_action) = self.send_matrix_driver_request(UpdateDelayedEventRequest { action: req.action, delay_id: req.delay_id, }); - request.then(|res, machine| { - vec![machine.send_from_widget_result_response( + + request.then(|res, _machine| { + vec![Self::send_from_widget_result_response( raw_request, // This is mapped to another type because the update_delay_event::Response // does not impl Serialize res.map(Into::::into), )] }); + request_action.map(|a| vec![a]).unwrap_or_default() } } @@ -288,14 +302,14 @@ impl WidgetMachine { ) -> Option { let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else { let text = "Received read event request before capabilities were negotiated"; - return Some(self.send_from_widget_error_response(raw_request, text)); + return Some(Self::send_from_widget_error_response(raw_request, text)); }; match request { ReadEventRequest::ReadMessageLikeEvent { event_type, limit } => { - let filter_fn = |f: &EventFilter| f.matches_message_like_event_type(&event_type); - if !capabilities.read.iter().any(filter_fn) { - return Some(self.send_from_widget_error_response( + if !capabilities.read.iter().any(|f| f.matches_message_like_event_type(&event_type)) + { + return Some(Self::send_from_widget_error_response( raw_request, "Not allowed to read message like event", )); @@ -304,7 +318,9 @@ impl WidgetMachine { const DEFAULT_EVENT_LIMIT: u32 = 50; let limit = limit.unwrap_or(DEFAULT_EVENT_LIMIT); let request = ReadMessageLikeEventRequest { event_type, limit }; + let (request, action) = self.send_matrix_driver_request(request); + request.then(|result, machine| { let response = result.and_then(|mut events| { let CapabilitiesState::Negotiated(capabilities) = &machine.capabilities @@ -316,10 +332,13 @@ impl WidgetMachine { events.retain(|e| capabilities.raw_event_matches_read_filter(e)); Ok(ReadEventResponse { events }) }); - vec![machine.send_from_widget_result_response(raw_request, response)] + + vec![Self::send_from_widget_result_response(raw_request, response)] }); + action } + ReadEventRequest::ReadStateEvent { event_type, state_key } => { let allowed = match &state_key { StateKeySelector::Any => capabilities @@ -342,13 +361,13 @@ impl WidgetMachine { if allowed { let request = ReadStateEventRequest { event_type, state_key }; let (request, action) = self.send_matrix_driver_request(request); - request.then(|result, machine| { + request.then(|result, _machine| { let response = result.map(|events| ReadEventResponse { events }); - vec![machine.send_from_widget_result_response(raw_request, response)] + vec![Self::send_from_widget_result_response(raw_request, response)] }); action } else { - Some(self.send_from_widget_error_response( + Some(Self::send_from_widget_error_response( raw_request, "Not allowed to read state event", )) @@ -377,8 +396,9 @@ impl WidgetMachine { Default::default() }), }; + if !capabilities.send_delayed_event && request.delay.is_some() { - return Some(self.send_from_widget_error_response( + return Some(Self::send_from_widget_error_response( raw_request, format!( "Not allowed: missing the {} capability.", @@ -386,19 +406,23 @@ impl WidgetMachine { ), )); } + if !capabilities.send.iter().any(|filter| filter.matches(&filter_in)) { - return Some( - self.send_from_widget_error_response(raw_request, "Not allowed to send event"), - ); + return Some(Self::send_from_widget_error_response( + raw_request, + "Not allowed to send event", + )); } let (request, action) = self.send_matrix_driver_request(request); + request.then(|mut result, machine| { if let Ok(r) = result.as_mut() { r.set_room_id(machine.room_id.clone()); } - vec![machine.send_from_widget_result_response(raw_request, result)] + vec![Self::send_from_widget_result_response(raw_request, result)] }); + action } @@ -453,38 +477,39 @@ impl WidgetMachine { } } - #[instrument(skip_all, fields(request_id))] + #[instrument(skip_all)] fn send_from_widget_response( - &self, raw_request: Raw, response_data: impl Serialize, ) -> Action { - let mut object = raw_request - .deserialize_as::>>() - .expect("Failed to converted FromWidgetRequest to object representation"); - let response_data = serde_json::value::to_raw_value(&response_data) - .expect("Failed to serialize response data"); - object.insert("response".to_owned(), response_data); - let serialized = serde_json::to_string(&object).expect("Failed to serialize response"); + let f = || { + let mut object = raw_request.deserialize_as::>>()?; + let response_data = serde_json::value::to_raw_value(&response_data)?; + object.insert("response".to_owned(), response_data); + serde_json::to_string(&object) + }; + + // SAFETY: we expect the raw request to be a valid JSON map, to which we add a + // new field. + let serialized = f().expect("error when attaching response to incoming request"); + Action::SendToWidget(serialized) } fn send_from_widget_error_response( - &self, raw_request: Raw, error: impl fmt::Display, ) -> Action { - self.send_from_widget_response(raw_request, FromWidgetErrorResponse::new(error)) + Self::send_from_widget_response(raw_request, FromWidgetErrorResponse::new(error)) } fn send_from_widget_result_response( - &self, raw_request: Raw, result: Result, ) -> Action { match result { - Ok(res) => self.send_from_widget_response(raw_request, res), - Err(msg) => self.send_from_widget_error_response(raw_request, msg), + Ok(res) => Self::send_from_widget_response(raw_request, res), + Err(msg) => Self::send_from_widget_error_response(raw_request, msg), } } @@ -495,7 +520,7 @@ impl WidgetMachine { ) -> (ToWidgetRequestHandle<'_, T::ResponseData>, Option) { #[derive(Serialize)] #[serde(tag = "api", rename = "toWidget", rename_all = "camelCase")] - struct ToWidgetRequestSerHelper<'a, T> { + struct ToWidgetRequestSerdeHelper<'a, T> { widget_id: &'a str, request_id: Uuid, action: &'static str, @@ -503,7 +528,7 @@ impl WidgetMachine { } let request_id = Uuid::new_v4(); - let full_request = ToWidgetRequestSerHelper { + let full_request = ToWidgetRequestSerdeHelper { widget_id: &self.widget_id, request_id, action: T::ACTION, @@ -561,7 +586,7 @@ impl WidgetMachine { let update = NotifyCapabilitiesChanged { approved, requested }; let (_request, action) = machine.send_to_widget_request(update); - (subscribe_required).then(|| Action::Subscribe).into_iter().chain(action).collect() + subscribe_required.then(|| Action::Subscribe).into_iter().chain(action).collect() }); action.map(|a| vec![a]).unwrap_or_default() @@ -598,9 +623,13 @@ impl MatrixDriverRequestMeta { } } +/// Current negotiation state for capabilities. enum CapabilitiesState { + /// Capabilities have never been defined. Unset, + /// We're currently negotiating capabilities. Negotiating, + /// The capabilities have already been negotiated. Negotiated(Capabilities), } diff --git a/crates/matrix-sdk/src/widget/mod.rs b/crates/matrix-sdk/src/widget/mod.rs index d5f7109e4ff..3176c5f0889 100644 --- a/crates/matrix-sdk/src/widget/mod.rs +++ b/crates/matrix-sdk/src/widget/mod.rs @@ -115,26 +115,25 @@ impl WidgetDriver { (driver, channels) } - /// Starts a client widget API state machine for a given `widget` in a given - /// joined `room`. The function returns once the widget is disconnected or - /// any terminal error occurs. + /// Run client widget API state machine in a given joined `room` forever. /// - /// Not implemented yet! Currently, it does not contain any useful - /// functionality, it only blindly forwards the messages and returns errors - /// once a non-implemented part is triggered. + /// The function returns once the widget is disconnected or any terminal + /// error occurs. pub async fn run( self, room: Room, capabilities_provider: impl CapabilitiesProvider, ) -> Result<(), ()> { - // Create a channel so that we can conveniently send all events to it. - let (events_tx, mut events_rx) = unbounded_channel(); - - // Forward all of the incoming messages from the widget to the `events_tx`. - let tx = events_tx.clone(); - tokio::spawn(async move { - while let Ok(msg) = self.from_widget_rx.recv().await { - let _ = tx.send(IncomingMessage::WidgetMessage(msg)); + // Create a channel so that we can conveniently send all messages to it. + let (incoming_messages_tx, mut incoming_messages_rx) = unbounded_channel(); + + // Forward all of the incoming messages from the widget. + tokio::spawn({ + let incoming_messages_tx = incoming_messages_tx.clone(); + async move { + while let Ok(msg) = self.from_widget_rx.recv().await { + let _ = incoming_messages_tx.send(IncomingMessage::WidgetMessage(msg)); + } } }); @@ -152,7 +151,7 @@ impl WidgetDriver { matrix_driver: MatrixDriver::new(room.clone()), event_forwarding_guard: None, to_widget_tx: self.to_widget_tx, - events_tx, + events_tx: incoming_messages_tx, capabilities_provider, }; @@ -161,9 +160,9 @@ impl WidgetDriver { ctx.process_action(action).await?; } - // Process incoming events. - while let Some(event) = events_rx.recv().await { - ctx.process_event(event).await?; + // Process incoming messages as they're coming in. + while let Some(message) = incoming_messages_rx.recv().await { + ctx.process_incoming_message(message).await?; } Ok(()) @@ -181,8 +180,10 @@ struct ProcessingContext { } impl ProcessingContext { - async fn process_event(&mut self, event: IncomingMessage) -> Result<(), ()> { - for action in self.widget_machine.process(event) { + /// Compute the actions for a single given incoming message, and performs + /// them immediately. + async fn process_incoming_message(&mut self, msg: IncomingMessage) -> Result<(), ()> { + for action in self.widget_machine.process(msg) { self.process_action(action).await?; } @@ -194,6 +195,7 @@ impl ProcessingContext { Action::SendToWidget(msg) => { self.to_widget_tx.send(msg).await.map_err(|_| ())?; } + Action::MatrixDriverRequest { request_id, data } => { let response = match data { MatrixDriverRequestData::AcquireCapabilities(cmd) => { @@ -253,6 +255,7 @@ impl ProcessingContext { .send(IncomingMessage::MatrixDriverResponse { request_id, response }) .map_err(|_| ())?; } + Action::Subscribe => { // Only subscribe if we are not already subscribed. if self.event_forwarding_guard.is_none() { @@ -264,10 +267,12 @@ impl ProcessingContext { self.event_forwarding_guard = Some(guard); let (mut matrix, events_tx) = (self.matrix_driver.events(), self.events_tx.clone()); + tokio::spawn(async move { loop { tokio::select! { _ = stop_forwarding.cancelled() => { return } + Some(event) = matrix.recv() => { let _ = events_tx.send(IncomingMessage::MatrixEventReceived(event)); } @@ -276,6 +281,7 @@ impl ProcessingContext { }); } } + Action::Unsubscribe => { self.event_forwarding_guard = None; } From f3c0309fbc64d4706ca4b399633785231e398964 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 14 Nov 2024 11:17:43 +0100 Subject: [PATCH 533/979] refactor(widget): get rid of `ProcessingContext` and inline it in its callers --- .../matrix-sdk/src/widget/machine/incoming.rs | 2 +- crates/matrix-sdk/src/widget/machine/mod.rs | 11 +- crates/matrix-sdk/src/widget/mod.rs | 149 +++++++++--------- 3 files changed, 84 insertions(+), 78 deletions(-) diff --git a/crates/matrix-sdk/src/widget/machine/incoming.rs b/crates/matrix-sdk/src/widget/machine/incoming.rs index e0ca2c1b968..7d165b59c99 100644 --- a/crates/matrix-sdk/src/widget/machine/incoming.rs +++ b/crates/matrix-sdk/src/widget/machine/incoming.rs @@ -44,7 +44,7 @@ pub(crate) enum IncomingMessage { /// The `MatrixDriver` notified the `WidgetMachine` of a new matrix event. /// /// This means that the machine previously subscribed to some events - /// (`Action::Subscribe` request). + /// ([`crate::widget::Action::Subscribe`] request). MatrixEventReceived(Raw), } diff --git a/crates/matrix-sdk/src/widget/machine/mod.rs b/crates/matrix-sdk/src/widget/machine/mod.rs index d3b600c2c7f..e7b86075b87 100644 --- a/crates/matrix-sdk/src/widget/machine/mod.rs +++ b/crates/matrix-sdk/src/widget/machine/mod.rs @@ -68,7 +68,10 @@ pub(crate) use self::{ incoming::{IncomingMessage, MatrixDriverResponse}, }; -/// Action (a command) that client (driver) must perform. +/// A command to perform in reaction to an [`IncomingMessage`]. +/// +/// There are also initial actions that may be performed at the creation of a +/// [`WidgetMachine`]. #[derive(Debug)] pub(crate) enum Action { /// Send a raw message to the widget. @@ -144,8 +147,10 @@ impl WidgetMachine { capabilities: CapabilitiesState::Unset, }; - let actions = (!init_on_content_load).then(|| machine.negotiate_capabilities()); - (machine, actions.unwrap_or_default()) + let initial_actions = + if init_on_content_load { Vec::new() } else { machine.negotiate_capabilities() }; + + (machine, initial_actions) } /// Main entry point to drive the state machine. diff --git a/crates/matrix-sdk/src/widget/mod.rs b/crates/matrix-sdk/src/widget/mod.rs index 3176c5f0889..74e847f0137 100644 --- a/crates/matrix-sdk/src/widget/mod.rs +++ b/crates/matrix-sdk/src/widget/mod.rs @@ -61,6 +61,12 @@ pub struct WidgetDriver { /// /// These can be both requests and responses. to_widget_tx: Sender, + + /// Drop guard for an event handler forwarding all events from the Matrix + /// room to the widget. + /// + /// Only set if a subscription happened ([`Action::Subscribe`]). + event_forwarding_guard: Option, } /// A handle that encapsulates the communication between a widget driver and the @@ -109,7 +115,7 @@ impl WidgetDriver { let (from_widget_tx, from_widget_rx) = async_channel::unbounded(); let (to_widget_tx, to_widget_rx) = async_channel::unbounded(); - let driver = Self { settings, from_widget_rx, to_widget_tx }; + let driver = Self { settings, from_widget_rx, to_widget_tx, event_forwarding_guard: None }; let channels = WidgetDriverHandle { from_widget_tx, to_widget_rx }; (driver, channels) @@ -120,77 +126,69 @@ impl WidgetDriver { /// The function returns once the widget is disconnected or any terminal /// error occurs. pub async fn run( - self, + mut self, room: Room, capabilities_provider: impl CapabilitiesProvider, ) -> Result<(), ()> { // Create a channel so that we can conveniently send all messages to it. - let (incoming_messages_tx, mut incoming_messages_rx) = unbounded_channel(); + // + // It will receive: + // - all incoming messages from the widget + // - all responses from the Matrix driver + // - all events from the Matrix driver, if subscribed + let (incoming_msg_tx, mut incoming_msg_rx) = unbounded_channel(); // Forward all of the incoming messages from the widget. tokio::spawn({ - let incoming_messages_tx = incoming_messages_tx.clone(); + let incoming_msg_tx = incoming_msg_tx.clone(); + let from_widget_rx = self.from_widget_rx.clone(); async move { - while let Ok(msg) = self.from_widget_rx.recv().await { - let _ = incoming_messages_tx.send(IncomingMessage::WidgetMessage(msg)); + while let Ok(msg) = from_widget_rx.recv().await { + let _ = incoming_msg_tx.send(IncomingMessage::WidgetMessage(msg)); } } }); // Create widget API machine. - let (client_api, initial_actions) = WidgetMachine::new( + let (mut widget_machine, initial_actions) = WidgetMachine::new( self.settings.widget_id().to_owned(), room.room_id().to_owned(), self.settings.init_on_content_load(), None, ); - // The environment for the processing of actions from the widget machine. - let mut ctx = ProcessingContext { - widget_machine: client_api, - matrix_driver: MatrixDriver::new(room.clone()), - event_forwarding_guard: None, - to_widget_tx: self.to_widget_tx, - events_tx: incoming_messages_tx, - capabilities_provider, - }; + let matrix_driver = MatrixDriver::new(room.clone()); // Process initial actions that "initialise" the widget api machine. for action in initial_actions { - ctx.process_action(action).await?; + self.process_action(&matrix_driver, &incoming_msg_tx, &capabilities_provider, action) + .await?; } // Process incoming messages as they're coming in. - while let Some(message) = incoming_messages_rx.recv().await { - ctx.process_incoming_message(message).await?; - } - - Ok(()) - } -} - -/// A small wrapper of all the data that we need to process an incoming event. -struct ProcessingContext { - widget_machine: WidgetMachine, - matrix_driver: MatrixDriver, - event_forwarding_guard: Option, - to_widget_tx: Sender, - events_tx: UnboundedSender, - capabilities_provider: T, -} - -impl ProcessingContext { - /// Compute the actions for a single given incoming message, and performs - /// them immediately. - async fn process_incoming_message(&mut self, msg: IncomingMessage) -> Result<(), ()> { - for action in self.widget_machine.process(msg) { - self.process_action(action).await?; + while let Some(msg) = incoming_msg_rx.recv().await { + for action in widget_machine.process(msg) { + self.process_action( + &matrix_driver, + &incoming_msg_tx, + &capabilities_provider, + action, + ) + .await?; + } } Ok(()) } - async fn process_action(&mut self, action: Action) -> Result<(), ()> { + /// Process a single [`Action`]. + async fn process_action( + &mut self, + matrix_driver: &MatrixDriver, + incoming_msg_tx: &UnboundedSender, + capabilities_provider: &impl CapabilitiesProvider, + action: Action, + ) -> Result<(), ()> { match action { Action::SendToWidget(msg) => { self.to_widget_tx.send(msg).await.map_err(|_| ())?; @@ -199,29 +197,25 @@ impl ProcessingContext { Action::MatrixDriverRequest { request_id, data } => { let response = match data { MatrixDriverRequestData::AcquireCapabilities(cmd) => { - let obtained = self - .capabilities_provider + let obtained = capabilities_provider .acquire_capabilities(cmd.desired_capabilities) .await; Ok(MatrixDriverResponse::CapabilitiesAcquired(obtained)) } - MatrixDriverRequestData::GetOpenId => self - .matrix_driver + MatrixDriverRequestData::GetOpenId => matrix_driver .get_open_id() .await .map(MatrixDriverResponse::OpenIdReceived) .map_err(|e| e.to_string()), - MatrixDriverRequestData::ReadMessageLikeEvent(cmd) => self - .matrix_driver + MatrixDriverRequestData::ReadMessageLikeEvent(cmd) => matrix_driver .read_message_like_events(cmd.event_type.clone(), cmd.limit) .await .map(MatrixDriverResponse::MatrixEventRead) .map_err(|e| e.to_string()), - MatrixDriverRequestData::ReadStateEvent(cmd) => self - .matrix_driver + MatrixDriverRequestData::ReadStateEvent(cmd) => matrix_driver .read_state_events(cmd.event_type.clone(), &cmd.state_key) .await .map(MatrixDriverResponse::MatrixEventRead) @@ -236,50 +230,57 @@ impl ProcessingContext { let delay_event_parameter = delay.map(|d| DelayParameters::Timeout { timeout: Duration::from_millis(d), }); - self.matrix_driver + matrix_driver .send(event_type, state_key, content, delay_event_parameter) .await .map(MatrixDriverResponse::MatrixEventSent) .map_err(|e: crate::Error| e.to_string()) } - MatrixDriverRequestData::UpdateDelayedEvent(req) => self - .matrix_driver + MatrixDriverRequestData::UpdateDelayedEvent(req) => matrix_driver .update_delayed_event(req.delay_id, req.action) .await .map(MatrixDriverResponse::MatrixDelayedEventUpdate) .map_err(|e: HttpError| e.to_string()), }; - self.events_tx + // Forward the matrix driver response to the incoming message stream. + incoming_msg_tx .send(IncomingMessage::MatrixDriverResponse { request_id, response }) .map_err(|_| ())?; } Action::Subscribe => { // Only subscribe if we are not already subscribed. - if self.event_forwarding_guard.is_none() { - let (stop_forwarding, guard) = { - let token = CancellationToken::new(); - (token.child_token(), token.drop_guard()) - }; - - self.event_forwarding_guard = Some(guard); - let (mut matrix, events_tx) = - (self.matrix_driver.events(), self.events_tx.clone()); - - tokio::spawn(async move { - loop { - tokio::select! { - _ = stop_forwarding.cancelled() => { return } - - Some(event) = matrix.recv() => { - let _ = events_tx.send(IncomingMessage::MatrixEventReceived(event)); - } + if self.event_forwarding_guard.is_some() { + return Ok(()); + } + + let (stop_forwarding, guard) = { + let token = CancellationToken::new(); + (token.child_token(), token.drop_guard()) + }; + + self.event_forwarding_guard = Some(guard); + + let mut matrix = matrix_driver.events(); + let incoming_msg_tx = incoming_msg_tx.clone(); + + tokio::spawn(async move { + loop { + tokio::select! { + _ = stop_forwarding.cancelled() => { + // Upon cancellation, stop this task. + return; + } + + Some(event) = matrix.recv() => { + // Forward all events to the incoming messages stream. + let _ = incoming_msg_tx.send(IncomingMessage::MatrixEventReceived(event)); } } - }); - } + } + }); } Action::Unsubscribe => { From 0d01cabb8d5e5617beb681d08115b0b3fb5957ff Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 14 Nov 2024 11:20:10 +0100 Subject: [PATCH 534/979] refactor(widget): get rid of unused `limits` parameter when constructing a `WidgetMachine` --- crates/matrix-sdk/src/widget/machine/mod.rs | 7 ++-- .../src/widget/machine/tests/api_versions.rs | 10 ++---- .../src/widget/machine/tests/capabilities.rs | 12 +++---- .../src/widget/machine/tests/error.rs | 34 ++++++++----------- .../src/widget/machine/tests/openid.rs | 20 ++++------- crates/matrix-sdk/src/widget/matrix.rs | 4 +++ crates/matrix-sdk/src/widget/mod.rs | 1 - 7 files changed, 36 insertions(+), 52 deletions(-) diff --git a/crates/matrix-sdk/src/widget/machine/mod.rs b/crates/matrix-sdk/src/widget/machine/mod.rs index e7b86075b87..4649dc3efa0 100644 --- a/crates/matrix-sdk/src/widget/machine/mod.rs +++ b/crates/matrix-sdk/src/widget/machine/mod.rs @@ -132,12 +132,9 @@ impl WidgetMachine { widget_id: String, room_id: OwnedRoomId, init_on_content_load: bool, - limits: Option, ) -> (Self, Vec) { - let limits = limits.unwrap_or_else(|| RequestLimits { - max_pending_requests: 15, - response_timeout: Duration::from_secs(10), - }); + let limits = + RequestLimits { max_pending_requests: 15, response_timeout: Duration::from_secs(10) }; let mut machine = Self { widget_id, diff --git a/crates/matrix-sdk/src/widget/machine/tests/api_versions.rs b/crates/matrix-sdk/src/widget/machine/tests/api_versions.rs index 9b803e8f8e2..c85d9743d0d 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/api_versions.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/api_versions.rs @@ -20,13 +20,9 @@ use super::WIDGET_ID; use crate::widget::machine::{Action, IncomingMessage, WidgetMachine}; #[test] -fn get_supported_api_versions() { - let (mut machine, _) = WidgetMachine::new( - WIDGET_ID.to_owned(), - owned_room_id!("!a98sd12bjh:example.org"), - true, - None, - ); +fn test_get_supported_api_versions() { + let (mut machine, _) = + WidgetMachine::new(WIDGET_ID.to_owned(), owned_room_id!("!a98sd12bjh:example.org"), true); let actions = machine.process(IncomingMessage::WidgetMessage(json_string!({ "api": "fromWidget", diff --git a/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs b/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs index 3c06da002ae..633d7880163 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs @@ -23,16 +23,16 @@ use crate::widget::machine::{ }; #[test] -fn machine_can_negotiate_capabilities_immediately() { +fn test_machine_can_negotiate_capabilities_immediately() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false); assert_capabilities_dance(&mut machine, actions, None); } #[test] -fn machine_can_request_capabilities_on_content_load() { +fn test_machine_can_request_capabilities_on_content_load() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, true, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, true); assert!(actions.is_empty()); // Content loaded event processed. @@ -67,9 +67,9 @@ fn machine_can_request_capabilities_on_content_load() { } #[test] -fn capabilities_failure_results_into_empty_capabilities() { +fn test_capabilities_failure_results_into_empty_capabilities() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false); // Ask widget to provide desired capabilities. let actions = { diff --git a/crates/matrix-sdk/src/widget/machine/tests/error.rs b/crates/matrix-sdk/src/widget/machine/tests/error.rs index ccce797d478..a9508b0823b 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/error.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/error.rs @@ -20,9 +20,9 @@ use super::{capabilities::assert_capabilities_dance, parse_msg, WIDGET_ID}; use crate::widget::machine::{Action, IncomingMessage, WidgetMachine}; #[test] -fn machine_sends_error_for_unknown_request() { +fn test_machine_sends_error_for_unknown_request() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, _) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, true, None); + let (mut machine, _) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, true); let actions = machine.process(IncomingMessage::WidgetMessage(json_string!({ "api": "fromWidget", @@ -46,13 +46,9 @@ fn machine_sends_error_for_unknown_request() { } #[test] -fn read_messages_without_capabilities() { - let (mut machine, _) = WidgetMachine::new( - WIDGET_ID.to_owned(), - owned_room_id!("!a98sd12bjh:example.org"), - true, - None, - ); +fn test_read_messages_without_capabilities() { + let (mut machine, _) = + WidgetMachine::new(WIDGET_ID.to_owned(), owned_room_id!("!a98sd12bjh:example.org"), true); let actions = machine.process(IncomingMessage::WidgetMessage(json_string!({ "api": "fromWidget", @@ -77,9 +73,9 @@ fn read_messages_without_capabilities() { } #[test] -fn read_request_for_non_allowed_message_like_events() { +fn test_read_request_for_non_allowed_message_like_events() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false); assert_capabilities_dance(&mut machine, actions, None); let actions = machine.process(IncomingMessage::WidgetMessage(json_string!({ @@ -105,9 +101,9 @@ fn read_request_for_non_allowed_message_like_events() { } #[test] -fn read_request_for_non_allowed_state_events() { +fn test_read_request_for_non_allowed_state_events() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false); assert_capabilities_dance(&mut machine, actions, None); let actions = machine.process(IncomingMessage::WidgetMessage(json_string!({ @@ -134,9 +130,9 @@ fn read_request_for_non_allowed_state_events() { } #[test] -fn send_request_for_non_allowed_state_events() { +fn test_send_request_for_non_allowed_state_events() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false); assert_capabilities_dance( &mut machine, actions, @@ -166,9 +162,9 @@ fn send_request_for_non_allowed_state_events() { } #[test] -fn send_request_for_non_allowed_message_like_events() { +fn test_send_request_for_non_allowed_message_like_events() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false); assert_capabilities_dance( &mut machine, actions, @@ -198,9 +194,9 @@ fn send_request_for_non_allowed_message_like_events() { } #[test] -fn read_request_for_message_like_with_disallowed_msg_type_fails() { +fn test_read_request_for_message_like_with_disallowed_msg_type_fails() { let room_id = owned_room_id!("!a98sd12bjh:example.org"); - let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false, None); + let (mut machine, actions) = WidgetMachine::new(WIDGET_ID.to_owned(), room_id, false); assert_capabilities_dance( &mut machine, actions, diff --git a/crates/matrix-sdk/src/widget/machine/tests/openid.rs b/crates/matrix-sdk/src/widget/machine/tests/openid.rs index f5b248b3c0b..2aef7f66963 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/openid.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/openid.rs @@ -27,13 +27,9 @@ use crate::widget::machine::{ }; #[test] -fn openid_request_handling_works() { - let (mut machine, _) = WidgetMachine::new( - WIDGET_ID.to_owned(), - owned_room_id!("!a98sd12bjh:example.org"), - true, - None, - ); +fn test_openid_request_handling_works() { + let (mut machine, _) = + WidgetMachine::new(WIDGET_ID.to_owned(), owned_room_id!("!a98sd12bjh:example.org"), true); // Widget requests an open ID token, since we don't have any caching yet, // we reply with a pending response right away. @@ -112,13 +108,9 @@ fn openid_request_handling_works() { } #[test] -fn openid_fail_results_in_response_blocked() { - let (mut machine, _) = WidgetMachine::new( - WIDGET_ID.to_owned(), - owned_room_id!("!a98sd12bjh:example.org"), - true, - None, - ); +fn test_openid_fail_results_in_response_blocked() { + let (mut machine, _) = + WidgetMachine::new(WIDGET_ID.to_owned(), owned_room_id!("!a98sd12bjh:example.org"), true); // Widget requests an open ID token, since we don't have any caching yet, // we reply with a pending response right away. diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index db840dd673e..84e25de4c31 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -130,13 +130,16 @@ impl MatrixDriver { self.room.redact(&redacts, None, None).await?.event_id, )); } + Ok(match (state_key, delayed_event_parameters) { (None, None) => SendEventResponse::from_event_id( self.room.send_raw(&type_str, content).await?.event_id, ), + (Some(key), None) => SendEventResponse::from_event_id( self.room.send_state_event_raw(&type_str, &key, content).await?.event_id, ), + (None, Some(delayed_event_parameters)) => { let r = delayed_events::delayed_message_event::unstable::Request::new_raw( self.room.room_id().to_owned(), @@ -147,6 +150,7 @@ impl MatrixDriver { ); self.room.client.send(r, None).await.map(|r| r.into())? } + (Some(key), Some(delayed_event_parameters)) => { let r = delayed_events::delayed_state_event::unstable::Request::new_raw( self.room.room_id().to_owned(), diff --git a/crates/matrix-sdk/src/widget/mod.rs b/crates/matrix-sdk/src/widget/mod.rs index 74e847f0137..c80d0c0db75 100644 --- a/crates/matrix-sdk/src/widget/mod.rs +++ b/crates/matrix-sdk/src/widget/mod.rs @@ -154,7 +154,6 @@ impl WidgetDriver { self.settings.widget_id().to_owned(), room.room_id().to_owned(), self.settings.init_on_content_load(), - None, ); let matrix_driver = MatrixDriver::new(room.clone()); From a4999886219303173ea04713875aedfed3f67308 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 14 Nov 2024 16:24:05 +0100 Subject: [PATCH 535/979] task(CI): rename the upload code coverage task to make its name clearer --- .github/workflows/upload_coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/upload_coverage.yml b/.github/workflows/upload_coverage.yml index eda15459eed..72a1d832a2e 100644 --- a/.github/workflows/upload_coverage.yml +++ b/.github/workflows/upload_coverage.yml @@ -1,6 +1,6 @@ # Copied with minimal adjustments, source: # https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml -name: Codecov +name: Upload code coverage on: # This workflow is triggered after every successful execution From bf4a2ed297abd0b5286d2fd177d903ebc4fed5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 14 Nov 2024 11:26:03 +0100 Subject: [PATCH 536/979] feat(ffi): add `is_direct` and `fn inviter` to `RoomPreview` --- bindings/matrix-sdk-ffi/src/room_preview.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 505081fc526..14237d2544f 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -3,6 +3,7 @@ use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client}; use ruma::space::SpaceRoomJoinRule; use tracing::warn; +use crate::room_member::RoomMember; use crate::{client::JoinRule, error::ClientError, room::Membership, utils::AsyncRuntimeDropped}; /// A room preview for a room. It's intended to be used to represent rooms that @@ -33,6 +34,7 @@ impl RoomPreview { .clone() .try_into() .map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?, + is_direct: info.is_direct, }) } @@ -45,6 +47,13 @@ impl RoomPreview { self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?; room.leave().await.map_err(Into::into) } + + /// Get the user who created the invite, if any. + pub async fn inviter(&self) -> Option { + let room = self.client.get_room(&self.inner.room_id)?; + let invite_details = room.invite_details().await.ok()?; + invite_details.inviter.and_then(|m| m.try_into().ok()) + } } impl RoomPreview { @@ -76,6 +85,8 @@ pub struct RoomPreviewInfo { pub membership: Option, /// The join rule for this room (private, public, knock, etc.). pub join_rule: JoinRule, + /// Whether the room is direct or not, if known. + pub is_direct: Option, } impl TryFrom for JoinRule { From 97952902a36ec224ffa1845f596b6ab252a86739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 14 Nov 2024 11:58:42 +0100 Subject: [PATCH 537/979] feat(ffi): add `RoomPreviewInfo::num_active_members` --- bindings/matrix-sdk-ffi/src/room_preview.rs | 3 +++ crates/matrix-sdk/src/room_preview.rs | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 14237d2544f..5de8e5981c9 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -27,6 +27,7 @@ impl RoomPreview { avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()), num_joined_members: info.num_joined_members, room_type: info.room_type.as_ref().map(|room_type| room_type.to_string()), + num_active_members: info.num_active_members, is_history_world_readable: info.is_world_readable, membership: info.state.map(|state| state.into()), join_rule: info @@ -77,6 +78,8 @@ pub struct RoomPreviewInfo { pub avatar_url: Option, /// The number of joined members. pub num_joined_members: u64, + /// The number of active members, if known (joined + invited). + pub num_active_members: Option, /// The room type (space, custom) or nothing, if it's a regular room. pub room_type: Option, /// Is the history world-readable for this room? diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index c102b6225ff..3cccf952ea3 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -55,6 +55,9 @@ pub struct RoomPreview { /// The number of joined members. pub num_joined_members: u64, + /// The number of active members, if known (joined + invited). + pub num_active_members: Option, + /// The room type (space, custom) or nothing, if it's a regular room. pub room_type: Option, @@ -83,6 +86,7 @@ impl RoomPreview { room_info: RoomInfo, is_direct: Option, num_joined_members: u64, + num_active_members: Option, state: Option, ) -> Self { RoomPreview { @@ -107,6 +111,7 @@ impl RoomPreview { }, is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable, num_joined_members, + num_active_members, state, is_direct, } @@ -121,6 +126,7 @@ impl RoomPreview { room.clone_info(), is_direct, room.joined_members_count(), + Some(room.active_members_count()), Some(room.state()), ) } @@ -178,6 +184,8 @@ impl RoomPreview { response.membership.map(|membership| RoomState::from(&membership)) }; + let num_active_members = cached_room.as_ref().map(|r| r.active_members_count()); + let is_direct = if let Some(cached_room) = cached_room { cached_room.is_direct().await.ok() } else { @@ -191,6 +199,7 @@ impl RoomPreview { topic: response.topic, avatar_url: response.avatar_url, num_joined_members: response.num_joined_members.into(), + num_active_members, room_type: response.room_type, join_rule: response.join_rule, is_world_readable: response.world_readable, @@ -238,8 +247,15 @@ impl RoomPreview { let room = client.get_room(room_id); let state = room.as_ref().map(|room| room.state()); + let num_active_members = room.as_ref().map(|r| r.active_members_count()); let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None }; - Ok(Self::from_room_info(room_info, is_direct, num_joined_members, state)) + Ok(Self::from_room_info( + room_info, + is_direct, + num_joined_members, + num_active_members, + state, + )) } } From cefd5a27f5d765f28e3ff2fa35e569f8f3cfc0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 14 Nov 2024 11:59:10 +0100 Subject: [PATCH 538/979] feat(ffi): make `RoomPreviewInfo::room_type` an enum, not an optional String --- bindings/matrix-sdk-ffi/src/room_preview.rs | 36 ++++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 5de8e5981c9..3a3e08cb867 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -1,10 +1,12 @@ use anyhow::Context as _; use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client}; -use ruma::space::SpaceRoomJoinRule; +use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule}; use tracing::warn; -use crate::room_member::RoomMember; -use crate::{client::JoinRule, error::ClientError, room::Membership, utils::AsyncRuntimeDropped}; +use crate::{ + client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember, + utils::AsyncRuntimeDropped, +}; /// A room preview for a room. It's intended to be used to represent rooms that /// aren't joined yet. @@ -26,8 +28,8 @@ impl RoomPreview { topic: info.topic.clone(), avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()), num_joined_members: info.num_joined_members, - room_type: info.room_type.as_ref().map(|room_type| room_type.to_string()), num_active_members: info.num_active_members, + room_type: info.room_type.as_ref().into(), is_history_world_readable: info.is_world_readable, membership: info.state.map(|state| state.into()), join_rule: info @@ -81,7 +83,7 @@ pub struct RoomPreviewInfo { /// The number of active members, if known (joined + invited). pub num_active_members: Option, /// The room type (space, custom) or nothing, if it's a regular room. - pub room_type: Option, + pub room_type: RoomType, /// Is the history world-readable for this room? pub is_history_world_readable: bool, /// The membership state for the current user, if known. @@ -111,3 +113,27 @@ impl TryFrom for JoinRule { }) } } + +/// The type of room for a [`RoomPreviewInfo`]. +#[derive(Debug, Clone, uniffi::Enum)] +pub enum RoomType { + /// It's a plain chat room. + Room, + /// It's a space that can group several rooms. + Space, + /// It's a custom implementation. + Custom { value: String }, +} + +impl From> for RoomType { + fn from(value: Option<&RumaRoomType>) -> Self { + match value { + Some(RumaRoomType::Space) => RoomType::Space, + Some(RumaRoomType::_Custom(_)) => RoomType::Custom { + // SAFETY: this was checked in the match branch above + value: value.unwrap().to_string(), + }, + _ => RoomType::Room, + } + } +} From 232391c6b262f759f7422764dc2fc58f2d345094 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 14 Nov 2024 17:14:40 +0100 Subject: [PATCH 539/979] task(send queue): move some assertions back to logged errors Better safe than panicky. --- crates/matrix-sdk/src/send_queue.rs | 35 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 9f7befe4df5..b8a77ca8f05 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -958,7 +958,13 @@ impl QueueStorage { transaction_id: request.transaction_id.clone(), cancel_upload: cancel_upload_tx, }); - assert!(prev.is_none()); + + if let Some(prev) = prev { + error!( + prev_txn = ?prev.transaction_id, + "a previous request was still active while picking a new one" + ); + } Ok(Some((request.clone(), cancel_upload_rx))) } else { @@ -971,10 +977,11 @@ impl QueueStorage { /// be removed from the queue later. async fn mark_as_not_being_sent(&self, transaction_id: &TransactionId) { let was_being_sent = self.being_sent.write().await.take(); - assert_eq!( - was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()), - Some(transaction_id) - ); + + let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()); + if prev_txn != Some(transaction_id) { + error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after transient error)"); + } } /// Marks a request popped with [`Self::peek_next_to_send`] and identified @@ -988,10 +995,11 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; let was_being_sent = being_sent.take(); - assert_eq!( - was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()), - Some(transaction_id) - ); + + let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()); + if prev_txn != Some(transaction_id) { + error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after permanent error)"); + } Ok(self .client()? @@ -1023,10 +1031,11 @@ impl QueueStorage { // Keep the lock until we're done touching the storage. let mut being_sent = self.being_sent.write().await; let was_being_sent = being_sent.take(); - assert_eq!( - was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()), - Some(transaction_id) - ); + + let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()); + if prev_txn != Some(transaction_id) { + error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after successful send"); + } let client = self.client()?; let store = client.store(); From 3ed5d34f493330453d3cbb966f3ccbabb4a731d8 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 15 Nov 2024 17:27:49 +0000 Subject: [PATCH 540/979] feat(ffi): Add support for including captions with file uploads. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 99b9032d499..517554daa3f 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -418,15 +418,22 @@ impl Timeline { self: Arc, url: String, file_info: FileInfo, + caption: Option, + formatted_caption: Option, progress_watcher: Option>, use_send_queue: bool, ) -> Arc { + let formatted_caption = + formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_file_info: BaseFileInfo = BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?; let attachment_info = AttachmentInfo::File(base_file_info); - let attachment_config = AttachmentConfig::new().info(attachment_info); + let attachment_config = AttachmentConfig::new() + .info(attachment_info) + .caption(caption) + .formatted_caption(formatted_caption.map(Into::into)); self.send_attachment( url, From 31006ab3bf10dffe4389a4574c6bd2b9a58d29a7 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 11 Nov 2024 16:46:18 -0500 Subject: [PATCH 541/979] feat(crypto): pin identity when we withdraw verification --- .../matrix-sdk-crypto/src/identities/user.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index bbfdf54c4ee..62de8fa7f15 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -803,6 +803,9 @@ impl OtherUserIdentityData { /// reported to the user. In order to remove this notice users have to /// verify again or to withdraw the verification requirement. pub fn withdraw_verification(&self) { + // We also pin when we withdraw, since withdrawing implicitly acknowledges + // the identity change + self.pin(); self.previously_verified.store(false, Ordering::SeqCst) } @@ -1770,6 +1773,45 @@ pub(crate) mod tests { assert!(other_identity.inner.has_pin_violation()); } + #[async_test] + async fn test_resolve_identity_pin_violation_with_withdraw_verification() { + use test_json::keys_query_sets::IdentityChangeDataSet as DataSet; + + let my_user_id = user_id!("@me:localhost"); + let machine = OlmMachine::new(my_user_id, device_id!("ABCDEFGH")).await; + machine.bootstrap_cross_signing(false).await.unwrap(); + + let keys_query = DataSet::key_query_with_identity_a(); + let txn_id = TransactionId::new(); + machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap(); + + // Simulate an identity change + let keys_query = DataSet::key_query_with_identity_b(); + let txn_id = TransactionId::new(); + machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap(); + + let other_user_id = DataSet::user_id(); + + let other_identity = + machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap(); + + // For testing purpose mark it as previously verified + other_identity.mark_as_previously_verified().await.unwrap(); + + // The identity should need user approval now + assert!(other_identity.identity_needs_user_approval()); + + // We withdraw verification + other_identity.withdraw_verification().await.unwrap(); + + // The identity should not need any user approval now + let other_identity = + machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap(); + assert!(!other_identity.identity_needs_user_approval()); + // And should not have a pin violation + assert!(!other_identity.inner.has_pin_violation()); + } + #[async_test] async fn test_resolve_identity_verification_violation_with_withdraw() { use test_json::keys_query_sets::VerificationViolationTestData as DataSet; From 47246483fa43d52ac79b8a6190b27577da5fd906 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 16 Nov 2024 16:04:56 +0100 Subject: [PATCH 542/979] doc(crypto): Fix typo Signed-off-by: Tobias Fella --- crates/matrix-sdk-crypto/src/machine/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 9439a83dea4..660a6d13cf1 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -737,7 +737,7 @@ impl OlmMachine { .await } - /// Get the a key claiming request for the user/device pairs that we are + /// Get a key claiming request for the user/device pairs that we are /// missing Olm sessions for. /// /// Returns None if no key claiming request needs to be sent out. From a8a83c3b4563049669a54d5436700d5f2597fd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 15 Nov 2024 15:50:03 +0100 Subject: [PATCH 543/979] feat(room_preview): Use room directory search as another data source --- crates/matrix-sdk/src/room_preview.rs | 111 +++++++++++++++++- crates/matrix-sdk/src/test_utils/mocks.rs | 111 +++++++++++++++++- .../src/tests/sliding_sync/room.rs | 107 ++++++++++++++++- 3 files changed, 317 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 3cccf952ea3..f68371b4b28 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -18,9 +18,11 @@ //! This offers a few capabilities for previewing the content of the room as //! well. +use futures_util::future::join_all; use matrix_sdk_base::{RoomInfo, RoomState}; use ruma::{ api::client::{membership::joined_members, state::get_state_events}, + directory::PublicRoomJoinRule, events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}, room::RoomType, space::SpaceRoomJoinRule, @@ -29,7 +31,7 @@ use ruma::{ use tokio::try_join; use tracing::{instrument, warn}; -use crate::{Client, Room}; +use crate::{room_directory_search::RoomDirectorySearch, Client, Room}; /// The preview of a room, be it invited/joined/left, or not. #[derive(Debug, Clone)] @@ -140,21 +142,76 @@ impl RoomPreview { ) -> crate::Result { // Use the room summary endpoint, if available, as described in // https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md - match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via).await { + match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via.clone()).await + { Ok(res) => return Ok(res), Err(err) => { warn!("error when previewing room from the room summary endpoint: {err}"); } } - // TODO: (optimization) Use the room search directory, if available: - // - if the room directory visibility is public, - // - then use a public room filter set to this room id + // Try room directory search next. + match Self::from_room_directory_search(client, &room_id, room_or_alias_id, via).await { + Ok(Some(res)) => return Ok(res), + Ok(None) => warn!("Room '{room_or_alias_id}' not found in room directory search."), + Err(err) => { + warn!("Searching for '{room_or_alias_id}' in room directory search failed: {err}"); + } + } // Resort to using the room state endpoint, as well as the joined members one. Self::from_state_events(client, &room_id).await } + /// Get a [`RoomPreview`] by searching in the room directory for the + /// provided room alias or room id and transforming the [`RoomDescription`] + /// into a preview. + pub(crate) async fn from_room_directory_search( + client: &Client, + room_id: &RoomId, + room_or_alias_id: &RoomOrAliasId, + via: Vec, + ) -> crate::Result> { + // Get either the room alias or the room id without the leading identifier char + let search_term = if room_or_alias_id.is_room_alias_id() { + Some(room_or_alias_id.as_str()[1..].to_owned()) + } else { + None + }; + + // If we have no alias, filtering using a room id is impossible, so just take + // the first 100 results and try to find the current room #YOLO + let batch_size = if search_term.is_some() { 20 } else { 100 }; + + if via.is_empty() { + // Just search in the current homeserver + search_for_room_preview_in_room_directory( + client.clone(), + search_term, + batch_size, + None, + room_id, + ) + .await + } else { + let mut futures = Vec::new(); + // Search for all servers and retrieve the results + for server in via { + futures.push(search_for_room_preview_in_room_directory( + client.clone(), + search_term.clone(), + batch_size, + Some(server), + room_id, + )); + } + + let joined_results = join_all(futures).await; + + Ok(joined_results.into_iter().flatten().next().flatten()) + } + } + /// Get a [`RoomPreview`] using MSC3266, if available on the remote server. /// /// Will fail with a 404 if the API is not available. @@ -259,3 +316,47 @@ impl RoomPreview { )) } } + +async fn search_for_room_preview_in_room_directory( + client: Client, + filter: Option, + batch_size: u32, + server: Option, + expected_room_id: &RoomId, +) -> crate::Result> { + let mut directory_search = RoomDirectorySearch::new(client); + directory_search.search(filter, batch_size, server).await?; + + let (results, _) = directory_search.results(); + + for room_description in results { + // Iterate until we find a room description with a matching room id + if room_description.room_id != expected_room_id { + continue; + } + return Ok(Some(RoomPreview { + room_id: room_description.room_id, + canonical_alias: room_description.alias, + name: room_description.name, + topic: room_description.topic, + avatar_url: room_description.avatar_url, + num_joined_members: room_description.joined_members, + num_active_members: None, + // Assume it's a room + room_type: None, + join_rule: match room_description.join_rule { + PublicRoomJoinRule::Public => SpaceRoomJoinRule::Public, + PublicRoomJoinRule::Knock => SpaceRoomJoinRule::Knock, + PublicRoomJoinRule::_Custom(rule) => SpaceRoomJoinRule::_Custom(rule), + _ => { + panic!("Unexpected PublicRoomJoinRule {:?}", room_description.join_rule) + } + }, + is_world_readable: room_description.is_world_readable, + state: None, + is_direct: None, + })); + } + + Ok(None) +} diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 6f923eb73c2..9ca28630f43 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -17,24 +17,31 @@ #![allow(missing_debug_implementations)] -use std::sync::{Arc, Mutex}; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; use matrix_sdk_base::{deserialized_responses::TimelineEvent, store::StoreConfig, SessionMeta}; use matrix_sdk_test::{ test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, }; -use ruma::{api::MatrixVersion, device_id, user_id, MxcUri, OwnedEventId, OwnedRoomId, RoomId}; +use ruma::{ + api::MatrixVersion, device_id, directory::PublicRoomsChunk, user_id, MxcUri, OwnedEventId, + OwnedRoomId, RoomId, ServerName, +}; +use serde::Deserialize; use serde_json::json; use wiremock::{ matchers::{body_partial_json, header, method, path, path_regex}, - Mock, MockBuilder, MockGuard, MockServer, Respond, ResponseTemplate, Times, + Mock, MockBuilder, MockGuard, MockServer, Request, Respond, ResponseTemplate, Times, }; use crate::{ config::RequestConfig, matrix_auth::{MatrixSession, MatrixSessionTokens}, - Client, ClientBuilder, Room, + Client, ClientBuilder, OwnedServerName, Room, }; /// A [`wiremock`] [`MockServer`] along with useful methods to help mocking @@ -452,6 +459,49 @@ impl MatrixMockServer { Mock::given(method("PUT")).and(path_regex(r"/_matrix/client/v3/directory/room/.*")); MockEndpoint { mock, server: &self.server, endpoint: CreateRoomAliasEndpoint } } + + /// Create a prebuilt mock for listing public rooms. + /// + /// # Examples + /// + /// ``` + /// # + /// tokio_test::block_on(async { + /// use js_int::uint; + /// use ruma::directory::PublicRoomsChunkInit; + /// use matrix_sdk::room_directory_search::RoomDirectorySearch; + /// use matrix_sdk::{ + /// ruma::{event_id, room_id}, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// let event_id = event_id!("$id"); + /// let room_id = room_id!("!room_id:localhost"); + /// + /// let chunk = vec![PublicRoomsChunkInit { + /// num_joined_members: uint!(0), + /// room_id: room_id.to_owned(), + /// world_readable: true, + /// guest_can_join: true, + /// }.into()]; + /// + /// mock_server.mock_public_rooms().ok(chunk, None, None, Some(20)).mock_once().mount().await; + /// let mut room_directory_search = RoomDirectorySearch::new(client); + /// + /// room_directory_search.search(Some("some-alias".to_owned()), 100, None) + /// .await + /// .expect("Room directory search failed"); + /// + /// let (results, _) = room_directory_search.results(); + /// assert_eq!(results.len(), 1); + /// assert_eq!(results.get(0).unwrap().room_id, room_id.to_owned()); + /// # }); + /// ``` + pub fn mock_public_rooms(&self) -> MockEndpoint<'_, PublicRoomsEndpoint> { + let mock = Mock::given(method("POST")).and(path_regex(r"/_matrix/client/v3/publicRooms")); + MockEndpoint { mock, server: &self.server, endpoint: PublicRoomsEndpoint } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -1033,6 +1083,59 @@ impl<'a> MockEndpoint<'a, CreateRoomAliasEndpoint> { } } +/// A prebuilt mock for paginating the public room list. +pub struct PublicRoomsEndpoint; + +impl<'a> MockEndpoint<'a, PublicRoomsEndpoint> { + /// Returns a data endpoint for paginating the public room list. + pub fn ok( + self, + chunk: Vec, + next_batch: Option, + prev_batch: Option, + total_room_count_estimate: Option, + ) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "chunk": chunk, + "next_batch": next_batch, + "prev_batch": prev_batch, + "total_room_count_estimate": total_room_count_estimate, + }))); + MatrixMock { server: self.server, mock } + } + + /// Returns a data endpoint for paginating the public room list with several + /// `via` params. + /// + /// Each `via` param must be in the `server_map` parameter, otherwise it'll + /// fail. + pub fn ok_with_via_params( + self, + server_map: BTreeMap>, + ) -> MatrixMock<'a> { + let mock = self.mock.respond_with(move |req: &Request| { + #[derive(Deserialize)] + struct PartialRequest { + server: Option, + } + + let (_, server) = req + .url + .query_pairs() + .into_iter() + .find(|(key, _)| key == "server") + .expect("Server param not found in request URL"); + let server = ServerName::parse(server).expect("Couldn't parse server name"); + let chunk = server_map.get(&server).expect("Chunk for the server param not found"); + ResponseTemplate::new(200).set_body_json(json!({ + "chunk": chunk, + "total_room_count_estimate": chunk.len(), + })) + }); + MatrixMock { server: self.server, mock } + } +} + /// An augmented [`ClientBuilder`] that also allows for handling session login. pub struct MockClientBuilder { builder: ClientBuilder, diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index e8b41d9c528..9a775188fd6 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -2,6 +2,7 @@ #![allow(unused)] use std::{ + collections::BTreeMap, sync::{Arc, Mutex as StdMutex}, time::Duration, }; @@ -21,6 +22,7 @@ use matrix_sdk::{ room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, }, assign, + directory::PublicRoomsChunkInit, events::{ receipt::ReceiptThread, room::{ @@ -30,14 +32,19 @@ use matrix_sdk::{ }, AnySyncMessageLikeEvent, InitialStateEvent, Mentions, StateEventType, }, - mxc_uri, + mxc_uri, owned_server_name, room_id, space::SpaceRoomJoinRule, - RoomId, + uint, RoomId, }, sliding_sync::VersionBuilder, + test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, Client, RoomInfo, RoomMemberships, RoomState, SlidingSyncList, SlidingSyncMode, }; -use matrix_sdk_base::sliding_sync::http; +use matrix_sdk_base::{ + ruma::{owned_room_id, room_alias_id}, + sliding_sync::http, +}; +use matrix_sdk_test::async_test; use matrix_sdk_ui::{ room_list_service::filters::new_filter_all, sync_service::SyncService, timeline::RoomExt, RoomListService, @@ -1092,6 +1099,100 @@ async fn test_room_preview() -> Result<()> { Ok(()) } +#[async_test] +async fn test_room_preview_with_room_directory_search_and_room_alias_only() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_alias = room_alias_id!("#a-room:matrix.org"); + let expected_room_id = room_id!("!a-room:matrix.org"); + + // Allow resolving the room via the room directory + server + .mock_room_directory_resolve_alias() + .ok(expected_room_id.as_ref(), Vec::new()) + .mock_once() + .mount() + .await; + + // Given a successful public room search + let chunks = vec![PublicRoomsChunkInit { + num_joined_members: uint!(0), + room_id: expected_room_id.to_owned(), + world_readable: true, + guest_can_join: true, + } + .into()]; + server.mock_public_rooms().ok(chunks, None, None, Some(1)).mock_once().mount().await; + + // The room preview is found + let preview = client + .get_room_preview(room_alias.into(), Vec::new()) + .await + .expect("room preview couldn't be retrieved"); + assert_eq!(preview.room_id, expected_room_id); +} + +#[async_test] +async fn test_room_preview_with_room_directory_search_and_room_alias_only_in_several_homeservers() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_alias = room_alias_id!("#a-room:matrix.org"); + let expected_room_id = room_id!("!a-room:matrix.org"); + + // Allow resolving the room via the room directory + server + .mock_room_directory_resolve_alias() + .ok(expected_room_id.as_ref(), Vec::new()) + .mock_once() + .mount() + .await; + + let via_1 = owned_server_name!("server1.com"); + let via_2 = owned_server_name!("server2.com"); + + // Given a couple of successful public room search responses + let via_map = BTreeMap::from_iter(vec![ + ( + via_1.to_owned(), + // The actual room we want + vec![PublicRoomsChunkInit { + num_joined_members: uint!(0), + room_id: expected_room_id.to_owned(), + world_readable: true, + guest_can_join: true, + } + .into()], + ), + ( + via_2.to_owned(), + // Some other room + vec![PublicRoomsChunkInit { + num_joined_members: uint!(1), + room_id: owned_room_id!("!some-other-room:matrix.org"), + world_readable: true, + guest_can_join: true, + } + .into()], + ), + ]); + server + .mock_public_rooms() + .ok_with_via_params(via_map) + // Expect this to be called once for every server in the `via_map` + .expect(2) + .mount() + .await; + + // The room preview is found in the first response + let preview = client + .get_room_preview(room_alias.into(), vec![via_1, via_2]) + .await + .expect("room preview couldn't be retrieved"); + assert_eq!(preview.room_id, expected_room_id); +} + fn assert_room_preview(preview: &RoomPreview, room_alias: &str) { assert_eq!(preview.canonical_alias.as_ref().unwrap().alias(), room_alias); assert_eq!(preview.name.as_ref().unwrap(), "Alice's Room"); From f1a442bad058f6a8f250b95813a64d817fbe7f59 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 14 Nov 2024 16:22:35 +0100 Subject: [PATCH 544/979] refactor(send queue): use a specialized mutex for locking access to the state store and `being_sent` There was an implicit relationship that the `being_sent` lock needed to be taken in order to do non-atomic state store operations. With the change from this commit, the relationship is now more explicit: to get a handle to the state store, or being_sent, you have to obtain a `StoreLockGuard` by locking against the store itself. The `WeakClient` isn't stored in the QueueStorage data structure itself, so it's the only way to get a `dyn StateStore` from the `QueueStorage`. --- crates/matrix-sdk/src/send_queue.rs | 146 +++++++++++++-------- crates/matrix-sdk/src/send_queue/upload.rs | 12 +- 2 files changed, 99 insertions(+), 59 deletions(-) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index b8a77ca8f05..c248dc841e9 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -133,7 +133,7 @@ use std::{ str::FromStr as _, sync::{ atomic::{AtomicBool, Ordering}, - Arc, RwLock as SyncRwLock, + Arc, RwLock, }, }; @@ -161,7 +161,7 @@ use ruma::{ serde::Raw, OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, }; -use tokio::sync::{broadcast, oneshot, Notify, RwLock}; +use tokio::sync::{broadcast, oneshot, Mutex, Notify, OwnedMutexGuard}; use tracing::{debug, error, info, instrument, trace, warn}; #[cfg(feature = "e2e-encryption")] @@ -310,7 +310,7 @@ impl Client { pub(super) struct SendQueueData { /// Mapping of room to their unique send queue. - rooms: SyncRwLock>, + rooms: RwLock>, /// Is the whole mechanism enabled or disabled? /// @@ -877,19 +877,56 @@ impl BeingSentInfo { } } +/// A specialized lock that guards both against the state store and the +/// [`Self::being_sent`] data. #[derive(Clone)] -struct QueueStorage { +struct StoreLock { /// Reference to the client, to get access to the underlying store. client: WeakClient, - /// To which room is this storage related. - room_id: OwnedRoomId, - /// The one queued request that is being sent at the moment, along with /// associated data that can be useful to act upon it. /// - /// It also serves as an internal lock on the storage backend. - being_sent: Arc>>, + /// Also used as the lock to access the state store. + being_sent: Arc>>, +} + +impl StoreLock { + /// Gets a hold of the locked store and [`Self::being_sent`] pair. + async fn lock(&self) -> StoreLockGuard { + StoreLockGuard { + client: self.client.clone(), + being_sent: self.being_sent.clone().lock_owned().await, + } + } +} + +/// A lock guard obtained through locking with [`StoreLock`]. +/// `being_sent` data. +struct StoreLockGuard { + /// Reference to the client, to get access to the underlying store. + client: WeakClient, + + /// The one queued request that is being sent at the moment, along with + /// associated data that can be useful to act upon it. + being_sent: OwnedMutexGuard>, +} + +impl StoreLockGuard { + /// Get a client from the locked state, useful to get a handle on a store. + fn client(&self) -> Result { + self.client.get().ok_or(RoomSendQueueStorageError::ClientShuttingDown) + } +} + +#[derive(Clone)] +struct QueueStorage { + /// A lock to make sure the state store is only accessed once at a time, to + /// make some store operations atomic. + store: StoreLock, + + /// To which room is this storage related. + room_id: OwnedRoomId, } impl QueueStorage { @@ -901,12 +938,7 @@ impl QueueStorage { /// Create a new queue for queuing requests to be sent later. fn new(client: WeakClient, room: OwnedRoomId) -> Self { - Self { room_id: room, being_sent: Default::default(), client } - } - - /// Small helper to get a strong Client from the weak one. - fn client(&self) -> Result { - self.client.get().ok_or(RoomSendQueueStorageError::ClientShuttingDown) + Self { room_id: room, store: StoreLock { client, being_sent: Default::default() } } } /// Push a new event to be sent in the queue, with a default priority of 0. @@ -918,7 +950,10 @@ impl QueueStorage { ) -> Result { let transaction_id = TransactionId::new(); - self.client()? + self.store + .lock() + .await + .client()? .store() .save_send_queue_request( &self.room_id, @@ -939,11 +974,9 @@ impl QueueStorage { &self, ) -> Result>)>, RoomSendQueueStorageError> { - // Keep the lock until we're done touching the storage. - let mut being_sent = self.being_sent.write().await; - + let mut guard = self.store.lock().await; let queued_requests = - self.client()?.store().load_send_queue_requests(&self.room_id).await?; + guard.client()?.store().load_send_queue_requests(&self.room_id).await?; if let Some(request) = queued_requests.iter().find(|queued| !queued.is_wedged()) { let (cancel_upload_tx, cancel_upload_rx) = @@ -954,7 +987,7 @@ impl QueueStorage { Default::default() }; - let prev = being_sent.replace(BeingSentInfo { + let prev = guard.being_sent.replace(BeingSentInfo { transaction_id: request.transaction_id.clone(), cancel_upload: cancel_upload_tx, }); @@ -976,7 +1009,7 @@ impl QueueStorage { /// with the given transaction id as not being sent anymore, so it can /// be removed from the queue later. async fn mark_as_not_being_sent(&self, transaction_id: &TransactionId) { - let was_being_sent = self.being_sent.write().await.take(); + let was_being_sent = self.store.lock().await.being_sent.take(); let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()); if prev_txn != Some(transaction_id) { @@ -993,15 +1026,15 @@ impl QueueStorage { reason: QueueWedgeError, ) -> Result<(), RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. - let mut being_sent = self.being_sent.write().await; - let was_being_sent = being_sent.take(); + let mut guard = self.store.lock().await; + let was_being_sent = guard.being_sent.take(); let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()); if prev_txn != Some(transaction_id) { error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after permanent error)"); } - Ok(self + Ok(guard .client()? .store() .update_send_queue_request_status(&self.room_id, transaction_id, Some(reason)) @@ -1015,6 +1048,9 @@ impl QueueStorage { transaction_id: &TransactionId, ) -> Result<(), RoomSendQueueStorageError> { Ok(self + .store + .lock() + .await .client()? .store() .update_send_queue_request_status(&self.room_id, transaction_id, None) @@ -1029,15 +1065,15 @@ impl QueueStorage { parent_key: SentRequestKey, ) -> Result<(), RoomSendQueueStorageError> { // Keep the lock until we're done touching the storage. - let mut being_sent = self.being_sent.write().await; - let was_being_sent = being_sent.take(); + let mut guard = self.store.lock().await; + let was_being_sent = guard.being_sent.take(); let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref()); if prev_txn != Some(transaction_id) { error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after successful send"); } - let client = self.client()?; + let client = guard.client()?; let store = client.store(); // Update all dependent requests. @@ -1062,12 +1098,14 @@ impl QueueStorage { &self, transaction_id: &TransactionId, ) -> Result { - // Keep the lock until we're done touching the storage. - let being_sent = self.being_sent.read().await; + let guard = self.store.lock().await; - if being_sent.as_ref().map(|info| info.transaction_id.as_ref()) == Some(transaction_id) { + if guard.being_sent.as_ref().map(|info| info.transaction_id.as_ref()) + == Some(transaction_id) + { // Save the intent to redact the event. - self.client()? + guard + .client()? .store() .save_dependent_queued_request( &self.room_id, @@ -1080,8 +1118,11 @@ impl QueueStorage { return Ok(true); } - let removed = - self.client()?.store().remove_send_queue_request(&self.room_id, transaction_id).await?; + let removed = guard + .client()? + .store() + .remove_send_queue_request(&self.room_id, transaction_id) + .await?; Ok(removed) } @@ -1097,12 +1138,14 @@ impl QueueStorage { transaction_id: &TransactionId, serializable: SerializableEventContent, ) -> Result { - // Keep the lock until we're done touching the storage. - let being_sent = self.being_sent.read().await; + let guard = self.store.lock().await; - if being_sent.as_ref().map(|info| info.transaction_id.as_ref()) == Some(transaction_id) { + if guard.being_sent.as_ref().map(|info| info.transaction_id.as_ref()) + == Some(transaction_id) + { // Save the intent to edit the associated event. - self.client()? + guard + .client()? .store() .save_dependent_queued_request( &self.room_id, @@ -1115,7 +1158,7 @@ impl QueueStorage { return Ok(true); } - let edited = self + let edited = guard .client()? .store() .update_send_queue_request(&self.room_id, transaction_id, serializable.into()) @@ -1136,12 +1179,8 @@ impl QueueStorage { file_media_request: MediaRequestParameters, thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequestParameters, Mime)>, ) -> Result<(), RoomSendQueueStorageError> { - // Keep the lock until we're done touching the storage. - // TODO refactor to make the relationship between being_sent and the store more - // obvious. - let _guard = self.being_sent.read().await; - - let client = self.client()?; + let guard = self.store.lock().await; + let client = guard.client()?; let store = client.store(); let thumbnail_info = @@ -1223,7 +1262,8 @@ impl QueueStorage { transaction_id: &TransactionId, key: String, ) -> Result, RoomSendQueueStorageError> { - let client = self.client()?; + let guard = self.store.lock().await; + let client = guard.client()?; let store = client.store(); let requests = store.load_send_queue_requests(&self.room_id).await?; @@ -1253,7 +1293,8 @@ impl QueueStorage { &self, room: &RoomSendQueue, ) -> Result, RoomSendQueueStorageError> { - let client = self.client()?; + let guard = self.store.lock().await; + let client = guard.client()?; let store = client.store(); let local_requests = @@ -1563,10 +1604,9 @@ impl QueueStorage { &self, new_updates: &mut Vec, ) -> Result<(), RoomSendQueueError> { - // Keep the lock until we're done touching the storage. - let _being_sent = self.being_sent.read().await; + let guard = self.store.lock().await; - let client = self.client()?; + let client = guard.client()?; let store = client.store(); let dependent_requests = store @@ -1637,10 +1677,10 @@ impl QueueStorage { &self, dependent_event_id: &ChildTransactionId, ) -> Result { - // Keep the lock until we're done touching the storage. - let _being_sent = self.being_sent.read().await; - Ok(self + .store + .lock() + .await .client()? .store() .remove_dependent_queued_request(&self.room_id, dependent_event_id) diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 9e885d3cc57..e363574458b 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -429,10 +429,10 @@ impl QueueStorage { event_txn: &TransactionId, handles: &MediaHandles, ) -> Result { - let client = self.client()?; + let mut guard = self.store.lock().await; + let client = guard.client()?; // Keep the lock until we're done touching the storage. - let mut being_sent = self.being_sent.write().await; debug!("trying to abort an upload"); let store = client.store(); @@ -450,10 +450,10 @@ impl QueueStorage { trace!("could remove thumbnail request, removing 2 dependent requests now"); // 1. Try to abort sending using the being_sent info, in case it was active. - if let Some(info) = being_sent.as_ref() { + if let Some(info) = guard.being_sent.as_ref() { if info.transaction_id == *thumbnail_txn { // SAFETY: we knew it was Some(), two lines above. - let info = being_sent.take().unwrap(); + let info = guard.being_sent.take().unwrap(); if info.cancel_upload() { trace!("aborted ongoing thumbnail upload"); } @@ -492,10 +492,10 @@ impl QueueStorage { trace!("could remove file upload request, removing 1 dependent request"); // 1. Try to abort sending using the being_sent info, in case it was active. - if let Some(info) = being_sent.as_ref() { + if let Some(info) = guard.being_sent.as_ref() { if info.transaction_id == handles.upload_file_txn { // SAFETY: we knew it was Some(), two lines above. - let info = being_sent.take().unwrap(); + let info = guard.being_sent.take().unwrap(); if info.cancel_upload() { trace!("aborted ongoing file upload"); } From 21bb85ac21eb8d38f8ea965ffff5983db5571fee Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:15:28 +0100 Subject: [PATCH 545/979] feat(Room): Check if the user is allowed to do a room mention before trying to send a call notify event. (#4271) --- crates/matrix-sdk/src/room/mod.rs | 4 ++ .../tests/integration/room/joined.rs | 47 +++++++++++++++++-- .../src/test_json/sync_events.rs | 3 ++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 00484d31457..c8b95ca283e 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2975,6 +2975,10 @@ impl Room { return Ok(()); } + if !self.can_user_trigger_room_notification(self.own_user_id()).await? { + return Ok(()); + } + self.send_call_notification( self.room_id().to_string().to_owned(), ApplicationType::Call, diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 2c81f1caaca..440d1a9030d 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -14,8 +14,8 @@ use matrix_sdk_test::{ async_test, mocks::{mock_encryption_state, mock_redaction}, test_json::{self, sync::CUSTOM_ROOM_POWER_LEVELS}, - EphemeralTestEvent, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, - DEFAULT_TEST_ROOM_ID, + EphemeralTestEvent, GlobalAccountDataTestEvent, JoinedRoomBuilder, StateTestEvent, + SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, }; use ruma::{ api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType}, @@ -635,7 +635,8 @@ async fn test_call_notifications_ring_for_dms() { let (client, server) = logged_in_client_with_server().await; let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::default()); + let room_builder = JoinedRoomBuilder::default().add_state_event(StateTestEvent::PowerLevels); + sync_builder.add_joined_room(room_builder); sync_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Direct); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; @@ -678,9 +679,10 @@ async fn test_call_notifications_notify_for_rooms() { let (client, server) = logged_in_client_with_server().await; let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::default()); - + let room_builder = JoinedRoomBuilder::default().add_state_event(StateTestEvent::PowerLevels); + sync_builder.add_joined_room(room_builder); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + mock_encryption_state(&server, false).await; let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); @@ -715,6 +717,41 @@ async fn test_call_notifications_notify_for_rooms() { room.send_call_notification_if_needed().await.unwrap(); } +#[async_test] +async fn test_call_notifications_dont_notify_room_without_mention_powerlevel() { + let (client, server) = logged_in_client_with_server().await; + + let mut sync_builder = SyncResponseBuilder::new(); + let mut power_level_event = StateTestEvent::PowerLevels.into_json_value(); + // Allow noone to send room notify events. + *power_level_event.get_mut("content").unwrap().get_mut("notifications").unwrap() = + json!({"room": 101}); + + sync_builder.add_joined_room( + JoinedRoomBuilder::default().add_state_event(StateTestEvent::Custom(power_level_event)), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + assert!(!room.is_direct().await.unwrap()); + assert!(!room.has_active_room_call()); + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({"event_id": "$event_id"}))) + // Expect no calls of the send because we dont have permission to notify. + .expect(0) + .mount(&server) + .await; + + room.send_call_notification_if_needed().await.unwrap(); +} + #[async_test] async fn test_make_reply_event_doesnt_require_event_cache() { // Even if we don't have enabled the event cache, we'll resort to using the diff --git a/testing/matrix-sdk-test/src/test_json/sync_events.rs b/testing/matrix-sdk-test/src/test_json/sync_events.rs index e5f5d824b29..df68df07bc9 100644 --- a/testing/matrix-sdk-test/src/test_json/sync_events.rs +++ b/testing/matrix-sdk-test/src/test_json/sync_events.rs @@ -350,6 +350,9 @@ pub static POWER_LEVELS: Lazy = Lazy::new(|| { "kick": 50, "redact": 50, "state_default": 50, + "notifications": { + "room": 0 + }, "users": { "@example:localhost": 100, "@bob:localhost": 0 From 05505a5a48f840be9099cf8548b3fa8ecb6d5283 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:55:34 +0000 Subject: [PATCH 546/979] chore(deps): bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/upload_coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/upload_coverage.yml b/.github/workflows/upload_coverage.yml index 72a1d832a2e..67ed06a0213 100644 --- a/.github/workflows/upload_coverage.yml +++ b/.github/workflows/upload_coverage.yml @@ -64,7 +64,7 @@ jobs: path: repo_root - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} fail_ci_if_error: true From 22bbe0c32e507504e3e9b614391bf674fc1acc0c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 19 Nov 2024 10:30:31 +0000 Subject: [PATCH 547/979] Add 'conn_id' field to `sync_once` span This is to make it easier to see which sync requests are for which connection when debugging. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 322a7563f00..36f4f896a1e 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -703,7 +703,7 @@ impl SlidingSync { || !self.inner.lists.read().await.is_empty() } - #[instrument(skip_all, fields(pos))] + #[instrument(skip_all, fields(pos, conn_id = self.inner.id))] async fn sync_once(&self) -> Result { let (request, request_config, position_guard) = self.generate_sync_request(&mut LazyTransactionId::new()).await?; From e4ebeb8a4271e9a55de5e00e9bae74db2082092c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 4 Nov 2024 14:34:05 +0100 Subject: [PATCH 548/979] feat(base): Introduce a DisplayName struct This patch introduces a struct that normalizes and sanitizes display names. Display names can be a source of abuse and can contain characters which might make it hard to distinguish one display name from the other. This struct attempts to make it easier to protect against such abuse. Changelog: Introduce a DisplayName struct which normalizes and sanitizes display names. Co-authored-by: Denis Kasak --- Cargo.lock | 18 +- crates/matrix-sdk-base/Cargo.toml | 5 +- .../src/deserialized_responses.rs | 414 +++++++++++++++++- 3 files changed, 433 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80ca196886a..0d23120497d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,6 +1246,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "decancer" +version = "3.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a41401dd84c9335e2f5aec7f64057e243585d62622260d41c245919a601ccc9" +dependencies = [ + "lazy_static", + "paste", + "regex", +] + [[package]] name = "delegate-display" version = "2.1.1" @@ -2955,6 +2966,7 @@ dependencies = [ "assign", "async-trait", "bitflags 2.6.0", + "decancer", "eyeball", "eyeball-im", "futures-executor", @@ -2970,10 +2982,12 @@ dependencies = [ "ruma", "serde", "serde_json", + "similar-asserts", "stream_assert", "thiserror", "tokio", "tracing", + "unicode-normalization", "uniffi", "wasm-bindgen-test", ] @@ -5907,9 +5921,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 0c8d575d4b2..d54aa38ff6d 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -50,6 +50,7 @@ assert_matches = { workspace = true, optional = true } assert_matches2 = { workspace = true, optional = true } async-trait = { workspace = true } bitflags = { version = "2.4.0", features = ["serde"] } +decancer = "3.2.4" eyeball = { workspace = true } eyeball-im = { workspace = true } futures-util = { workspace = true } @@ -60,14 +61,15 @@ matrix-sdk-crypto = { workspace = true, optional = true } matrix-sdk-store-encryption = { workspace = true } matrix-sdk-test = { workspace = true, optional = true } once_cell = { workspace = true } +regex = "1.11.0" ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] } +unicode-normalization = "0.1.24" serde = { workspace = true, features = ["rc"] } serde_json = { workspace = true } tokio = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } uniffi = { workspace = true, optional = true } -regex = "1.11.1" [dev-dependencies] assert_matches = { workspace = true } @@ -77,6 +79,7 @@ futures-executor = { workspace = true } http = { workspace = true } matrix-sdk-test = { workspace = true } stream_assert = { workspace = true } +similar-asserts = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index 9c73ea64703..47535f1fe63 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -14,9 +14,11 @@ //! SDK-specific variations of response types from Ruma. -use std::{collections::BTreeMap, fmt, iter}; +use std::{collections::BTreeMap, fmt, hash::Hash, iter}; pub use matrix_sdk_common::deserialized_responses::*; +use once_cell::sync::Lazy; +use regex::Regex; use ruma::{ events::{ room::{ @@ -31,6 +33,7 @@ use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId, }; use serde::Serialize; +use unicode_normalization::UnicodeNormalization; /// A change in ambiguity of room members that an `m.room.member` event /// triggers. @@ -67,6 +70,178 @@ pub struct AmbiguityChanges { pub changes: BTreeMap>, } +static MXID_REGEX: Lazy = Lazy::new(|| { + Regex::new(DisplayName::MXID_PATTERN) + .expect("We should be able to create a regex from our static MXID pattern") +}); +static LEFT_TO_RIGHT_REGEX: Lazy = Lazy::new(|| { + Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN) + .expect("We should be able to create a regex from our static left-to-right pattern") +}); +static HIDDEN_CHARACTERS_REGEX: Lazy = Lazy::new(|| { + Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN) + .expect("We should be able to create a regex from our static hidden characters pattern") +}); + +/// Regex to match `i` characters. +/// +/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello" +/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us. +static I_REGEX: Lazy = Lazy::new(|| { + Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern") +}); + +/// Regex to match `0` characters. +/// +/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO" +/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us. +static ZERO_REGEX: Lazy = Lazy::new(|| { + Regex::new("[0]").expect("We should be able to create a regex from our zero pattern") +}); + +/// Regex to match a couple of dot-like characters, also matches an actual dot. +/// +/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as +/// ambiguous. +static DOT_REGEX: Lazy = Lazy::new(|| { + Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern") +}); + +/// A high-level wrapper for strings representing display names. +/// +/// This wrapper provides attempts to determine whether a display name +/// contains characters that could make it ambiguous or easily confused +/// with similar names. +/// +/// +/// # Examples +/// +/// ``` +/// use matrix_sdk_base::deserialized_responses::DisplayName; +/// +/// let display_name = DisplayName::new("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶"); +/// +/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str(). +/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla")); +/// ``` +/// +/// ``` +/// # use matrix_sdk_base::deserialized_responses::DisplayName; +/// let display_name = DisplayName::new("@alice:localhost"); +/// +/// // The display name looks like an MXID, which makes it ambiguous. +/// assert!(display_name.is_inherently_ambiguous()); +/// ``` +#[derive(Debug, Clone, Eq)] +pub struct DisplayName { + raw: String, + decancered: Option, +} + +impl Hash for DisplayName { + fn hash(&self, state: &mut H) { + if let Some(decancered) = &self.decancered { + decancered.hash(state); + } else { + self.raw.hash(state); + } + } +} + +impl PartialEq for DisplayName { + fn eq(&self, other: &Self) -> bool { + match (self.decancered.as_deref(), other.decancered.as_deref()) { + (None, None) => self.raw == other.raw, + (None, Some(_)) | (Some(_), None) => false, + (Some(this), Some(other)) => this == other, + } + } +} + +impl DisplayName { + /// Regex pattern matching an MXID. + const MXID_PATTERN: &str = "@.+[:.].+"; + + /// Regex pattern matching some left-to-right formatting marks: + /// * LTR and RTL marks U+200E and U+200F + /// * LTR/RTL and other directional formatting marks U+202A - U+202F + const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]"; + + /// Regex pattern matching bunch of unicode control characters and otherwise + /// misleading/invisible characters. + /// + /// This includes: + /// * various width spaces U+2000 - U+200D + /// * Combining characters U+0300 - U+036F + /// * Blank/invisible characters (U2800, U2062-U2063) + /// * Arabic Letter RTL mark U+061C + /// * Zero width no-break space (BOM) U+FEFF + const HIDDEN_CHARACTERS_PATTERN: &str = + "[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]"; + + /// Creates a new [`DisplayName`] from the given raw string. + /// + /// The raw display name is transformed into a Unicode-normalized form, with + /// common confusable characters removed to reduce ambiguity. + /// + /// **Note**: If removing confusable characters fails, + /// [`DisplayName::is_inherently_ambiguous`] will return `true`, and + /// [`DisplayName::as_normalized_str()`] will return `None. + pub fn new(raw: &str) -> Self { + let normalized = raw.nfd().collect::(); + let replaced = DOT_REGEX.replace_all(&normalized, ":"); + let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, ""); + + let decancered = decancer::cure!(&replaced).ok().map(|cured| { + let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), ""); + let replaced = I_REGEX.replace_all(&removed_left_to_right, "l"); + // We re-run the dot replacement because decancer normalized a lot of weird + // characets into a `.`, it just doesn't do that for /u{1d16d}. + let replaced = DOT_REGEX.replace_all(&replaced, ":"); + let replaced = ZERO_REGEX.replace_all(&replaced, "o"); + + replaced.to_string() + }); + + Self { raw: raw.to_owned(), decancered } + } + + /// Is this display name considered to be ambiguous? + /// + /// If the display name has cancer (i.e. fails normalisation or has a + /// different normalised form) or looks like an MXID, then it's ambiguous. + pub fn is_inherently_ambiguous(&self) -> bool { + // If we look like an MXID or have hidden characters then we're ambiguous. + self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none() + } + + /// Returns the underlying raw and and unsanitized string of this + /// [`DisplayName`]. + pub fn as_raw_str(&self) -> &str { + &self.raw + } + + /// Returns the underlying normalized and and sanitized string of this + /// [`DisplayName`]. + /// + /// Returns `None` if normalization failed during construction of this + /// [`DisplayName`]. + pub fn as_normalized_str(&self) -> Option<&str> { + self.decancered.as_deref() + } + + fn has_hidden_characters(&self) -> bool { + HIDDEN_CHARACTERS_REGEX.is_match(&self.raw) + } + + fn looks_like_an_mxid(&self) -> bool { + self.decancered + .as_deref() + .map(|d| MXID_REGEX.is_match(d)) + .unwrap_or_else(|| MXID_REGEX.is_match(&self.raw)) + } +} + /// A deserialized response for the rooms members API call. /// /// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers) @@ -310,3 +485,240 @@ impl SyncOrStrippedState { } } } + +#[cfg(test)] +mod test { + macro_rules! assert_display_name_eq { + ($left:expr, $right:expr $(, $desc:expr)?) => {{ + let left = crate::deserialized_responses::DisplayName::new($left); + let right = crate::deserialized_responses::DisplayName::new($right); + + similar_asserts::assert_eq!( + left, + right + $(, $desc)? + ); + }}; + } + + macro_rules! assert_display_name_ne { + ($left:expr, $right:expr $(, $desc:expr)?) => {{ + let left = crate::deserialized_responses::DisplayName::new($left); + let right = crate::deserialized_responses::DisplayName::new($right); + + assert_ne!( + left, + right + $(, $desc)? + ); + }}; + } + + macro_rules! assert_ambiguous { + ($name:expr) => { + let name = crate::deserialized_responses::DisplayName::new($name); + + assert!( + name.is_inherently_ambiguous(), + "The display {:?} should be considered amgibuous", + name + ); + }; + } + + macro_rules! assert_not_ambiguous { + ($name:expr) => { + let name = crate::deserialized_responses::DisplayName::new($name); + + assert!( + !name.is_inherently_ambiguous(), + "The display {:?} should not be considered amgibuous", + name + ); + }; + } + + #[test] + fn test_display_name_inherently_ambiguous() { + // These should not be inherently ambiguous, only if another similarly looking + // display name appears should they be considered to be ambiguous. + assert_not_ambiguous!("Alice"); + assert_not_ambiguous!("Carol"); + assert_not_ambiguous!("Car0l"); + assert_not_ambiguous!("Ivan"); + assert_not_ambiguous!("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶"); + assert_not_ambiguous!("Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ"); + assert_not_ambiguous!("🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰"); + assert_not_ambiguous!("Sahasrahla"); + // Left to right is fine, if it's the only one in the room. + assert_not_ambiguous!("\u{202e}alharsahas"); + + // These on the other hand contain invisible chars. + assert_ambiguous!("Sa̴hasrahla"); + assert_ambiguous!("Sahas\u{200D}rahla"); + } + + #[test] + fn test_display_name_equality_capitalization() { + // Display name with different capitalization + assert_display_name_eq!("Alice", "alice"); + } + + #[test] + fn test_display_name_equality_different_names() { + // Different display names + assert_display_name_ne!("Alice", "Carol"); + } + + #[test] + fn test_display_name_equality_capital_l() { + // Different display names + assert_display_name_eq!("Hello", "HeIlo"); + } + + #[test] + fn test_display_name_equality_confusable_zero() { + // Different display names + assert_display_name_eq!("Carol", "Car0l"); + } + + #[test] + fn test_display_name_equality_cyrilic() { + // Display name with scritpure symbols + assert_display_name_eq!("alice", "аlice"); + } + + #[test] + fn test_display_name_equality_scriptures() { + // Display name with scritpure symbols + assert_display_name_eq!("Sahasrahla", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶"); + } + + #[test] + fn test_display_name_equality_frakturs() { + // Display name with fraktur symbols + assert_display_name_eq!("Sahasrahla", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞"); + } + + #[test] + fn test_display_name_equality_circled() { + // Display name with circled symbols + assert_display_name_eq!("Sahasrahla", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ"); + } + + #[test] + fn test_display_name_equality_squared() { + // Display name with squared symbols + assert_display_name_eq!("Sahasrahla", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰"); + } + + #[test] + fn test_display_name_equality_big_unicode() { + // Display name with big unicode letters + assert_display_name_eq!("Sahasrahla", "Sahasrahla"); + } + + #[test] + fn test_display_name_equality_left_to_right() { + // Display name with a left-to-right character + assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas"); + } + + #[test] + fn test_display_name_equality_diacritical() { + // Display name with a diacritical mark. + assert_display_name_eq!("Sahasrahla", "Sa̴hasrahla"); + } + + #[test] + fn test_display_name_equality_zero_width_joiner() { + // Display name with a zero-width joiner + assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla"); + } + + #[test] + fn test_display_name_equality_zero_width_space() { + // Display name with zero-width space. + assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla"); + } + + #[test] + fn test_display_name_equality_ligatures() { + // Display name with a ligature. + assert_display_name_eq!("ff", "\u{FB00}"); + } + + #[test] + fn test_display_name_confusable_mxid_colon() { + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld"); + + // Additionally these should be considered to be ambiguous on their own. + assert_ambiguous!("@mxid\u{0589}domain.tld"); + assert_ambiguous!("@mxid\u{05c3}domain.tld"); + assert_ambiguous!("@mxid\u{0703}domain.tld"); + assert_ambiguous!("@mxid\u{0a83}domain.tld"); + assert_ambiguous!("@mxid\u{16ec}domain.tld"); + assert_ambiguous!("@mxid\u{205a}domain.tld"); + assert_ambiguous!("@mxid\u{2236}domain.tld"); + assert_ambiguous!("@mxid\u{fe13}domain.tld"); + assert_ambiguous!("@mxid\u{fe52}domain.tld"); + assert_ambiguous!("@mxid\u{fe30}domain.tld"); + assert_ambiguous!("@mxid\u{ff1a}domain.tld"); + } + + #[test] + fn test_display_name_confusable_mxid_dot() { + assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld"); + + // Additionally these should be considered to be ambiguous on their own. + assert_ambiguous!("@mxid:domain\u{0701}tld"); + assert_ambiguous!("@mxid:domain\u{0702}tld"); + assert_ambiguous!("@mxid:domain\u{2024}tld"); + assert_ambiguous!("@mxid:domain\u{fe52}tld"); + assert_ambiguous!("@mxid:domain\u{ff0e}tld"); + assert_ambiguous!("@mxid:domain\u{1d16d}tld"); + } + + #[test] + fn test_display_name_confusable_mxid_replacing_a() { + assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld"); + assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld"); + + // Additionally these should be considered to be ambiguous on their own. + assert_ambiguous!("@mxid:dom\u{1d44e}in.tld"); + assert_ambiguous!("@mxid:dom\u{0430}in.tld"); + } + + #[test] + fn test_display_name_confusable_mxid_replacing_l() { + assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId"); + assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d"); + assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d"); + assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d"); + assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d"); + assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d"); + + // Additionally these should be considered to be ambiguous on their own. + assert_ambiguous!("@mxid:domain.tId"); + assert_ambiguous!("@mxid:domain.t\u{217c}d"); + assert_ambiguous!("@mxid:domain.t\u{ff4c}d"); + assert_ambiguous!("@mxid:domain.t\u{1d5f9}d"); + assert_ambiguous!("@mxid:domain.t\u{1d695}d"); + assert_ambiguous!("@mxid:domain.t\u{2223}d"); + } +} From d40aac89cb13ff849c45c3ed83fac769c021a088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 4 Nov 2024 15:30:08 +0100 Subject: [PATCH 549/979] fix: Use the DisplayName struct to protect against homoglyph attacks --- crates/matrix-sdk-base/src/client.rs | 12 +- .../src/deserialized_responses.rs | 10 +- crates/matrix-sdk-base/src/rooms/members.rs | 13 +- crates/matrix-sdk-base/src/rooms/normal.rs | 9 +- .../matrix-sdk-base/src/sliding_sync/mod.rs | 12 +- .../src/store/ambiguity_map.rs | 245 +++++++++++++++++- .../src/store/integration_tests.rs | 32 ++- .../matrix-sdk-base/src/store/memory_store.rs | 20 +- crates/matrix-sdk-base/src/store/mod.rs | 5 +- crates/matrix-sdk-base/src/store/traits.rs | 18 +- .../src/state_store/mod.rs | 47 +++- crates/matrix-sdk-sqlite/src/state_store.rs | 90 +++++-- 12 files changed, 408 insertions(+), 105 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index a65d3e34ee7..1fcaee38b26 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -16,7 +16,7 @@ #[cfg(feature = "e2e-encryption")] use std::sync::Arc; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt, iter, ops::Deref, }; @@ -68,7 +68,7 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat #[cfg(feature = "e2e-encryption")] use crate::RoomMemberships; use crate::{ - deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent}, + deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent}, error::{Error, Result}, event_cache::store::EventCacheStoreLock, response_processors::AccountDataProcessor, @@ -1332,7 +1332,7 @@ impl BaseClient { #[cfg(feature = "e2e-encryption")] let mut user_ids = BTreeSet::new(); - let mut ambiguity_map: BTreeMap> = BTreeMap::new(); + let mut ambiguity_map: HashMap> = Default::default(); for raw_event in &response.chunk { let member = match raw_event.deserialize() { @@ -1363,7 +1363,11 @@ impl BaseClient { if let StateEvent::Original(e) = &member { if let Some(d) = &e.content.displayname { - ambiguity_map.entry(d.clone()).or_default().insert(member.state_key().clone()); + let display_name = DisplayName::new(d); + ambiguity_map + .entry(display_name) + .or_default() + .insert(member.state_key().clone()); } } diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index 47535f1fe63..b8f0f81701a 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -469,10 +469,12 @@ impl MemberEvent { /// /// It there is no `displayname` in the event's content, the localpart or /// the user ID is returned. - pub fn display_name(&self) -> &str { - self.original_content() - .and_then(|c| c.displayname.as_deref()) - .unwrap_or_else(|| self.user_id().localpart()) + pub fn display_name(&self) -> DisplayName { + DisplayName::new( + self.original_content() + .and_then(|c| c.displayname.as_deref()) + .unwrap_or_else(|| self.user_id().localpart()), + ) } } diff --git a/crates/matrix-sdk-base/src/rooms/members.rs b/crates/matrix-sdk-base/src/rooms/members.rs index ad5764b7c5c..a013653949b 100644 --- a/crates/matrix-sdk-base/src/rooms/members.rs +++ b/crates/matrix-sdk-base/src/rooms/members.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeSet, HashMap}, sync::Arc, }; @@ -30,7 +30,8 @@ use ruma::{ }; use crate::{ - deserialized_responses::{MemberEvent, SyncOrStrippedState}, + deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState}, + store::ambiguity_map::is_display_name_ambiguous, MinimalRoomMemberEvent, }; @@ -67,8 +68,10 @@ impl RoomMember { } = room_info; let is_room_creator = room_creator.as_deref() == Some(event.user_id()); - let display_name_ambiguous = - users_display_names.get(event.display_name()).is_some_and(|s| s.len() > 1); + let display_name = event.display_name(); + let display_name_ambiguous = users_display_names + .get(&display_name) + .is_some_and(|s| is_display_name_ambiguous(&display_name, s)); let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id())); Self { @@ -245,6 +248,6 @@ pub(crate) struct MemberRoomInfo<'a> { pub(crate) power_levels: Arc>>, pub(crate) max_power_level: i64, pub(crate) room_creator: Option, - pub(crate) users_display_names: BTreeMap<&'a str, BTreeSet>, + pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet>, pub(crate) ignored_users: Option>, } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 81bca1b6454..64e4b59f2fa 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -67,7 +67,7 @@ use super::{ #[cfg(feature = "experimental-sliding-sync")] use crate::latest_event::LatestEvent; use crate::{ - deserialized_responses::{MemberEvent, RawSyncOrStrippedState}, + deserialized_responses::{DisplayName, MemberEvent, RawSyncOrStrippedState}, notification_settings::RoomNotificationMode, read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, @@ -819,8 +819,7 @@ impl Room { }) .collect::>(); - let display_names = - member_events.iter().map(|e| e.display_name().to_owned()).collect::>(); + let display_names = member_events.iter().map(|e| e.display_name()).collect::>(); let room_info = self.member_room_info(&display_names).await?; let mut members = Vec::new(); @@ -900,7 +899,7 @@ impl Room { let profile = self.store.get_profile(self.room_id(), user_id).await?; - let display_names = [event.display_name().to_owned()]; + let display_names = [event.display_name()]; let room_info = self.member_room_info(&display_names).await?; Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info))) @@ -911,7 +910,7 @@ impl Room { /// Async because it can read from storage. async fn member_room_info<'a>( &self, - display_names: &'a [String], + display_names: &'a [DisplayName], ) -> StoreResult> { let max_power_level = self.max_power_level(); let room_creator = self.inner.read().creator().map(ToOwned::to_owned); diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 9350492a098..19957e1aa08 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -695,6 +695,10 @@ async fn cache_latest_events( changes: Option<&StateChanges>, store: Option<&Store>, ) { + use crate::{ + deserialized_responses::DisplayName, store::ambiguity_map::is_display_name_ambiguous, + }; + let mut encrypted_events = Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); @@ -752,11 +756,13 @@ async fn cache_latest_events( .as_original() .and_then(|profile| profile.content.displayname.as_ref()) .and_then(|display_name| { + let display_name = DisplayName::new(display_name); + changes.ambiguity_maps.get(room.room_id()).and_then( |map_for_room| { - map_for_room - .get(display_name) - .map(|user_ids| user_ids.len() > 1) + map_for_room.get(&display_name).map(|users| { + is_display_name_ambiguous(&display_name, users) + }) }, ) }); diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index f02de1cc645..45a53a794bc 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, sync::Arc, }; @@ -24,18 +24,18 @@ use ruma::{ }, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; -use tracing::trace; +use tracing::{instrument, trace}; use super::{DynStateStore, Result, StateChanges}; use crate::{ - deserialized_responses::{AmbiguityChange, RawMemberEvent}, + deserialized_responses::{AmbiguityChange, DisplayName, RawMemberEvent}, store::StateStoreExt, }; /// A map of users that use a certain display name. #[derive(Debug, Clone)] struct DisplayNameUsers { - display_name: String, + display_name: DisplayName, users: BTreeSet, } @@ -70,7 +70,7 @@ impl DisplayNameUsers { /// Is the display name considered to be ambiguous. fn is_ambiguous(&self) -> bool { - self.user_count() > 1 + is_display_name_ambiguous(&self.display_name, &self.users) } } @@ -82,10 +82,19 @@ fn is_member_active(membership: &MembershipState) -> bool { #[derive(Debug)] pub(crate) struct AmbiguityCache { pub store: Arc, - pub cache: BTreeMap>>, + pub cache: BTreeMap>>, pub changes: BTreeMap>, } +#[instrument(ret)] +pub(crate) fn is_display_name_ambiguous( + display_name: &DisplayName, + users_with_display_name: &BTreeSet, +) -> bool { + trace!("Checking if a display name is ambiguous"); + display_name.is_inherently_ambiguous() || users_with_display_name.len() > 1 +} + impl AmbiguityCache { /// Create a new [`AmbiguityCache`] backed by the given state store. pub fn new(store: Arc) -> Self { @@ -224,18 +233,15 @@ impl AmbiguityCache { async fn get_users_with_display_name( &mut self, room_id: &RoomId, - display_name: &str, + display_name: &DisplayName, ) -> Result { Ok(if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(display_name) { - DisplayNameUsers { display_name: display_name.to_owned(), users: u.clone() } + DisplayNameUsers { display_name: display_name.clone(), users: u.clone() } } else { let users_with_display_name = self.store.get_users_with_display_name(room_id, display_name).await?; - DisplayNameUsers { - display_name: display_name.to_owned(), - users: users_with_display_name, - } + DisplayNameUsers { display_name: display_name.clone(), users: users_with_display_name } }) } @@ -254,7 +260,8 @@ impl AmbiguityCache { let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?; let old_map = if let Some(old_name) = old_display_name.as_deref() { - Some(self.get_users_with_display_name(room_id, old_name).await?) + let old_display_name = DisplayName::new(old_name); + Some(self.get_users_with_display_name(room_id, &old_display_name).await?) } else { None }; @@ -275,11 +282,221 @@ impl AmbiguityCache { new }; - Some(self.get_users_with_display_name(room_id, new_display_name).await?) + let new_display_name = DisplayName::new(new_display_name); + + Some(self.get_users_with_display_name(room_id, &new_display_name).await?) } else { None }; Ok((old_map, new_map)) } + + #[cfg(test)] + fn check(&self, room_id: &RoomId, display_name: &DisplayName) -> bool { + self.cache + .get(room_id) + .and_then(|display_names| { + display_names + .get(display_name) + .map(|user_ids| is_display_name_ambiguous(display_name, user_ids)) + }) + .unwrap_or_else(|| { + panic!( + "The display name {:?} should be part of the cache {:?}", + display_name, self.cache + ) + }) + } +} + +#[cfg(test)] +mod test { + use matrix_sdk_test::async_test; + use ruma::{room_id, server_name, user_id, EventId}; + use serde_json::json; + + use super::*; + use crate::store::{IntoStateStore, MemoryStore}; + + fn generate_event(user_id: &UserId, display_name: &str) -> SyncRoomMemberEvent { + let server_name = server_name!("localhost"); + serde_json::from_value(json!({ + "content": { + "displayname": display_name, + "membership": "join" + }, + "event_id": EventId::new(server_name), + "origin_server_ts": 152037280, + "sender": user_id, + "state_key": user_id, + "type": "m.room.member", + + })) + .expect("We should be able to deserialize the static member event") + } + + macro_rules! assert_ambiguity { + ( + [ $( ($user:literal, $display_name:literal) ),* ], + [ $( ($check_display_name:literal, $ambiguous:expr) ),* ] $(,)? + ) => { + assert_ambiguity!( + [ $( ($user, $display_name) ),* ], + [ $( ($check_display_name, $ambiguous) ),* ], + "The test failed the ambiguity assertions" + ) + }; + + ( + [ $( ($user:literal, $display_name:literal) ),* ], + [ $( ($check_display_name:literal, $ambiguous:expr) ),* ], + $description:literal $(,)? + ) => { + let store = MemoryStore::new(); + let mut ambiguity_cache = AmbiguityCache::new(store.into_state_store()); + + let changes = Default::default(); + let room_id = room_id!("!foo:bar"); + + macro_rules! add_display_name { + ($u:literal, $n:literal) => { + let event = generate_event(user_id!($u), $n); + + ambiguity_cache + .handle_event(&changes, room_id, &event) + .await + .expect("We should be able to handle a member event to calculate the ambiguity."); + }; + } + + macro_rules! assert_display_name_ambiguity { + ($n:literal, $a:expr) => { + let display_name = DisplayName::new($n); + + if ambiguity_cache.check(room_id, &display_name) != $a { + let foo = if $a { "be" } else { "not be" }; + panic!("{}: the display name {} should {} ambiguous", $description, $n, foo); + } + }; + } + + $( + add_display_name!($user, $display_name); + )* + + $( + assert_display_name_ambiguity!($check_display_name, $ambiguous); + )* + }; + } + + #[async_test] + async fn test_disambiguation() { + assert_ambiguity!( + [("@alice:localhost", "alice")], + [("alice", false)], + "Alice is alone in the room" + ); + + assert_ambiguity!( + [("@alice:localhost", "alice")], + [("Alice", false)], + "Alice is alone in the room and has a capitalized display name" + ); + + assert_ambiguity!( + [("@alice:localhost", "alice"), ("@bob:localhost", "alice")], + [("alice", true)], + "Alice and bob share a display name" + ); + + assert_ambiguity!( + [ + ("@alice:localhost", "alice"), + ("@bob:localhost", "alice"), + ("@carol:localhost", "carol") + ], + [("alice", true), ("carol", false)], + "Alice and Bob share a display name, while Carol is unique" + ); + + assert_ambiguity!( + [("@alice:localhost", "alice"), ("@bob:localhost", "ALICE")], + [("alice", true)], + "Alice and Bob share a display name that is differently capitalized" + ); + + assert_ambiguity!( + [("@alice:localhost", "alice"), ("@bob:localhost", "аlice")], + [("alice", true)], + "Bob tries to impersonate Alice using a cyrilic а" + ); + + assert_ambiguity!( + [("@alice:localhost", "@bob:localhost"), ("@bob:localhost", "аlice")], + [("@bob:localhost", true)], + "Alice tries to impersonate bob using an mxid" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using scripture symbols" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using fraktur symbols" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using circled symbols" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using squared symbols" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahasrahla")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using big unicode letters" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "\u{202e}alharsahas")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using left to right shenanigans" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sa̴hasrahla")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using a diacritical mark" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200B}rahla")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using a zero-width space" + ); + + assert_ambiguity!( + [("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200D}rahla")], + [("Sahasrahla", true)], + "Bob tries to impersonate Alice using a zero-width space" + ); + + assert_ambiguity!( + [("@alice:localhost", "ff"), ("@bob:localhost", "\u{FB00}")], + [("ff", true)], + "Bob tries to impersonate Alice using a ligature" + ); + } } diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 28c4e90cf4b..3fb6ac0bc9d 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1,6 +1,6 @@ //! Trait and macro of integration tests for StateStore implementations. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use assert_matches::assert_matches; use assert_matches2::assert_let; @@ -34,7 +34,8 @@ use ruma::{ use serde_json::{json, value::Value as JsonValue}; use super::{ - send_queue::SentRequestKey, DependentQueuedRequestKind, DynStateStore, ServerCapabilities, + send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore, + ServerCapabilities, }; use crate::{ deserialized_responses::MemberEvent, @@ -141,13 +142,15 @@ impl StateStoreIntegrationTests for DynStateStore { room.handle_state_event(&topic_event); changes.add_state_event(room_id, topic_event, topic_raw); - let mut room_ambiguity_map = BTreeMap::new(); + let mut room_ambiguity_map = HashMap::new(); let mut room_profiles = BTreeMap::new(); let member_json: &JsonValue = &test_json::MEMBER; let member_event: SyncRoomMemberEvent = serde_json::from_value(member_json.clone()).unwrap(); - let displayname = member_event.as_original().unwrap().content.displayname.clone().unwrap(); + let displayname = DisplayName::new( + member_event.as_original().unwrap().content.displayname.as_ref().unwrap(), + ); room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()])); room_profiles.insert(user_id.to_owned(), (&member_event).into()); @@ -256,6 +259,8 @@ impl StateStoreIntegrationTests for DynStateStore { async fn test_populate_store(&self) -> Result<()> { let room_id = room_id(); let user_id = user_id(); + let display_name = DisplayName::new("example"); + self.populate().await?; assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some()); @@ -290,7 +295,7 @@ impl StateStoreIntegrationTests for DynStateStore { "Expected to find 1 joined user ids" ); assert_eq!( - self.get_users_with_display_name(room_id, "example").await?.len(), + self.get_users_with_display_name(room_id, &display_name).await?.len(), 2, "Expected to find 2 display names for room" ); @@ -962,6 +967,7 @@ impl StateStoreIntegrationTests for DynStateStore { async fn test_room_removal(&self) -> Result<()> { let room_id = room_id(); let user_id = user_id(); + let display_name = DisplayName::new("example"); let stripped_room_id = stripped_room_id(); self.populate().await?; @@ -990,7 +996,7 @@ impl StateStoreIntegrationTests for DynStateStore { "still joined users found" ); assert!( - self.get_users_with_display_name(room_id, "example").await?.is_empty(), + self.get_users_with_display_name(room_id, &display_name).await?.is_empty(), "still display names found" ); assert!(self @@ -1145,15 +1151,15 @@ impl StateStoreIntegrationTests for DynStateStore { async fn test_display_names_saving(&self) { let room_id = room_id!("!test_display_names_saving:localhost"); let user_id = user_id(); - let user_display_name = "User"; + let user_display_name = DisplayName::new("User"); let second_user_id = user_id!("@second:localhost"); let third_user_id = user_id!("@third:localhost"); - let other_display_name = "Raoul"; - let unknown_display_name = "Unknown"; + let other_display_name = DisplayName::new("Raoul"); + let unknown_display_name = DisplayName::new("Unknown"); // No event in store. let mut display_names = vec![user_display_name.to_owned()]; - let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap(); + let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap(); assert!(users.is_empty()); let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap(); assert!(names.is_empty()); @@ -1167,7 +1173,7 @@ impl StateStoreIntegrationTests for DynStateStore { .insert(user_display_name.to_owned(), [user_id.to_owned()].into()); self.save_changes(&changes).await.unwrap(); - let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap(); + let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap(); assert_eq!(users.len(), 1); let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap(); assert_eq!(names.len(), 1); @@ -1182,9 +1188,9 @@ impl StateStoreIntegrationTests for DynStateStore { self.save_changes(&changes).await.unwrap(); display_names.push(other_display_name.to_owned()); - let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap(); + let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap(); assert_eq!(users.len(), 1); - let users = self.get_users_with_display_name(room_id, other_display_name).await.unwrap(); + let users = self.get_users_with_display_name(room_id, &other_display_name).await.unwrap(); assert_eq!(users.len(), 2); let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap(); assert_eq!(names.len(), 2); diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 60701a3e5f6..ac2951be983 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -42,7 +42,8 @@ use super::{ StateChanges, StateStore, StoreError, }; use crate::{ - deserialized_responses::RawAnySyncOrStrippedState, store::QueueWedgeError, + deserialized_responses::{DisplayName, RawAnySyncOrStrippedState}, + store::QueueWedgeError, MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue, }; @@ -61,7 +62,7 @@ pub struct MemoryStore { utd_hook_manager_data: StdRwLock>, account_data: StdRwLock>>, profiles: StdRwLock>>, - display_names: StdRwLock>>>, + display_names: StdRwLock>>>, members: StdRwLock>>, room_info: StdRwLock>, room_state: StdRwLock< @@ -701,7 +702,7 @@ impl StateStore for MemoryStore { async fn get_users_with_display_name( &self, room_id: &RoomId, - display_name: &str, + display_name: &DisplayName, ) -> Result> { Ok(self .display_names @@ -715,21 +716,18 @@ impl StateStore for MemoryStore { async fn get_users_with_display_names<'a>( &self, room_id: &RoomId, - display_names: &'a [String], - ) -> Result>> { + display_names: &'a [DisplayName], + ) -> Result>> { if display_names.is_empty() { - return Ok(BTreeMap::new()); + return Ok(HashMap::new()); } let read_guard = &self.display_names.read().unwrap(); let Some(room_names) = read_guard.get(room_id) else { - return Ok(BTreeMap::new()); + return Ok(HashMap::new()); }; - Ok(display_names - .iter() - .filter_map(|n| room_names.get(n).map(|d| (n.as_str(), d.clone()))) - .collect()) + Ok(display_names.iter().filter_map(|n| room_names.get(n).map(|d| (n, d.clone()))).collect()) } async fn get_account_data_event( diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index c33e3259b23..fd31b917734 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -21,7 +21,7 @@ //! store. use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt, ops::Deref, result::Result as StdResult, @@ -58,6 +58,7 @@ use tokio::sync::{broadcast, Mutex, RwLock}; use tracing::warn; use crate::{ + deserialized_responses::DisplayName, event_cache::store as event_cache_store, rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState}, MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta, @@ -384,7 +385,7 @@ pub struct StateChanges { /// A map from room id to a map of a display name and a set of user ids that /// share that display name in the given room. - pub ambiguity_maps: BTreeMap>>, + pub ambiguity_maps: BTreeMap>>, } impl StateChanges { diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index f60189dd3d3..04dad0e24bd 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -14,7 +14,7 @@ use std::{ borrow::Borrow, - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt, sync::Arc, }; @@ -46,7 +46,9 @@ use super::{ StoreError, }; use crate::{ - deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState}, + deserialized_responses::{ + DisplayName, RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState, + }, MinimalRoomMemberEvent, RoomInfo, RoomMemberships, }; @@ -206,7 +208,7 @@ pub trait StateStore: AsyncTraitDeps { async fn get_users_with_display_name( &self, room_id: &RoomId, - display_name: &str, + display_name: &DisplayName, ) -> Result, Self::Error>; /// Get all the users that use the given display names in the given room. @@ -219,8 +221,8 @@ pub trait StateStore: AsyncTraitDeps { async fn get_users_with_display_names<'a>( &self, room_id: &RoomId, - display_names: &'a [String], - ) -> Result>, Self::Error>; + display_names: &'a [DisplayName], + ) -> Result>, Self::Error>; /// Get an event out of the account data store. /// @@ -567,7 +569,7 @@ impl StateStore for EraseStateStoreError { async fn get_users_with_display_name( &self, room_id: &RoomId, - display_name: &str, + display_name: &DisplayName, ) -> Result, Self::Error> { self.0.get_users_with_display_name(room_id, display_name).await.map_err(Into::into) } @@ -575,8 +577,8 @@ impl StateStore for EraseStateStoreError { async fn get_users_with_display_names<'a>( &self, room_id: &RoomId, - display_names: &'a [String], - ) -> Result>, Self::Error> { + display_names: &'a [DisplayName], + ) -> Result>, Self::Error> { self.0.get_users_with_display_names(room_id, display_names).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 372a179c9f9..6e6193e4314 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::{ - collections::{BTreeMap, BTreeSet, HashSet}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::Arc, }; @@ -23,7 +23,7 @@ use gloo_utils::format::JsValueSerdeExt; use growable_bloom_filter::GrowableBloom; use indexed_db_futures::prelude::*; use matrix_sdk_base::{ - deserialized_responses::RawAnySyncOrStrippedState, + deserialized_responses::{DisplayName, RawAnySyncOrStrippedState}, store::{ ChildTransactionId, ComposerDraft, DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent, @@ -665,7 +665,15 @@ impl_state_store!({ let store = tx.object_store(keys::DISPLAY_NAMES)?; for (room_id, ambiguity_maps) in &changes.ambiguity_maps { for (display_name, map) in ambiguity_maps { - let key = self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)); + let key = self.encode_key( + keys::DISPLAY_NAMES, + ( + room_id, + display_name + .as_normalized_str() + .unwrap_or_else(|| display_name.as_raw_str()), + ), + ); store.put_key_val(&key, &self.serialize_value(&map)?)?; } @@ -1122,12 +1130,18 @@ impl_state_store!({ async fn get_users_with_display_name( &self, room_id: &RoomId, - display_name: &str, + display_name: &DisplayName, ) -> Result> { self.inner .transaction_on_one_with_mode(keys::DISPLAY_NAMES, IdbTransactionMode::Readonly)? .object_store(keys::DISPLAY_NAMES)? - .get(&self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)))? + .get(&self.encode_key( + keys::DISPLAY_NAMES, + ( + room_id, + display_name.as_normalized_str().unwrap_or_else(|| display_name.as_raw_str()), + ), + ))? .await? .map(|f| self.deserialize_value::>(&f)) .unwrap_or_else(|| Ok(Default::default())) @@ -1136,10 +1150,12 @@ impl_state_store!({ async fn get_users_with_display_names<'a>( &self, room_id: &RoomId, - display_names: &'a [String], - ) -> Result>> { + display_names: &'a [DisplayName], + ) -> Result>> { + let mut map = HashMap::new(); + if display_names.is_empty() { - return Ok(BTreeMap::new()); + return Ok(map); } let txn = self @@ -1147,15 +1163,24 @@ impl_state_store!({ .transaction_on_one_with_mode(keys::DISPLAY_NAMES, IdbTransactionMode::Readonly)?; let store = txn.object_store(keys::DISPLAY_NAMES)?; - let mut map = BTreeMap::new(); for display_name in display_names { if let Some(user_ids) = store - .get(&self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)))? + .get( + &self.encode_key( + keys::DISPLAY_NAMES, + ( + room_id, + display_name + .as_normalized_str() + .unwrap_or_else(|| display_name.as_raw_str()), + ), + ), + )? .await? .map(|f| self.deserialize_value::>(&f)) .transpose()? { - map.insert(display_name.as_ref(), user_ids); + map.insert(display_name, user_ids); } } diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index ab152289059..02aeb0f545a 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -1,6 +1,6 @@ use std::{ borrow::Cow, - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt, iter, path::Path, sync::Arc, @@ -9,7 +9,7 @@ use std::{ use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ - deserialized_responses::{RawAnySyncOrStrippedState, SyncOrStrippedState}, + deserialized_responses::{DisplayName, RawAnySyncOrStrippedState, SyncOrStrippedState}, store::{ migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, @@ -1305,13 +1305,34 @@ impl StateStore for SqliteStateStore { let room_id = this.encode_key(keys::DISPLAY_NAME, room_id); for (name, user_ids) in display_names { - let name = this.encode_key(keys::DISPLAY_NAME, name); + let encoded_name = this.encode_key( + keys::DISPLAY_NAME, + name.as_normalized_str().unwrap_or_else(|| name.as_raw_str()), + ); let data = this.serialize_json(&user_ids)?; if user_ids.is_empty() { - txn.remove_display_name(&room_id, &name)?; + txn.remove_display_name(&room_id, &encoded_name)?; + + // We can't do a migration to merge the previously distinct buckets of + // user IDs since the display names themselves are hashed before they + // are persisted in the store. So the store will always retain two + // buckets: one for raw display names and one for normalised ones. + // + // We therefore do the next best thing, which is a sort of a soft + // migration: we fetch both the raw and normalised buckets, then merge + // the user IDs contained in them into a separate, temporary merged + // bucket. The SDK then operates on the merged buckets exclusively. See + // the comment in `get_users_with_display_names` for details. + // + // If the merged bucket is empty, that must mean that both the raw and + // normalised buckets were also empty, so we can remove both from the + // store. + let raw_name = this.encode_key(keys::DISPLAY_NAME, name.as_raw_str()); + txn.remove_display_name(&room_id, &raw_name)?; } else { - txn.set_display_name(&room_id, &name, &data)?; + // We only create new buckets with the normalized display name. + txn.set_display_name(&room_id, &encoded_name, &data)?; } } } @@ -1500,10 +1521,13 @@ impl StateStore for SqliteStateStore { async fn get_users_with_display_name( &self, room_id: &RoomId, - display_name: &str, + display_name: &DisplayName, ) -> Result> { let room_id = self.encode_key(keys::DISPLAY_NAME, room_id); - let names = vec![self.encode_key(keys::DISPLAY_NAME, display_name)]; + let names = vec![self.encode_key( + keys::DISPLAY_NAME, + display_name.as_normalized_str().unwrap_or_else(|| display_name.as_raw_str()), + )]; Ok(self .acquire() @@ -1520,33 +1544,49 @@ impl StateStore for SqliteStateStore { async fn get_users_with_display_names<'a>( &self, room_id: &RoomId, - display_names: &'a [String], - ) -> Result>> { + display_names: &'a [DisplayName], + ) -> Result>> { + let mut result = HashMap::new(); + if display_names.is_empty() { - return Ok(BTreeMap::new()); + return Ok(result); } let room_id = self.encode_key(keys::DISPLAY_NAME, room_id); let mut names_map = display_names .iter() - .map(|n| (self.encode_key(keys::DISPLAY_NAME, n), n.as_ref())) + .flat_map(|display_name| { + // We encode the display name as the `raw_str()` and the normalized string. + // + // This is for compatibility reasons since: + // 1. Previously "Alice" and "alice" were considered to be distinct display + // names, while we now consider them to be the same so we need to merge the + // previously distinct buckets of user IDs. + // 2. We can't do a migration to merge the previously distinct buckets of user + // IDs since the display names itself are hashed before they are persisted + // in the store. + let raw = + (self.encode_key(keys::DISPLAY_NAME, display_name.as_raw_str()), display_name); + let normalized = display_name.as_normalized_str().map(|normalized| { + (self.encode_key(keys::DISPLAY_NAME, normalized), display_name) + }); + + iter::once(raw).chain(normalized.into_iter()) + }) .collect::>(); let names = names_map.keys().cloned().collect(); - self.acquire() - .await? - .get_display_names(room_id, names) - .await? - .into_iter() - .map(|(name, data)| { - Ok(( - names_map - .remove(name.as_slice()) - .expect("returned display names were requested"), - self.deserialize_json(&data)?, - )) - }) - .collect::>>() + for (name, data) in + self.acquire().await?.get_display_names(room_id, names).await?.into_iter() + { + let display_name = + names_map.remove(name.as_slice()).expect("returned display names were requested"); + let user_ids: BTreeSet<_> = self.deserialize_json(&data)?; + + result.entry(display_name).or_insert_with(BTreeSet::new).extend(user_ids); + } + + Ok(result) } async fn get_account_data_event( From 0b16d488adc6122e7bacb6e11f5e39641fecc774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2024 14:11:19 +0100 Subject: [PATCH 550/979] chore: Release matrix-sdk version 0.8.0 (#4291) Co-authored-by: Ivan Enderlin --- Cargo.lock | 18 +- Cargo.toml | 18 +- crates/matrix-sdk-base/CHANGELOG.md | 78 ++++++- crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-common/CHANGELOG.md | 7 + crates/matrix-sdk-common/Cargo.toml | 2 +- crates/matrix-sdk-crypto/CHANGELOG.md | 206 ++++++++--------- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-indexeddb/CHANGELOG.md | 7 +- crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- crates/matrix-sdk-qrcode/CHANGELOG.md | 3 + crates/matrix-sdk-qrcode/Cargo.toml | 2 +- crates/matrix-sdk-sqlite/CHANGELOG.md | 12 + crates/matrix-sdk-sqlite/Cargo.toml | 2 +- .../matrix-sdk-store-encryption/CHANGELOG.md | 3 + crates/matrix-sdk-store-encryption/Cargo.toml | 2 +- crates/matrix-sdk-ui/CHANGELOG.md | 33 ++- crates/matrix-sdk-ui/Cargo.toml | 2 +- crates/matrix-sdk/CHANGELOG.md | 208 ++++++++++++++---- crates/matrix-sdk/Cargo.toml | 2 +- 20 files changed, 417 insertions(+), 194 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d23120497d..981239a578e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2882,7 +2882,7 @@ dependencies = [ [[package]] name = "matrix-sdk" -version = "0.7.1" +version = "0.8.0" dependencies = [ "anyhow", "anymap2", @@ -2958,7 +2958,7 @@ dependencies = [ [[package]] name = "matrix-sdk-base" -version = "0.7.0" +version = "0.8.0" dependencies = [ "as_variant", "assert_matches", @@ -2994,7 +2994,7 @@ dependencies = [ [[package]] name = "matrix-sdk-common" -version = "0.7.0" +version = "0.8.0" dependencies = [ "assert_matches", "async-trait", @@ -3022,7 +3022,7 @@ dependencies = [ [[package]] name = "matrix-sdk-crypto" -version = "0.7.2" +version = "0.8.0" dependencies = [ "aes", "anyhow", @@ -3145,7 +3145,7 @@ dependencies = [ [[package]] name = "matrix-sdk-indexeddb" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "assert_matches", @@ -3213,7 +3213,7 @@ dependencies = [ [[package]] name = "matrix-sdk-qrcode" -version = "0.7.1" +version = "0.8.0" dependencies = [ "byteorder", "image", @@ -3225,7 +3225,7 @@ dependencies = [ [[package]] name = "matrix-sdk-sqlite" -version = "0.7.1" +version = "0.8.0" dependencies = [ "assert_matches", "async-trait", @@ -3252,7 +3252,7 @@ dependencies = [ [[package]] name = "matrix-sdk-store-encryption" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -3298,7 +3298,7 @@ dependencies = [ [[package]] name = "matrix-sdk-ui" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "as_variant", diff --git a/Cargo.toml b/Cargo.toml index 1a06ef22aaa..4f076807b33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,17 +79,17 @@ vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] } wiremock = "0.6.0" zeroize = "1.6.0" -matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false } -matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" } -matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" } -matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" } +matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false } +matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" } +matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" } +matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.0" } matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" } -matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false } -matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" } -matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false } -matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" } +matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.8.0", default-features = false } +matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" } +matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false } +matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" } matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" } -matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false } +matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false } # Default release profile, select with `--release` [profile.release] diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index aebdc82fe75..a1a8fc2753d 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -2,24 +2,80 @@ All notable changes to this project will be documented in this file. -# unreleased +## [0.8.0] - 2024-11-19 +### Bug Fixes + +- Add more invalid characters for room aliases. + +- Use the `DisplayName` struct to protect against homoglyph attacks. + + +### Features - Add `BaseClient::room_key_recipient_strategy` field -- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one -- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom` -- `AmbiguityCache` contains the room member's user ID + +- `AmbiguityCache` contains the room member's user ID. + +- [**breaking**] `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to + request an animated thumbnail They both take a `MediaThumbnailSettings` + instead of `MediaThumbnailSize`. + +- Consider knocked members to be part of the room for display name + disambiguation. + +- `Client::cross_process_store_locks_holder_name` is used everywhere: + - `StoreConfig::new()` now takes a + `cross_process_store_locks_holder_name` argument. + - `StoreConfig` no longer implements `Default`. + - `BaseClient::new()` has been removed. + - `BaseClient::clone_with_in_memory_state_store()` now takes a + `cross_process_store_locks_holder_name` argument. + - `BaseClient` no longer implements `Default`. + - `EventCacheStoreLock::new()` no longer takes a `key` argument. + - `BuilderStoreConfig` no longer has + `cross_process_store_locks_holder_name` field for `Sqlite` and + `IndexedDb`. + +- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`. + +- Allow aborting media uploads. + +- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` + by a custom one. + +- Introduce a `DisplayName` struct which normalizes and sanitizes +display names. + + +### Refactor + +- [**breaking**] Rename `DisplayName` to `RoomDisplayName`. + +- Rename `AmbiguityMap` to `DisplayNameUsers`. + +- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`. + +- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`. + +- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`. + +- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`. + - `Store::get_rooms` and `Store::get_rooms_filtered` are way faster because they don't acquire the lock for every room they read. + - `Store::get_rooms`, `Store::get_rooms_filtered` and `Store::get_room` are renamed `Store::rooms`, `Store::rooms_filtered` and `Store::room`. -- `Client::get_rooms` and `Client::get_rooms_filtered` are renamed + +- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed `Client::rooms` and `Client::rooms_filtered`. -- `Client::get_stripped_rooms` has finally been removed. -- `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to request an animated thumbnail - - They both take a `MediaThumbnailSettings` instead of `MediaThumbnailSize`. -- The `StateStore` methods to access data in the media cache where moved to a separate - `EventCacheStore` trait. -- The `instant` module was removed, use the `ruma::time` module instead. + +- [**breaking**] `Client::get_stripped_rooms` has finally been removed. + +- [**breaking**] The `StateStore` methods to access data in the media cache + where moved to a separate `EventCacheStore` trait. + +- [**breaking**] The `instant` module was removed, use the `ruma::time` module instead. # 0.7.0 diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index d54aa38ff6d..65a3246d62e 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk-base" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.7.0" +version = "0.8.0" [package.metadata.docs.rs] all-features = true diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md index ba8118892a2..3fec5ae4492 100644 --- a/crates/matrix-sdk-common/CHANGELOG.md +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -2,3 +2,10 @@ All notable changes to this project will be documented in this file. +## [0.8.0] - 2024-11-19 + +### Refactor + +- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`. + + diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index e5e0995c083..c55b7a39583 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk-common" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.7.0" +version = "0.8.0" [package.metadata.docs.rs] default-target = "x86_64-unknown-linux-gnu" diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 51dc8caf79f..9809ea75244 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -1,6 +1,70 @@ -# UNRELEASED +# Changelog -Changes: +All notable changes to this project will be documented in this file. + +## [0.8.0] - 2024-11-19 + +### Features + +- Pin identity when we withdraw verification. + +- Expose new method `OlmMachine::room_keys_withheld_received_stream`, to allow + applications to receive notifications about received `m.room_key.withheld` + events. + ([#3660](https://github.com/matrix-org/matrix-rust-sdk/pull/3660)), + ([#3674](https://github.com/matrix-org/matrix-rust-sdk/pull/3674)) + +- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings. + ([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462)) + +- Expose new method `OlmMachine::upload_device_keys()`. + ([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457)) + +- Expose new method `CryptoStore::import_room_keys`. + ([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448)) + +- Expose new method `BackupMachine::backup_version`. + ([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320)) + +- Add data types to parse the QR code data for the QR code login defined in. + [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108) + +- Expose new method `CryptoStore::clear_caches`. + ([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338)) + +- Expose new method `OlmMachine::device_creation_time`. + ([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275)) + +- Log more details about the Olm session after encryption and decryption. + ([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242)) + +- When Olm message decryption fails, report the error code(s) from the failure. + ([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212)) + +- Expose new methods `OlmMachine::set_room_settings` and + `OlmMachine::get_room_settings`. + ([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042)) + +- Add new properties `session_rotation_period` and + `session_rotation_period_msgs` to `store::RoomSettings`. + ([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042)) + +- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage + keys whose metadata lacked check fields. + ([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046)) + +- Add new API `Device::encrypt_event_raw` that allows + to encrypt an event to a specific device. + ([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091)) + +- Add new API `store::Store::export_room_keys_stream` that provides room + keys on demand. + +- Include event timestamps on logs from event decryption. + ([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194)) + + +### Refactor - Add new method `OlmMachine::try_decrypt_room_event`. ([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116)) @@ -8,14 +72,14 @@ Changes: - Add reason code to `matrix_sdk_common::deserialized_responses::UnableToDecryptInfo`. ([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116)) -- The `UserIdentity` struct has been renamed to `OtherUserIdentity` +- [**breaking**] The `UserIdentity` struct has been renamed to `OtherUserIdentity`. ([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036])) -- The `UserIdentities` enum has been renamed to `UserIdentity` +- [**breaking**] The `UserIdentities` enum has been renamed to `UserIdentity`. ([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036])) -- Change the withheld code for keys not shared due to the `IdentityBasedStrategy`, from `m.unauthorised` - to `m.unverified`. +- Change the withheld code for keys not shared due to the + `IdentityBasedStrategy`, from `m.unauthorised` to `m.unverified`. ([#3985](https://github.com/matrix-org/matrix-rust-sdk/pull/3985)) - Improve logging for undecryptable Megolm events. @@ -59,47 +123,47 @@ Changes: Breaking changes: -- `VerificationRequestState::Transitioned` now includes a new field +- [**breaking**] `VerificationRequestState::Transitioned` now includes a new field `other_device_data` of type `DeviceData`. ([#4153](https://github.com/matrix-org/matrix-rust-sdk/pull/4153)) -- `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type, +- [**breaking**] `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type, instead of the more generic `TimelineEvent` type. -- **NOTE**: this version causes changes to the format of the serialised data in +- [**breaking**] **NOTE**: this version causes changes to the format of the serialised data in the CryptoStore, meaning that, once upgraded, it will not be possible to roll back applications to earlier versions without breaking user sessions. -- Renamed `VerificationLevel::PreviouslyVerified` to +- [**breaking**] Renamed `VerificationLevel::PreviouslyVerified` to `VerificationLevel::VerificationViolation`. -- `OlmMachine::decrypt_room_event` now takes a `DecryptionSettings` argument, - which includes a `TrustRequirement` indicating the required trust level for - the sending device. When it is called with `TrustRequirement` other than - `TrustRequirement::Unverified`, it may return the new - `MegolmError::SenderIdentityNotTrusted` variant if the sending device does not - satisfy the required trust level. +- [**breaking**] `OlmMachine::decrypt_room_event` now takes a + `DecryptionSettings` argument, which includes a `TrustRequirement` indicating + the required trust level for the sending device. When it is called with + `TrustRequirement` other than `TrustRequirement::Unverified`, it may return + the new `MegolmError::SenderIdentityNotTrusted` variant if the sending device + does not satisfy the required trust level. ([#3899](https://github.com/matrix-org/matrix-rust-sdk/pull/3899)) -- Change the structure of the `SenderData` enum to separate variants for - previously-verified, unverified and verified. +- [**breaking**] Change the structure of the `SenderData` enum to separate + variants for previously-verified, unverified and verified. ([#3877](https://github.com/matrix-org/matrix-rust-sdk/pull/3877)) -- Where `EncryptionInfo` is returned it may include the new `PreviouslyVerified` - variant of `VerificationLevel` to indicate that the user was previously - verified and is no longer verified. +- [**breaking**] Where `EncryptionInfo` is returned it may include the new + `PreviouslyVerified` variant of `VerificationLevel` to indicate that the user + was previously verified and is no longer verified. ([#3877](https://github.com/matrix-org/matrix-rust-sdk/pull/3877)) -- Expose new methods `OwnUserIdentity::was_previously_verified`, +- [**breaking**] Expose new methods `OwnUserIdentity::was_previously_verified`, `OwnUserIdentity::withdraw_verification`, and `OwnUserIdentity::has_verification_violation`, which track whether our own identity was previously verified. ([#3846](https://github.com/matrix-org/matrix-rust-sdk/pull/3846)) -- Add a new `error_on_verified_user_problem` property to +- [**breaking**] Add a new `error_on_verified_user_problem` property to `CollectStrategy::DeviceBasedStrategy`, which, when set, causes `OlmMachine::share_room_key` to fail with an error if any verified users on - the recipient list have unsigned devices, or are no lonver verified. + the recipient list have unsigned devices, or are no longer verified. When `CallectStrategy::IdentityBasedStrategy` is used, `OlmMachine::share_room_key` will fail with an error if any verified users on @@ -109,103 +173,43 @@ Breaking changes: Also remove `CollectStrategy::new_device_based`: callers should construct a `CollectStrategy::DeviceBasedStrategy` directly. - `EncryptionSettings::new` now takes a `CollectStrategy` argument, instead of - a list of booleans. + `EncryptionSettings::new` now takes a `CollectStrategy` argument, instead of a + list of booleans. ([#3810](https://github.com/matrix-org/matrix-rust-sdk/pull/3810)) ([#3816](https://github.com/matrix-org/matrix-rust-sdk/pull/3816)) ([#3896](https://github.com/matrix-org/matrix-rust-sdk/pull/3896)) -- Remove the method `OlmMachine::clear_crypto_cache()`, crypto stores are not - supposed to have any caches anymore. +- [**breaking**] Remove the method `OlmMachine::clear_crypto_cache()`, crypto + stores are not supposed to have any caches anymore. -- Add a `custom_account` argument to the `OlmMachine::with_store()` method, this - allows users to learn their identity keys before they get access to the user - and device ID. +- [**breaking**] Add a `custom_account` argument to the + `OlmMachine::with_store()` method, this allows users to learn their identity + keys before they get access to the user and device ID. ([#3451](https://github.com/matrix-org/matrix-rust-sdk/pull/3451)) -- Add a `backup_version` argument to `CryptoStore`'s +- [**breaking**] Add a `backup_version` argument to `CryptoStore`'s `inbound_group_sessions_for_backup`, - `mark_inbound_group_sessions_as_backed_up` and - `inbound_group_session_counts` methods. - ([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253)) + `mark_inbound_group_sessions_as_backed_up` and `inbound_group_session_counts` + methods. ([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253)) -- Rename the `OlmMachine::invalidate_group_session` method to - `OlmMachine::discard_room_key` +- [**breaking**] Rename the `OlmMachine::invalidate_group_session` method to + `OlmMachine::discard_room_key`. -- Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`. +- [**breaking**] Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`. (Call it with `olm_machine.store().export_room_keys(...)`.) -- Add new `dehydrated` property to `olm::account::PickledAccount`. +- [**breaking**] Add new `dehydrated` property to `olm::account::PickledAccount`. ([#3164](https://github.com/matrix-org/matrix-rust-sdk/pull/3164)) -- Remove deprecated `OlmMachine::import_room_keys`. +- [**breaking**] Remove deprecated `OlmMachine::import_room_keys`. ([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448)) -- Add the `SasState::Created` variant to differentiate the state between the +- [**breaking**] Add the `SasState::Created` variant to differentiate the state between the party that sent the verification start and the party that received it. -Deprecations: - -- Deprecate `BackupMachine::import_backed_up_room_keys`. - ([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448)) - -Additions: - -- Expose new method `OlmMachine::room_keys_withheld_received_stream`, to allow - applications to receive notifications about received `m.room_key.withheld` - events. - ([#3660](https://github.com/matrix-org/matrix-rust-sdk/pull/3660)), - ([#3674](https://github.com/matrix-org/matrix-rust-sdk/pull/3674)) - -- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings - ([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462)) - -- Expose new method `OlmMachine::upload_device_keys()`. - ([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457)) - -- Expose new method `CryptoStore::import_room_keys`. +- [**breaking**] Deprecate `BackupMachine::import_backed_up_room_keys`. ([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448)) -- Expose new method `BackupMachine::backup_version`. - ([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320)) - -- Add data types to parse the QR code data for the QR code login defined in - [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108) - -- Expose new method `CryptoStore::clear_caches`. - ([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338)) - -- Expose new method `OlmMachine::device_creation_time`. - ([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275)) - -- Log more details about the Olm session after encryption and decryption. - ([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242)) - -- When Olm message decryption fails, report the error code(s) from the failure. - ([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212)) - -- Expose new methods `OlmMachine::set_room_settings` and - `OlmMachine::get_room_settings`. - ([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042)) - -- Add new properties `session_rotation_period` and - `session_rotation_period_msgs` to `store::RoomSettings`. - ([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042)) - -- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage - keys whose metadata lacked check fields. - ([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046)) - -- Add new API `Device::encrypt_event_raw` that allows - to encrypt an event to a specific device. - ([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091)) - -- Add new API `store::Store::export_room_keys_stream` that provides room - keys on demand. - -- Include event timestamps on logs from event decryption. - ([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194)) - # 0.7.2 diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 76714b39542..12830bb60ca 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk-crypto" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.7.2" +version = "0.8.0" [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/matrix-sdk-indexeddb/CHANGELOG.md b/crates/matrix-sdk-indexeddb/CHANGELOG.md index 993b6152e47..f66e695a48c 100644 --- a/crates/matrix-sdk-indexeddb/CHANGELOG.md +++ b/crates/matrix-sdk-indexeddb/CHANGELOG.md @@ -2,7 +2,9 @@ All notable changes to this project will be documented in this file. -# UNRELEASED +## [0.8.0] - 2024-11-19 + +### Features - Improve the efficiency of objects stored in the crypto store. ([#3645](https://github.com/matrix-org/matrix-rust-sdk/pull/3645), [#3651](https://github.com/matrix-org/matrix-rust-sdk/pull/3651)) @@ -11,3 +13,6 @@ All notable changes to this project will be documented in this file. - `save_change` performance improvement, all encryption and serialization is done now outside of the db transaction. +### Bug Fixes + +- Use the `DisplayName` struct to protect against homoglyph attacks. diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 4a3a547f804..35610b05f10 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "matrix-sdk-indexeddb" -version = "0.7.0" +version = "0.8.0" repository = "https://github.com/matrix-org/matrix-rust-sdk" description = "Web's IndexedDB Storage backend for matrix-sdk" license = "Apache-2.0" diff --git a/crates/matrix-sdk-qrcode/CHANGELOG.md b/crates/matrix-sdk-qrcode/CHANGELOG.md index ba8118892a2..dff6a57ebbf 100644 --- a/crates/matrix-sdk-qrcode/CHANGELOG.md +++ b/crates/matrix-sdk-qrcode/CHANGELOG.md @@ -2,3 +2,6 @@ All notable changes to this project will be documented in this file. +## [0.8.0] - 2024-11-19 + +No notable changes in this release. diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index 264bfe0f777..f2f3b1afbd1 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "matrix-sdk-qrcode" description = "Library to encode and decode QR codes for interactive verifications in Matrix land" -version = "0.7.1" +version = "0.8.0" authors = ["Damir Jelić "] edition = "2021" homepage = "https://github.com/matrix-org/matrix-rust-sdk" diff --git a/crates/matrix-sdk-sqlite/CHANGELOG.md b/crates/matrix-sdk-sqlite/CHANGELOG.md index ba8118892a2..d525613a28c 100644 --- a/crates/matrix-sdk-sqlite/CHANGELOG.md +++ b/crates/matrix-sdk-sqlite/CHANGELOG.md @@ -2,3 +2,15 @@ All notable changes to this project will be documented in this file. +## [0.8.0] - 2024-11-19 + +### Bug Fixes + +- Use the `DisplayName` struct to protect against homoglyph attacks. + + +### Refactor + +- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`. + + diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index 38ac860f89f..96f342f86a0 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "matrix-sdk-sqlite" -version = "0.7.1" +version = "0.8.0" edition = "2021" repository = "https://github.com/matrix-org/matrix-rust-sdk" description = "Sqlite storage backend for matrix-sdk" diff --git a/crates/matrix-sdk-store-encryption/CHANGELOG.md b/crates/matrix-sdk-store-encryption/CHANGELOG.md index ba8118892a2..dff6a57ebbf 100644 --- a/crates/matrix-sdk-store-encryption/CHANGELOG.md +++ b/crates/matrix-sdk-store-encryption/CHANGELOG.md @@ -2,3 +2,6 @@ All notable changes to this project will be documented in this file. +## [0.8.0] - 2024-11-19 + +No notable changes in this release. diff --git a/crates/matrix-sdk-store-encryption/Cargo.toml b/crates/matrix-sdk-store-encryption/Cargo.toml index 178d4f355e5..4328298929e 100644 --- a/crates/matrix-sdk-store-encryption/Cargo.toml +++ b/crates/matrix-sdk-store-encryption/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "matrix-sdk-store-encryption" -version = "0.7.0" +version = "0.8.0" edition = "2021" description = "Helpers for encrypted storage keys for the Matrix SDK" repository = "https://github.com/matrix-org/matrix-rust-sdk" diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 1068434a5a6..e069adf9f10 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -2,29 +2,40 @@ All notable changes to this project will be documented in this file. -# unreleased +## [0.8.0] - 2024-11-19 -Breaking changes: +### Bug Fixes -- `Timeline::edit` now takes a `RoomMessageEventContentWithoutRelation`. -- `Timeline::send_attachment` now takes an `impl Into` for the path of - the file to send. -- `Timeline::item_by_transaction_id` has been renamed to `Timeline::local_item_by_transaction_id` -(always returns local echoes). - -Bug fixes: +- Disable `share_pos()` inside `RoomListService`. - `UtdHookManager` no longer re-reports UTD events as late decryptions. ([#3480](https://github.com/matrix-org/matrix-rust-sdk/pull/3480)) + - Messages that we were unable to decrypt no longer display a red padlock. ([#3956](https://github.com/matrix-org/matrix-rust-sdk/issues/3956)) -Other changes: - - `UtdHookManager` no longer reports UTD events that were already reported in a previous session. ([#3519](https://github.com/matrix-org/matrix-rust-sdk/pull/3519)) +### Features + +- Add `m.room.join_rules` to the required state. + +- `EncryptionSyncService` and `Notification` are using + `Client::cross_process_store_locks_holder_name`. + + +### Refactor + +- [**breaking**] `Timeline::edit` now takes a `RoomMessageEventContentWithoutRelation`. + +- [**breaking**] `Timeline::send_attachment` now takes an `impl Into` + for the path of the file to send. + +- [**breaking**] `Timeline::item_by_transaction_id` has been renamed to + `Timeline::local_item_by_transaction_id` (always returns local echoes). + # 0.7.0 diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index cd1293b6420..ab8f743cc88 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "matrix-sdk-ui" description = "GUI-centric utilities on top of matrix-rust-sdk (experimental)." -version = "0.7.0" +version = "0.8.0" edition = "2021" repository = "https://github.com/matrix-org/matrix-rust-sdk" license = "Apache-2.0" diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index d7fb1387d6c..152518be3aa 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -1,51 +1,14 @@ -# unreleased +# Changelog -Breaking changes: +All notable changes to this project will be documented in this file. -- Renamed `VerificationLevel::PreviouslyVerified` to `VerificationLevel::VerificationViolation`. -- Add a `PreviouslyVerified` variant to `VerificationLevel` indicating that the identity is unverified and previously it was verified. -- Replace the `Notification` type from Ruma in `SyncResponse` and `Client::register_notification_handler` - by a custom one -- `Room::can_user_redact` and `Member::can_redact` are split between `*_redact_own` and `*_redact_other` -- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom` -- `AmbiguityCache` contains the room member's user ID -- Replace `impl MediaEventContent` with `&impl MediaEventContent` in - `Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail` -- A custom sliding sync proxy set with `ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered proxy. -- `Client::get_profile` was moved to `Account` and renamed to `Account::fetch_user_profile_of`. `Account::get_profile` was renamed to `Account::fetch_user_profile`. -- The `HttpError::UnableToCloneRequest` error variant has been removed because it was never used or - generated by the SDK. -- The `Error::InconsistentState` error variant has been removed because it was never used or - generated by the SDK. -- The widget capabilities in the FFI now need two additional flags: `update_delayed_event`, `send_delayed_event`. -- `Room::event` now takes an optional `RequestConfig` to allow for tweaking the network behavior. -- The `instant` module was removed, use the `ruma::time` module instead. -- Add `ClientBuilder::sqlite_store_with_cache_path` to build a client that stores caches in a different directory to state/crypto. -- The `body` parameter in `get_media_file` has been replaced with a `filename` parameter now that Ruma has a `filename()` method. +## [0.8.0] - 2024-11-19 -Additions: +### Bug Fixes -- new `UserIdentity::pin` method. -- new `ClientBuilder::with_decryption_trust_requirement` method. -- new `ClientBuilder::with_room_key_recipient_strategy` method -- new `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData setters, analogous to the GlobalAccountData -- new `RequestConfig.max_concurrent_requests` which allows to limit the maximum number of concurrent requests the internal HTTP client issues (all others have to wait until the number drops below that threshold again) -- Expose new method `Client::Oidc::login_with_qr_code()`. - ([#3466](https://github.com/matrix-org/matrix-rust-sdk/pull/3466)) -- Add the `ClientBuilder::add_root_certificates()` method which re-exposes the - `reqwest::ClientBuilder::add_root_certificate()` functionality. -- Add `Room::get_user_power_level(user_id)` and `Room::get_suggested_user_role(user_id)` to be able to fetch power level info about an user without loading the room member list. -- Add new method `discard_room_key` on `Room` that allows to discard the current - outbound session for that room. Can be used by clients as a dev tool like the `/discardsession` command. -- Add a new `LinkedChunk` data structure to represents all events per room ([#3166](https://github.com/matrix-org/matrix-rust-sdk/pull/3166)). -- Add new methods for tracking (on device only) the user's recently visited rooms called `Account::track_recently_visited_room(roomId)` and `Account::get_recently_visited_rooms()` -- Add `send_call_notification` and `send_call_notification_if_needed` methods. This allows to implement sending ring events on call start. -- The `get_media_content`, `get_media_file` and `get_file` methods of the - `Media` api now support the new authenticated media endpoints. -- WidgetDriver: Support the `"delay"` field in the `send_event` widget actions. -This allows to send delayed events, as defined in [MSC4157](https://github.com/matrix-org/matrix-spec-proposals/pull/4157) +- Add more invalid characters for room aliases. -Bug fixes: +- Match the right status code in `Client::is_room_alias_available`. - Fix a bug where room keys were considered to be downloaded before backups were enabled. This bug only affects the @@ -53,6 +16,165 @@ Bug fixes: made to download a room key, if a decryption failure with a given room key would have been encountered before the backups were enabled. +### Documentation + +- Improve documentation of `Client::observe_events`. + + +### Features + + +- Add `create_room_alias` function. + +- `Client::cross_process_store_locks_holder_name` is used everywhere: + - `StoreConfig::new()` now takes a + `cross_process_store_locks_holder_name` argument. + - `StoreConfig` no longer implements `Default`. + - `BaseClient::new()` has been removed. + - `BaseClient::clone_with_in_memory_state_store()` now takes a + `cross_process_store_locks_holder_name` argument. + - `BaseClient` no longer implements `Default`. + - `EventCacheStoreLock::new()` no longer takes a `key` argument. + - `BuilderStoreConfig` no longer has + `cross_process_store_locks_holder_name` field for `Sqlite` and + `IndexedDb`. + +- `EncryptionSyncService` and `Notification` are using `Client::cross_process_store_locks_holder_name`. + +- Allow passing a custom `RequestConfig` to an upload request. + +- Retry uploads if they've failed with transient errors. + +- Implement `EventHandlerContext` for tuples. + +- Introduce a mechanism similar to `Client::add_event_handler` and + `Client::add_room_event_handler` but with a reactive programming pattern. Add + `Client::observe_events` and `Client::observe_room_events`. + + ```rust + // Get an observer. + let observer = + client.observe_events::)>(); + + // Subscribe to the observer. + let mut subscriber = observer.subscribe(); + + // Use the subscriber as a `Stream`. + let (message_event, (room, push_actions)) = subscriber.next().await.unwrap(); + ``` + + When calling `observe_events`, one has to specify the type of event (in the + example, `SyncRoomMessageEvent`) and a context (in the example, `(Room, + Vec)`, respectively for the room and the push actions). + +- Implement unwedging for media uploads. + +- Send state from state sync and not from timeline to widget ([#4254](https://github.com/matrix-org/matrix-rust-sdk/pull/4254)) + +- Allow aborting media uploads. + +- Add `RoomPreviewInfo::num_active_members`. + +- Use room directory search as another data source. + +- Check if the user is allowed to do a room mention before trying to send a call + notify event. + ([#4271](https://github.com/matrix-org/matrix-rust-sdk/pull/4271)) + +- Add `Client::cross_process_store_locks_holder_name()`. + +- Add a `PreviouslyVerified` variant to `VerificationLevel` indicating that the + identity is unverified and previously it was verified. + +- New `UserIdentity::pin` method. + +- New `ClientBuilder::with_decryption_trust_requirement` method. + +- New `ClientBuilder::with_room_key_recipient_strategy` method + +- New `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData + setters, analogous to the GlobalAccountData + +- New `RequestConfig.max_concurrent_requests` which allows to limit the maximum + number of concurrent requests the internal HTTP client issues (all others have + to wait until the number drops below that threshold again) + +- Implement proper redact handling in the widget driver. This allows the Rust + SDK widget driver to support widgets that rely on redacting. + + +### Refactor +- [**breaking**] Rename `DisplayName` to `RoomDisplayName`. + +- Improve `is_room_alias_format_valid` so it's more strict. + +- Remove duplicated fields in media event contents. + +- Use `SendHandle` for media uploads too. + +- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`. + +- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`. + +- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`. + +- Move `formatted_caption_from` to the SDK, rename it. + +- Tidy up and start commenting the widget code. + +- Get rid of `ProcessingContext` and inline it in its callers. + +- Get rid of unused `limits` parameter when constructing a `WidgetMachine`. + +- Use a specialized mutex for locking access to the state store and + `being_sent`. + +- Renamed `VerificationLevel::PreviouslyVerified` to + `VerificationLevel::VerificationViolation`. + +- [**breaking**] Replace the `Notification` type from Ruma in `SyncResponse` and + `Client::register_notification_handler` by a custom one. + +- [**breaking**] The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` + and `LeftRoom`. + +- [**breaking**] `Room::can_user_redact` and `Member::can_redact` are split + between `*_redact_own` and `*_redact_other`. + +- [**breaking**] `AmbiguityCache` contains the room member's user ID. + +- [**breaking**] Replace `impl MediaEventContent` with `&impl MediaEventContent` in + `Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail` + +- [**breaking**] A custom sliding sync proxy set with + `ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered + proxy. + +- [**breaking**] `Client::get_profile` was moved to `Account` and renamed to + `Account::fetch_user_profile_of`. `Account::get_profile` was renamed to + `Account::fetch_user_profile`. + +- [**breaking**] The `HttpError::UnableToCloneRequest` error variant has been + removed because it was never used or generated by the SDK. + +- [**breaking**] The `Error::InconsistentState` error variant has been removed + because it was never used or generated by the SDK. + +- [**breaking**] The widget capabilities in the FFI now need two additional + flags: `update_delayed_event`, `send_delayed_event`. + +- [**breaking**] `Room::event` now takes an optional `RequestConfig` to allow + for tweaking the network behavior. + +- [**breaking**] The `instant` module was removed, use the `ruma::time` module + instead. + +- [**breaking**] Add `ClientBuilder::sqlite_store_with_cache_path` to build a + client that stores caches in a different directory to state/crypto. + +- [**breaking**] The `body` parameter in `get_media_file` has been replaced with + a `filename` parameter now that Ruma has a `filename()` method. + # 0.7.0 Breaking changes: diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 59b11de6b8c..b983f4312d7 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.7.1" +version = "0.8.0" [package.metadata.docs.rs] features = ["docsrs"] From bc0c2a6be204eeaab5072150b6679f2cd9619464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 19 Nov 2024 09:05:41 +0100 Subject: [PATCH 551/979] feat(room_preview): Add `RoomPreview::heroes` field for known rooms --- bindings/matrix-sdk-ffi/src/room_preview.rs | 11 ++++++++++- crates/matrix-sdk/src/room_preview.rs | 9 ++++++++- .../src/tests/sliding_sync/room.rs | 15 ++++++++------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 3a3e08cb867..8bfe945519e 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -4,7 +4,10 @@ use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule}; use tracing::warn; use crate::{ - client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember, + client::JoinRule, + error::ClientError, + room::{Membership, RoomHero}, + room_member::RoomMember, utils::AsyncRuntimeDropped, }; @@ -38,6 +41,10 @@ impl RoomPreview { .try_into() .map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?, is_direct: info.is_direct, + heroes: info + .heroes + .as_ref() + .map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()), }) } @@ -92,6 +99,8 @@ pub struct RoomPreviewInfo { pub join_rule: JoinRule, /// Whether the room is direct or not, if known. pub is_direct: Option, + /// Room heroes. + pub heroes: Option>, } impl TryFrom for JoinRule { diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index f68371b4b28..18f43120800 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -19,7 +19,7 @@ //! well. use futures_util::future::join_all; -use matrix_sdk_base::{RoomInfo, RoomState}; +use matrix_sdk_base::{RoomHero, RoomInfo, RoomState}; use ruma::{ api::client::{membership::joined_members, state::get_state_events}, directory::PublicRoomJoinRule, @@ -77,6 +77,9 @@ pub struct RoomPreview { /// The `m.room.direct` state of the room, if known. pub is_direct: Option, + + /// Room heroes. + pub heroes: Option>, } impl RoomPreview { @@ -116,6 +119,7 @@ impl RoomPreview { num_active_members, state, is_direct, + heroes: Some(room_info.heroes().to_vec()), } } @@ -262,6 +266,7 @@ impl RoomPreview { is_world_readable: response.world_readable, state, is_direct, + heroes: None, }) } @@ -313,6 +318,7 @@ impl RoomPreview { num_joined_members, num_active_members, state, + None, )) } } @@ -355,6 +361,7 @@ async fn search_for_room_preview_in_room_directory( is_world_readable: room_description.is_world_readable, state: None, is_direct: None, + heroes: None, })); } diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 9a775188fd6..15a277d6314 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -1092,7 +1092,7 @@ async fn test_room_preview() -> Result<()> { // methods. info!("Alice gets a preview of the public room using any method"); let preview = alice.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); - assert_room_preview(&preview, &room_alias); + assert_room_preview_from_unknown(&preview, &room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); } @@ -1193,7 +1193,7 @@ async fn test_room_preview_with_room_directory_search_and_room_alias_only_in_sev assert_eq!(preview.room_id, expected_room_id); } -fn assert_room_preview(preview: &RoomPreview, room_alias: &str) { +fn assert_room_preview_from_unknown(preview: &RoomPreview, room_alias: &str) { assert_eq!(preview.canonical_alias.as_ref().unwrap().alias(), room_alias); assert_eq!(preview.name.as_ref().unwrap(), "Alice's Room"); assert_eq!(preview.topic.as_ref().unwrap(), "Discussing Alice's Topic"); @@ -1202,6 +1202,7 @@ fn assert_room_preview(preview: &RoomPreview, room_alias: &str) { assert!(preview.room_type.is_none()); assert_eq!(preview.join_rule, SpaceRoomJoinRule::Invite); assert!(preview.is_world_readable); + assert!(preview.heroes.is_none()); } async fn get_room_preview_with_room_state( @@ -1214,14 +1215,14 @@ async fn get_room_preview_with_room_state( // Alice has joined the room, so they get the full details. info!("Alice gets a preview of the public room from state events"); let preview = RoomPreview::from_state_events(alice, room_id).await.unwrap(); - assert_room_preview(&preview, room_alias); + assert_room_preview_from_unknown(&preview, room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); // Bob definitely doesn't know about the room, but they can get a preview of the // room too. info!("Bob gets a preview of the public room from state events"); let preview = RoomPreview::from_state_events(bob, room_id).await.unwrap(); - assert_room_preview(&preview, room_alias); + assert_room_preview_from_unknown(&preview, room_alias); assert!(preview.state.is_none()); // Bob can't preview the second room, because its history visibility is neither @@ -1245,7 +1246,7 @@ async fn get_room_preview_with_room_summary( .await .unwrap(); - assert_room_preview(&preview, room_alias); + assert_room_preview_from_unknown(&preview, room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); // The preview also works when using the room alias parameter. @@ -1260,7 +1261,7 @@ async fn get_room_preview_with_room_summary( .await .unwrap(); - assert_room_preview(&preview, room_alias); + assert_room_preview_from_unknown(&preview, room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); // Bob definitely doesn't know about the room, but they can get a preview of the @@ -1270,7 +1271,7 @@ async fn get_room_preview_with_room_summary( RoomPreview::from_room_summary(bob, room_id.to_owned(), room_id.into(), Vec::new()) .await .unwrap(); - assert_room_preview(&preview, room_alias); + assert_room_preview_from_unknown(&preview, room_alias); assert!(preview.state.is_none()); // Bob can preview the second room with the room summary (because its join rule From 0af53e99eeee15c4608a72e82f776085589245a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 19 Nov 2024 09:05:52 +0100 Subject: [PATCH 552/979] feat(room_preview): Compute display name for `RoomPreview` when possible --- crates/matrix-sdk/src/room_preview.rs | 10 ++++-- .../tests/integration/room_preview.rs | 36 ++++++++++++++++++- .../src/tests/sliding_sync/room.rs | 26 +++++++++----- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 18f43120800..9238b9eeb74 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -93,11 +93,12 @@ impl RoomPreview { num_joined_members: u64, num_active_members: Option, state: Option, + computed_display_name: Option, ) -> Self { RoomPreview { room_id: room_info.room_id().to_owned(), canonical_alias: room_info.canonical_alias().map(ToOwned::to_owned), - name: room_info.name().map(ToOwned::to_owned), + name: computed_display_name.or_else(|| room_info.name().map(ToOwned::to_owned)), topic: room_info.topic().map(ToOwned::to_owned), avatar_url: room_info.avatar_url().map(ToOwned::to_owned), room_type: room_info.room_type().cloned(), @@ -128,12 +129,15 @@ impl RoomPreview { pub(crate) async fn from_known(room: &Room) -> Self { let is_direct = room.is_direct().await.ok(); + let display_name = room.compute_display_name().await.ok().map(|name| name.to_string()); + Self::from_room_info( room.clone_info(), is_direct, room.joined_members_count(), Some(room.active_members_count()), Some(room.state()), + display_name, ) } @@ -247,7 +251,7 @@ impl RoomPreview { let num_active_members = cached_room.as_ref().map(|r| r.active_members_count()); - let is_direct = if let Some(cached_room) = cached_room { + let is_direct = if let Some(cached_room) = &cached_room { cached_room.is_direct().await.ok() } else { None @@ -266,7 +270,7 @@ impl RoomPreview { is_world_readable: response.world_readable, state, is_direct, - heroes: None, + heroes: cached_room.map(|r| r.heroes()), }) } diff --git a/crates/matrix-sdk/tests/integration/room_preview.rs b/crates/matrix-sdk/tests/integration/room_preview.rs index 142df0b83ef..1d78439c1c9 100644 --- a/crates/matrix-sdk/tests/integration/room_preview.rs +++ b/crates/matrix-sdk/tests/integration/room_preview.rs @@ -1,9 +1,15 @@ +#[cfg(feature = "experimental-sliding-sync")] +use js_int::uint; use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; +#[cfg(feature = "experimental-sliding-sync")] +use matrix_sdk_base::sliding_sync; use matrix_sdk_base::RoomState; use matrix_sdk_test::{ async_test, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, SyncResponseBuilder, }; -use ruma::{room_id, space::SpaceRoomJoinRule, RoomId}; +#[cfg(feature = "experimental-sliding-sync")] +use ruma::{api::client::sync::sync_events::v5::response::Hero, assign}; +use ruma::{owned_user_id, room_id, space::SpaceRoomJoinRule, RoomId}; use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, @@ -93,6 +99,34 @@ async fn test_room_preview_leave_unknown_room_fails() { assert!(client.get_room(room_id).is_none()); } +#[cfg(feature = "experimental-sliding-sync")] +#[async_test] +async fn test_room_preview_computes_name_if_room_is_known() { + let (client, _) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + // Given a room with no name but a hero + let room = assign!(sliding_sync::http::response::Room::new(), { + name: None, + heroes: Some(vec![assign!(Hero::new(owned_user_id!("@alice:matrix.org")), { + name: Some("Alice".to_owned()), + avatar: None, + })]), + joined_count: Some(uint!(1)), + invited_count: Some(uint!(1)), + }); + let mut response = sliding_sync::http::Response::new("0".to_owned()); + response.rooms.insert(room_id.to_owned(), room); + + client.process_sliding_sync_test_helper(&response).await.expect("Failed to process sync"); + + // When we get its preview + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + + // Its name is computed from its heroes + assert_eq!(room_preview.name.unwrap(), "Alice"); +} + async fn mock_leave(room_id: &RoomId, server: &MockServer) { Mock::given(method("POST")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/leave")) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 15a277d6314..aa85dcb0360 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -1091,9 +1091,10 @@ async fn test_room_preview() -> Result<()> { // Dummy test for `Client::get_room_preview` which may call one or the other // methods. info!("Alice gets a preview of the public room using any method"); - let preview = alice.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); - assert_room_preview_from_unknown(&preview, &room_alias); + let preview = alice.get_room_preview(room_id.into(), Vec::new()).await?; + assert_room_preview(&preview, &room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); + assert!(preview.heroes.is_some()); } Ok(()) @@ -1131,6 +1132,7 @@ async fn test_room_preview_with_room_directory_search_and_room_alias_only() { .await .expect("room preview couldn't be retrieved"); assert_eq!(preview.room_id, expected_room_id); + assert!(preview.heroes.is_none()); } #[async_test] @@ -1191,9 +1193,10 @@ async fn test_room_preview_with_room_directory_search_and_room_alias_only_in_sev .await .expect("room preview couldn't be retrieved"); assert_eq!(preview.room_id, expected_room_id); + assert!(preview.heroes.is_none()); } -fn assert_room_preview_from_unknown(preview: &RoomPreview, room_alias: &str) { +fn assert_room_preview(preview: &RoomPreview, room_alias: &str) { assert_eq!(preview.canonical_alias.as_ref().unwrap().alias(), room_alias); assert_eq!(preview.name.as_ref().unwrap(), "Alice's Room"); assert_eq!(preview.topic.as_ref().unwrap(), "Discussing Alice's Topic"); @@ -1202,7 +1205,6 @@ fn assert_room_preview_from_unknown(preview: &RoomPreview, room_alias: &str) { assert!(preview.room_type.is_none()); assert_eq!(preview.join_rule, SpaceRoomJoinRule::Invite); assert!(preview.is_world_readable); - assert!(preview.heroes.is_none()); } async fn get_room_preview_with_room_state( @@ -1215,15 +1217,17 @@ async fn get_room_preview_with_room_state( // Alice has joined the room, so they get the full details. info!("Alice gets a preview of the public room from state events"); let preview = RoomPreview::from_state_events(alice, room_id).await.unwrap(); - assert_room_preview_from_unknown(&preview, room_alias); + assert_room_preview(&preview, room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); + assert!(preview.heroes.is_some()); // Bob definitely doesn't know about the room, but they can get a preview of the // room too. info!("Bob gets a preview of the public room from state events"); let preview = RoomPreview::from_state_events(bob, room_id).await.unwrap(); - assert_room_preview_from_unknown(&preview, room_alias); + assert_room_preview(&preview, room_alias); assert!(preview.state.is_none()); + assert!(preview.heroes.is_some()); // Bob can't preview the second room, because its history visibility is neither // world-readable, nor have they joined the room before. @@ -1246,8 +1250,9 @@ async fn get_room_preview_with_room_summary( .await .unwrap(); - assert_room_preview_from_unknown(&preview, room_alias); + assert_room_preview(&preview, room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); + assert!(preview.heroes.is_some()); // The preview also works when using the room alias parameter. info!("Alice gets a preview of the public room from msc3266 using the room alias"); @@ -1261,8 +1266,9 @@ async fn get_room_preview_with_room_summary( .await .unwrap(); - assert_room_preview_from_unknown(&preview, room_alias); + assert_room_preview(&preview, room_alias); assert_eq!(preview.state, Some(RoomState::Joined)); + assert!(preview.heroes.is_some()); // Bob definitely doesn't know about the room, but they can get a preview of the // room too. @@ -1271,8 +1277,9 @@ async fn get_room_preview_with_room_summary( RoomPreview::from_room_summary(bob, room_id.to_owned(), room_id.into(), Vec::new()) .await .unwrap(); - assert_room_preview_from_unknown(&preview, room_alias); + assert_room_preview(&preview, room_alias); assert!(preview.state.is_none()); + assert!(preview.heroes.is_none()); // Bob can preview the second room with the room summary (because its join rule // is set to public, or because Alice is a member of that room). @@ -1287,4 +1294,5 @@ async fn get_room_preview_with_room_summary( .unwrap(); assert_eq!(preview.name.unwrap(), "Alice's Room 2"); assert!(preview.state.is_none()); + assert!(preview.heroes.is_none()); } From 6b80055bd25cd57fa15505836cf58e6c11b515c2 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 19 Nov 2024 16:40:18 +0100 Subject: [PATCH 553/979] fix(utd_hook): Fix regression causing retry to report false late decrypt (#4252) There has been a recent change on `Decryptor::decrypt_event_impl` causing the function to return an TimelineEvent of kind unable to decrypt instead of failing with an error. The `late_decrypt` detection code was not changed, causing any retry to mark UTDs as late decrypt. --- .../src/timeline/controller/mod.rs | 26 ++++-- .../src/timeline/tests/encryption.rs | 91 ++++++++++++++++--- crates/matrix-sdk-ui/src/timeline/traits.rs | 14 ++- 3 files changed, 106 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 8f09d4f3095..b6fb11bfca3 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -22,7 +22,7 @@ use imbl::Vector; #[cfg(test)] use matrix_sdk::crypto::OlmMachine; use matrix_sdk::{ - deserialized_responses::SyncTimelineEvent, + deserialized_responses::{SyncTimelineEvent, TimelineEventKind as SdkTimelineEventKind}, event_cache::{paginator::Paginator, RoomEventCache}, send_queue::{ LocalEcho, LocalEchoContent, RoomSendQueueUpdate, SendHandle, SendReactionHandle, @@ -1064,16 +1064,22 @@ impl TimelineController

{ match decryptor.decrypt_event_impl(original_json).await { Ok(event) => { - trace!( - "Successfully decrypted event that previously failed to decrypt" - ); - - // Notify observers that we managed to eventually decrypt an event. - if let Some(hook) = unable_to_decrypt_hook { - hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await; + if let SdkTimelineEventKind::UnableToDecrypt { utd_info, .. } = + event.kind + { + info!( + "Failed to decrypt event after receiving room key: {:?}", + utd_info.reason + ); + None + } else { + // Notify observers that we managed to eventually decrypt an event. + if let Some(hook) = unable_to_decrypt_hook { + hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await; + } + + Some(event) } - - Some(event) } Err(e) => { info!("Failed to decrypt event after receiving room key: {e}"); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index d7e0f812d6f..131cffae580 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -18,6 +18,7 @@ use std::{ io::Cursor, iter, sync::{Arc, Mutex}, + time::Duration, }; use as_variant::as_variant; @@ -43,6 +44,7 @@ use ruma::{ }; use serde_json::{json, value::to_raw_value}; use stream_assert::assert_next_matches; +use tokio::time::sleep; use super::TestTimeline; use crate::{ @@ -50,6 +52,17 @@ use crate::{ unable_to_decrypt_hook::{UnableToDecryptHook, UnableToDecryptInfo, UtdHookManager}, }; +#[derive(Debug, Default)] +struct DummyUtdHook { + utds: Mutex>, +} + +impl UnableToDecryptHook for DummyUtdHook { + fn on_utd(&self, info: UnableToDecryptInfo) { + self.utds.lock().unwrap().push(info); + } +} + #[async_test] async fn test_retry_message_decryption() { const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU"; @@ -67,17 +80,6 @@ async fn test_retry_message_decryption() { HztoSJUr/2Y\n\ -----END MEGOLM SESSION DATA-----"; - #[derive(Debug, Default)] - struct DummyUtdHook { - utds: Mutex>, - } - - impl UnableToDecryptHook for DummyUtdHook { - fn on_utd(&self, info: UnableToDecryptInfo) { - self.utds.lock().unwrap().push(info); - } - } - let hook = Arc::new(DummyUtdHook::default()); let client = test_client_builder(None).build().await.unwrap(); let utd_hook = Arc::new(UtdHookManager::new(hook.clone(), client)); @@ -170,6 +172,73 @@ async fn test_retry_message_decryption() { } } +// There has been a regression when the `retry_event_decryption` function +// changed from failing with an Error to instead return a new type of timeline +// event in UTD. The regression caused the timeline to consider any +// re-decryption attempt as successful. +#[async_test] +async fn test_false_positive_late_decryption_regression() { + const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU"; + + let hook = Arc::new(DummyUtdHook::default()); + let client = test_client_builder(None).build().await.unwrap(); + let utd_hook = + Arc::new(UtdHookManager::new(hook.clone(), client).with_max_delay(Duration::from_secs(1))); + + let timeline = TestTimeline::with_unable_to_decrypt_hook(utd_hook.clone()); + + let f = &timeline.factory; + timeline + .handle_live_event( + f.event(RoomEncryptedEventContent::new( + EncryptedEventScheme::MegolmV1AesSha2( + MegolmV1AesSha2ContentInit { + ciphertext: "\ + AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\ + cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\ + YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\ + CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\ + hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\ + QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ" + .to_owned(), + sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA".to_owned(), + device_id: "NLAZCWIOCO".into(), + session_id: SESSION_ID.into(), + } + .into(), + ), + None, + )) + .sender(&BOB) + .into_utd_sync_timeline_event(), + ) + .await; + + let own_user_id = user_id!("@example:morheus.localhost"); + let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await; + + timeline + .controller + .retry_event_decryption_test( + room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost"), + olm_machine, + Some(iter::once(SESSION_ID.to_owned()).collect()), + ) + .await; + assert_eq!(timeline.controller.items().await.len(), 2); + + // Wait past the max delay for utd late decryption detection + sleep(Duration::from_secs(2)).await; + + { + let utds = hook.utds.lock().unwrap(); + assert_eq!(utds.len(), 1); + // This is the main thing we're testing: if this wasn't identified as a definite + // UTD, this would be `Some(..)`. + assert!(utds[0].time_to_decrypt.is_none()); + } +} + #[async_test] async fn test_retry_edit_decryption() { const SESSION1_KEY: &[u8] = b"\ diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index 158488eb06f..24015c46774 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -18,7 +18,7 @@ use eyeball::Subscriber; use futures_util::FutureExt as _; use indexmap::IndexMap; #[cfg(test)] -use matrix_sdk::crypto::{DecryptionSettings, TrustRequirement}; +use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement}; use matrix_sdk::{ deserialized_responses::TimelineEvent, event_cache::paginator::PaginableRoom, BoxFuture, Result, Room, @@ -302,8 +302,14 @@ impl Decryptor for (matrix_sdk_base::crypto::OlmMachine, ruma::OwnedRoomId) { let (olm_machine, room_id) = self; let decryption_settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; - let event = - olm_machine.decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings).await?; - Ok(event.into()) + match olm_machine + .try_decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings) + .await? + { + RoomEventDecryptionResult::Decrypted(decrypted) => Ok(decrypted.into()), + RoomEventDecryptionResult::UnableToDecrypt(utd_info) => { + Ok(TimelineEvent::new_utd_event(raw.clone(), utd_info)) + } + } } } From efeac2ef39d3d81ae25088270ffd6356af6dbbb1 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 16:16:21 +0100 Subject: [PATCH 554/979] fix(base): clear a room's send queue and dependent event queue after removing it from the state store --- .../src/store/integration_tests.rs | 20 +++++++++++++++++++ .../matrix-sdk-base/src/store/memory_store.rs | 2 ++ crates/matrix-sdk-sqlite/src/state_store.rs | 11 ++++++++++ 3 files changed, 33 insertions(+) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 3fb6ac0bc9d..26412fa8aca 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -972,6 +972,24 @@ impl StateStoreIntegrationTests for DynStateStore { self.populate().await?; + { + // Add a send queue request in that room. + let txn = TransactionId::new(); + let ev = + SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into()) + .unwrap(); + self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?; + + // Add a single dependent queue request. + self.save_dependent_queued_request( + room_id, + &txn, + ChildTransactionId::new(), + DependentQueuedRequestKind::RedactEvent, + ) + .await?; + } + self.remove_room(room_id).await?; assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there"); @@ -1023,6 +1041,8 @@ impl StateStoreIntegrationTests for DynStateStore { .is_empty(), "still event recepts in the store" ); + assert!(self.load_send_queue_requests(room_id).await?.is_empty()); + assert!(self.load_dependent_queued_requests(room_id).await?.is_empty()); self.remove_room(stripped_room_id).await?; diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index ac2951be983..623fba571b9 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -796,6 +796,8 @@ impl StateStore for MemoryStore { self.stripped_members.write().unwrap().remove(room_id); self.room_user_receipts.write().unwrap().remove(room_id); self.room_event_receipts.write().unwrap().remove(room_id); + self.send_queue_events.write().unwrap().remove(room_id); + self.dependent_send_queue_events.write().unwrap().remove(room_id); Ok(()) } diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 02aeb0f545a..b67b7c459c0 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -509,6 +509,7 @@ trait SqliteConnectionStateStoreExt { fn remove_display_name(&self, room_id: &[u8], name: &[u8]) -> rusqlite::Result<()>; fn remove_room_display_names(&self, room_id: &[u8]) -> rusqlite::Result<()>; fn remove_room_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()>; + fn remove_room_dependent_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()>; } impl SqliteConnectionStateStoreExt for rusqlite::Connection { @@ -720,6 +721,12 @@ impl SqliteConnectionStateStoreExt for rusqlite::Connection { self.prepare("DELETE FROM send_queue_events WHERE room_id = ?")?.execute((room_id,))?; Ok(()) } + + fn remove_room_dependent_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()> { + self.prepare("DELETE FROM dependent_send_queue_events WHERE room_id = ?")? + .execute((room_id,))?; + Ok(()) + } } #[async_trait] @@ -1726,6 +1733,10 @@ impl StateStore for SqliteStateStore { let send_queue_room_id = this.encode_key(keys::SEND_QUEUE, &room_id); txn.remove_room_send_queue(&send_queue_room_id)?; + let dependent_send_queue_room_id = + this.encode_key(keys::DEPENDENTS_SEND_QUEUE, &room_id); + txn.remove_room_dependent_send_queue(&dependent_send_queue_room_id)?; + Ok(()) }) .await From b987fc1de21d2ca4e4bcc13b77b4dad623105c63 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 12:40:16 +0100 Subject: [PATCH 555/979] fix(media): include the formatted caption and filename for audio and file attachments too --- crates/matrix-sdk/src/room/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index c8b95ca283e..1c3dc8f028b 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2076,7 +2076,10 @@ impl Room { } mime::AUDIO => { - let mut content = AudioMessageEventContent::new(body, source); + let mut content = assign!(AudioMessageEventContent::new(body, source), { + formatted: formatted_caption, + filename + }); if let Some(AttachmentInfo::Voice { audio_info, waveform: Some(waveform_vec) }) = &info @@ -2117,7 +2120,9 @@ impl Room { thumbnail_info }); let content = assign!(FileMessageEventContent::new(body, source), { - info: Some(Box::new(info)) + info: Some(Box::new(info)), + formatted: formatted_caption, + filename, }); MessageType::File(content) } From f20401c65788689238f8620e727949f522747105 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 12:40:25 +0100 Subject: [PATCH 556/979] test(timeline): add an integration test for sending an attachment in the timeline Also includes a caption for a file media event, which acts as a regression test for the previous commit. --- .../tests/integration/timeline/media.rs | 141 ++++++++++++++++++ .../tests/integration/timeline/mod.rs | 1 + 2 files changed, 142 insertions(+) create mode 100644 crates/matrix-sdk-ui/tests/integration/timeline/media.rs diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs new file mode 100644 index 00000000000..ef5cb20d163 --- /dev/null +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -0,0 +1,141 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fs::File, io::Write as _, time::Duration}; + +use assert_matches::assert_matches; +use assert_matches2::assert_let; +use eyeball_im::VectorDiff; +use futures_util::{FutureExt, StreamExt}; +use matrix_sdk::{ + assert_let_timeout, + attachment::AttachmentConfig, + test_utils::{events::EventFactory, mocks::MatrixMockServer}, +}; +use matrix_sdk_test::{async_test, JoinedRoomBuilder, ALICE}; +use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent}; +use ruma::{ + event_id, + events::room::{message::MessageType, MediaSource}, + room_id, +}; +use serde_json::json; +use tempfile::TempDir; +use tokio::time::sleep; +use wiremock::ResponseTemplate; + +#[async_test] +async fn test_send_attachment() { + let mock = MatrixMockServer::new().await; + let client = mock.client_builder().build().await; + + mock.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = mock.sync_joined_room(&client, room_id).await; + let timeline = room.timeline().await.unwrap(); + + let (items, mut timeline_stream) = + timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; + assert!(items.is_empty()); + + let f = EventFactory::new(); + mock.sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hello").sender(&ALICE)), + ) + .await; + + // Sanity check. + assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + assert_eq!(msg.body(), "hello"); + + // No other updates. + assert!(timeline_stream.next().now_or_never().is_none()); + + // Store a file in a temporary directory. + let tmp_dir = TempDir::new().unwrap(); + + let file_path = tmp_dir.path().join("test.bin"); + { + let mut file = File::create(&file_path).unwrap(); + file.write_all(b"hello world").unwrap(); + } + + // Set up mocks for the file upload. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(2)).set_body_json( + json!({ + "content_uri": "mxc://sdk.rs/media" + }), + )) + .mock_once() + .mount() + .await; + + mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await; + + // Queue sending of an attachment. + let config = AttachmentConfig::new().caption(Some("caption".to_owned())); + timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap(); + + { + assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + + assert_let!(TimelineItemContent::Message(msg) = item.content()); + + // Body is the caption, because there's both a caption and filename. + assert_eq!(msg.body(), "caption"); + assert_let!(MessageType::File(file) = msg.msgtype()); + assert_eq!(file.filename(), "test.bin"); + assert_eq!(file.caption(), Some("caption")); + + // The URI refers to the local cache. + assert_let!(MediaSource::Plain(uri) = &file.source); + assert!(uri.to_string().contains("localhost")); + } + + // Eventually, the media is updated with the final MXC IDs… + sleep(Duration::from_secs(2)).await; + + { + assert_let_timeout!( + Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next() + ); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + + assert_let!(MessageType::File(file) = msg.msgtype()); + assert_eq!(file.filename(), "test.bin"); + assert_eq!(file.caption(), Some("caption")); + + // The URI now refers to the final MXC URI. + assert_let!(MediaSource::Plain(uri) = &file.source); + assert_eq!(uri.to_string(), "mxc://sdk.rs/media"); + } + + // And eventually the event itself is sent. + { + assert_let_timeout!( + Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next() + ); + assert_matches!(item.send_state(), Some(EventSendState::Sent{ event_id }) => { + assert_eq!(event_id, event_id!("$media")); + }); + } + + // That's all, folks! + assert!(timeline_stream.next().now_or_never().is_none()); +} diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index b6bc4211140..a70f3a499ff 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -53,6 +53,7 @@ use crate::mock_sync; mod echo; mod edit; mod focus_event; +mod media; mod pagination; mod pinned_event; mod profiles; From 8a6ced0e8fc4ae0b184082d53759d2e89ebb86e6 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 14:27:21 +0100 Subject: [PATCH 557/979] fix(send queue): when adding a local reaction, look for media events in dependent requests too --- .../matrix-sdk-base/src/store/send_queue.rs | 21 ++++ .../src/timeline/controller/mod.rs | 2 +- .../tests/integration/timeline/media.rs | 96 +++++++++++++++---- crates/matrix-sdk/src/send_queue.rs | 12 ++- 4 files changed, 113 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 4d3b4a76bb8..6e7aac5f072 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -367,6 +367,27 @@ pub struct DependentQueuedRequest { pub parent_key: Option, } +impl DependentQueuedRequest { + /// Does the dependent request represent a new event that is *not* + /// aggregated, aka it is going to be its own item in a timeline? + pub fn is_own_event(&self) -> bool { + match self.kind { + DependentQueuedRequestKind::EditEvent { .. } + | DependentQueuedRequestKind::RedactEvent + | DependentQueuedRequestKind::ReactEvent { .. } + | DependentQueuedRequestKind::UploadFileWithThumbnail { .. } => { + // These are all aggregated events, or non-visible items (file upload producing + // a new MXC ID). + false + } + DependentQueuedRequestKind::FinishUpload { .. } => { + // This one graduates into a new media event. + true + } + } + } +} + #[cfg(not(tarpaulin_include))] impl fmt::Debug for QueuedRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index b6fb11bfca3..6b8605ba7e2 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -505,7 +505,7 @@ impl TimelineController

{ let Some(prev_status) = prev_status else { match &item.kind { EventTimelineItemKind::Local(local) => { - if let Some(send_handle) = local.send_handle.clone() { + if let Some(send_handle) = &local.send_handle { if send_handle .react(key.to_owned()) .await diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs index ef5cb20d163..a6065e5f3d9 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{fs::File, io::Write as _, time::Duration}; +use std::{fs::File, io::Write as _, path::PathBuf, time::Duration}; use assert_matches::assert_matches; use assert_matches2::assert_let; @@ -35,6 +35,24 @@ use tempfile::TempDir; use tokio::time::sleep; use wiremock::ResponseTemplate; +fn create_temporary_file(filename: &str) -> (TempDir, PathBuf) { + let tmp_dir = TempDir::new().unwrap(); + let file_path = tmp_dir.path().join(filename); + let mut file = File::create(&file_path).unwrap(); + file.write_all(b"hello world").unwrap(); + (tmp_dir, file_path) +} + +fn get_filename_and_caption(msg: &MessageType) -> (&str, Option<&str>) { + match msg { + MessageType::File(event) => (event.filename(), event.caption()), + MessageType::Image(event) => (event.filename(), event.caption()), + MessageType::Video(event) => (event.filename(), event.caption()), + MessageType::Audio(event) => (event.filename(), event.caption()), + _ => panic!("unexpected message type"), + } +} + #[async_test] async fn test_send_attachment() { let mock = MatrixMockServer::new().await; @@ -48,6 +66,7 @@ async fn test_send_attachment() { let (items, mut timeline_stream) = timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; + assert!(items.is_empty()); let f = EventFactory::new(); @@ -66,13 +85,7 @@ async fn test_send_attachment() { assert!(timeline_stream.next().now_or_never().is_none()); // Store a file in a temporary directory. - let tmp_dir = TempDir::new().unwrap(); - - let file_path = tmp_dir.path().join("test.bin"); - { - let mut file = File::create(&file_path).unwrap(); - file.write_all(b"hello world").unwrap(); - } + let (_tmp_dir, file_path) = create_temporary_file("test.bin"); // Set up mocks for the file upload. mock.mock_upload() @@ -94,16 +107,14 @@ async fn test_send_attachment() { { assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - assert_let!(TimelineItemContent::Message(msg) = item.content()); // Body is the caption, because there's both a caption and filename. assert_eq!(msg.body(), "caption"); - assert_let!(MessageType::File(file) = msg.msgtype()); - assert_eq!(file.filename(), "test.bin"); - assert_eq!(file.caption(), Some("caption")); + assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption"))); // The URI refers to the local cache. + assert_let!(MessageType::File(file) = msg.msgtype()); assert_let!(MediaSource::Plain(uri) = &file.source); assert!(uri.to_string().contains("localhost")); } @@ -116,12 +127,11 @@ async fn test_send_attachment() { Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next() ); assert_let!(TimelineItemContent::Message(msg) = item.content()); - - assert_let!(MessageType::File(file) = msg.msgtype()); - assert_eq!(file.filename(), "test.bin"); - assert_eq!(file.caption(), Some("caption")); + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption"))); // The URI now refers to the final MXC URI. + assert_let!(MessageType::File(file) = msg.msgtype()); assert_let!(MediaSource::Plain(uri) = &file.source); assert_eq!(uri.to_string(), "mxc://sdk.rs/media"); } @@ -139,3 +149,57 @@ async fn test_send_attachment() { // That's all, folks! assert!(timeline_stream.next().now_or_never().is_none()); } + +#[async_test] +async fn test_react_to_local_media() { + let mock = MatrixMockServer::new().await; + let client = mock.client_builder().build().await; + + // Disable the sending queue, to simulate offline mode. + client.send_queue().set_enabled(false).await; + + mock.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = mock.sync_joined_room(&client, room_id).await; + let timeline = room.timeline().await.unwrap(); + + let (items, mut timeline_stream) = + timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; + + assert!(items.is_empty()); + assert!(timeline_stream.next().now_or_never().is_none()); + + // Store a file in a temporary directory. + let (_tmp_dir, file_path) = create_temporary_file("test.bin"); + + // Queue sending of an attachment (no captions). + let config = AttachmentConfig::new(); + timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap(); + + let item_id = { + assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None)); + + // The item starts with no reactions. + assert!(item.reactions().is_empty()); + + item.identifier() + }; + + // Add a reaction to the file media event. + timeline.toggle_reaction(&item_id, "🤪").await.unwrap(); + + assert_let_timeout!(Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next()); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None)); + + // There's a reaction for the current user for the given emoji. + let reactions = item.reactions(); + let own_user_id = client.user_id().unwrap(); + reactions.get("🤪").unwrap().get(own_user_id).unwrap(); + + // That's all, folks! + assert!(timeline_stream.next().now_or_never().is_none()); +} diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index c248dc841e9..3ab04db900a 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -1270,7 +1270,17 @@ impl QueueStorage { // If the target event has been already sent, abort immediately. if !requests.iter().any(|item| item.transaction_id == transaction_id) { - return Ok(None); + // We didn't find it as a queued request; try to find it as a dependent queued + // request. + let dependent_requests = store.load_dependent_queued_requests(&self.room_id).await?; + if !dependent_requests + .into_iter() + .filter_map(|item| item.is_own_event().then_some(item.own_transaction_id)) + .any(|child_txn| *child_txn == *transaction_id) + { + // We didn't find it as either a request or a dependent request, abort. + return Ok(None); + } } // Record the dependent request. From 900cf5d071f7dca613996b4305a4834fbcd7e510 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 18 Nov 2024 12:23:35 +0100 Subject: [PATCH 558/979] room: create edits to add a caption to a media event --- crates/matrix-sdk-ui/src/timeline/mod.rs | 5 + crates/matrix-sdk/src/room/edit.rs | 316 ++++++++++++++++++--- crates/matrix-sdk/src/test_utils/events.rs | 49 +++- 3 files changed, 326 insertions(+), 44 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index b5bd98b10c2..93f738185e3 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -461,6 +461,7 @@ impl Timeline { .into()); } } + EditedContent::PollStart { new_content, .. } => { if matches!(item.content, TimelineItemContent::Poll(_)) { AnyMessageLikeEventContent::UnstablePollStart( @@ -476,6 +477,10 @@ impl Timeline { .into()); } } + + EditedContent::MediaCaption { caption: _, formatted_caption: _ } => { + todo!("bnjbvr you had one job"); + } }; if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? { diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index 0ed178ff62d..4f79fb56140 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -23,9 +23,13 @@ use ruma::{ ReplacementUnstablePollStartEventContent, UnstablePollStartContentBlock, UnstablePollStartEventContent, }, - room::message::{Relation, ReplacementMetadata, RoomMessageEventContentWithoutRelation}, + room::message::{ + FormattedBody, MessageType, Relation, ReplacementMetadata, RoomMessageEventContent, + RoomMessageEventContentWithoutRelation, + }, AnyMessageLikeEvent, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, - AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEvent, SyncMessageLikeEvent, + AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEvent, OriginalMessageLikeEvent, + SyncMessageLikeEvent, }, EventId, RoomId, UserId, }; @@ -39,6 +43,19 @@ pub enum EditedContent { /// The content is a `m.room.message`. RoomMessage(RoomMessageEventContentWithoutRelation), + /// Tweak a caption for a `m.room.message` that's a media. + MediaCaption { + /// New caption for the media. + /// + /// Set to `None` to remove an existing caption. + caption: Option, + + /// New formatted caption for the media. + /// + /// Set to `None` to remove an existing formatted caption. + formatted_caption: Option, + }, + /// The content is a new poll start. PollStart { /// New fallback text for the poll. @@ -53,6 +70,7 @@ impl std::fmt::Debug for EditedContent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::RoomMessage(_) => f.debug_tuple("RoomMessage").finish(), + Self::MediaCaption { .. } => f.debug_tuple("MediaCaption").finish(), Self::PollStart { .. } => f.debug_tuple("PollStart").finish(), } } @@ -133,6 +151,28 @@ impl<'a> EventSource for &'a Room { } } +/// Sets the caption of a media event content. +/// +/// Why a macro over a plain function: the event content types all differ from +/// each other, and it would require adding a trait and implementing it for all +/// event types instead of having this simple macro. +macro_rules! set_caption { + ($event:expr, $caption:expr) => { + let filename = $event.filename().to_owned(); + // As a reminder: + // - body and no filename set means the body is the filename + // - body and filename set means the body is the caption, and filename is the + // filename. + if let Some(caption) = $caption { + $event.filename = Some(filename); + $event.body = caption; + } else { + $event.filename = None; + $event.body = filename; + } + }; +} + async fn make_edit_event( source: S, room_id: &RoomId, @@ -167,47 +207,66 @@ async fn make_edit_event( }; let mentions = original.content.mentions.clone(); + let replied_to_original_room_msg = + extract_replied_to(source, room_id, original.content.relates_to).await; - // Do a best effort at finding the replied-to original event. - let replied_to_sync_timeline_event = - if let Some(Relation::Reply { in_reply_to }) = original.content.relates_to { - source - .get_event(&in_reply_to.event_id) - .await - .map_err(|err| { - warn!("couldn't fetch the replied-to event, when editing: {err}"); - err - }) - .ok() - } else { - None - }; - - let replied_to_original_room_msg = replied_to_sync_timeline_event - .and_then(|sync_timeline_event| { - sync_timeline_event - .raw() - .deserialize() - .map_err(|err| warn!("unable to deserialize replied-to event: {err}")) - .ok() - }) - .and_then(|event| { - if let AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage( - MessageLikeEvent::Original(original), - )) = event.into_full_event(room_id.to_owned()) - { - Some(original) - } else { - None - } + let replacement = new_content.make_replacement( + ReplacementMetadata::new(event_id.to_owned(), mentions), + replied_to_original_room_msg.as_ref(), + ); + + Ok(replacement.into()) + } + + EditedContent::MediaCaption { caption, formatted_caption } => { + // Handle edits of m.room.message. + let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original)) = + message_like_event + else { + return Err(EditError::IncompatibleEditType { + target: message_like_event.event_type().to_string(), + new_content: "caption for a media room message", }); + }; - Ok(new_content - .make_replacement( - ReplacementMetadata::new(event_id.to_owned(), mentions), - replied_to_original_room_msg.as_ref(), - ) - .into()) + let mentions = original.content.mentions.clone(); + let replied_to_original_room_msg = + extract_replied_to(source, room_id, original.content.relates_to.clone()).await; + + let mut prev_content = original.content; + + match &mut prev_content.msgtype { + MessageType::Audio(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + } + MessageType::File(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + } + MessageType::Image(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + } + MessageType::Video(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + } + + _ => { + return Err(EditError::IncompatibleEditType { + target: prev_content.msgtype.msgtype().to_owned(), + new_content: "caption for a media room message", + }) + } + } + + let replacement = prev_content.make_replacement( + ReplacementMetadata::new(event_id.to_owned(), mentions), + replied_to_original_room_msg.as_ref(), + ); + + Ok(replacement.into()) } EditedContent::PollStart { fallback_text, new_content } => { @@ -234,6 +293,45 @@ async fn make_edit_event( } } +/// Try to find the original replied-to event content, in a best-effort manner. +async fn extract_replied_to( + source: S, + room_id: &RoomId, + relates_to: Option>, +) -> Option> { + let replied_to_sync_timeline_event = if let Some(Relation::Reply { in_reply_to }) = relates_to { + source + .get_event(&in_reply_to.event_id) + .await + .map_err(|err| { + warn!("couldn't fetch the replied-to event, when editing: {err}"); + err + }) + .ok() + } else { + None + }; + + replied_to_sync_timeline_event + .and_then(|sync_timeline_event| { + sync_timeline_event + .raw() + .deserialize() + .map_err(|err| warn!("unable to deserialize replied-to event: {err}")) + .ok() + }) + .and_then(|event| { + if let AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage( + MessageLikeEvent::Original(original), + )) = event.into_full_event(room_id.to_owned()) + { + Some(original) + } else { + None + } + }) +} + #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -244,10 +342,10 @@ mod tests { use ruma::{ event_id, events::{ - room::message::{Relation, RoomMessageEventContentWithoutRelation}, + room::message::{MessageType, Relation, RoomMessageEventContentWithoutRelation}, AnyMessageLikeEventContent, AnySyncTimelineEvent, }, - room_id, + owned_mxc_uri, room_id, serde::Raw, user_id, EventId, OwnedEventId, }; @@ -374,6 +472,140 @@ mod tests { assert_eq!(repl.new_content.msgtype.body(), "the edit"); } + #[async_test] + async fn test_make_edit_caption_for_non_media_room_message() { + let event_id = event_id!("$1"); + let own_user_id = user_id!("@me:saucisse.bzh"); + + let mut cache = TestEventCache::default(); + let f = EventFactory::new(); + cache.events.insert( + event_id.to_owned(), + f.text_msg("hello world").event_id(event_id).sender(own_user_id).into(), + ); + + let room_id = room_id!("!galette:saucisse.bzh"); + + let err = make_edit_event( + cache, + room_id, + own_user_id, + event_id, + EditedContent::MediaCaption { caption: Some("yo".to_owned()), formatted_caption: None }, + ) + .await + .unwrap_err(); + + assert_let!(EditError::IncompatibleEditType { target, new_content } = err); + assert_eq!(target, "m.text"); + assert_eq!(new_content, "caption for a media room message"); + } + + #[async_test] + async fn test_add_caption_for_media() { + let event_id = event_id!("$1"); + let own_user_id = user_id!("@me:saucisse.bzh"); + + let filename = "rickroll.gif"; + + let mut cache = TestEventCache::default(); + let f = EventFactory::new(); + cache.events.insert( + event_id.to_owned(), + f.image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll")) + .event_id(event_id) + .sender(own_user_id) + .into(), + ); + + let room_id = room_id!("!galette:saucisse.bzh"); + + let edit_event = make_edit_event( + cache, + room_id, + own_user_id, + event_id, + EditedContent::MediaCaption { + caption: Some("Best joke ever".to_owned()), + formatted_caption: None, + }, + ) + .await + .unwrap(); + + assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event); + assert_let!(MessageType::Image(image) = msg.msgtype); + + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), Some("* Best joke ever")); // Fallback for a replacement 🤷 + assert!(image.formatted_caption().is_none()); + + assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to); + assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype); + assert_eq!(new_image.filename(), filename); + assert_eq!(new_image.caption(), Some("Best joke ever")); + assert!(new_image.formatted_caption().is_none()); + } + + #[async_test] + async fn test_remove_caption_for_media() { + let event_id = event_id!("$1"); + let own_user_id = user_id!("@me:saucisse.bzh"); + + let filename = "rickroll.gif"; + + let mut cache = TestEventCache::default(); + let f = EventFactory::new(); + + let event: SyncTimelineEvent = f + .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll")) + .caption(Some("caption".to_owned()), None) + .event_id(event_id) + .sender(own_user_id) + .into(); + + { + // Sanity checks. + let event = event.raw().deserialize().unwrap(); + assert_let!(AnySyncTimelineEvent::MessageLike(event) = event); + assert_let!( + AnyMessageLikeEventContent::RoomMessage(msg) = event.original_content().unwrap() + ); + assert_let!(MessageType::Image(image) = msg.msgtype); + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), Some("caption")); + assert!(image.formatted_caption().is_none()); + } + + cache.events.insert(event_id.to_owned(), event); + + let room_id = room_id!("!galette:saucisse.bzh"); + + let edit_event = make_edit_event( + cache, + room_id, + own_user_id, + event_id, + // Remove the caption by setting it to None. + EditedContent::MediaCaption { caption: None, formatted_caption: None }, + ) + .await + .unwrap(); + + assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event); + assert_let!(MessageType::Image(image) = msg.msgtype); + + assert_eq!(image.filename(), "* rickroll.gif"); // Fallback for a replacement 🤷 + assert!(image.caption().is_none()); + assert!(image.formatted_caption().is_none()); + + assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to); + assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype); + assert_eq!(new_image.filename(), "rickroll.gif"); + assert!(new_image.caption().is_none()); + assert!(new_image.formatted_caption().is_none()); + } + #[async_test] async fn test_make_edit_event_success_with_response() { let event_id = event_id!("$1"); diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/crates/matrix-sdk/src/test_utils/events.rs index 3ecaebb5b47..6f447a64bcc 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/crates/matrix-sdk/src/test_utils/events.rs @@ -34,13 +34,16 @@ use ruma::{ relation::{Annotation, InReplyTo, Replacement, Thread}, room::{ encrypted::{EncryptedEventScheme, RoomEncryptedEventContent}, - message::{Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, + message::{ + FormattedBody, ImageMessageEventContent, MessageType, Relation, + RoomMessageEventContent, RoomMessageEventContentWithoutRelation, + }, redaction::RoomRedactionEventContent, }, AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, EventContent, }, serde::Raw, - server_name, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, + server_name, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId, }; use serde::Serialize; @@ -232,6 +235,37 @@ impl EventBuilder { Some(Relation::Replacement(Replacement::new(edited_event_id.to_owned(), new_content))); self } + + /// Adds a caption to a media event. + /// + /// Will crash if the event isn't a media room message. + pub fn caption( + mut self, + caption: Option, + formatted_caption: Option, + ) -> Self { + match &mut self.content.msgtype { + MessageType::Image(image) => { + let filename = image.filename().to_owned(); + if let Some(caption) = caption { + image.body = caption; + image.filename = Some(filename); + } else { + image.body = filename; + image.filename = None; + } + image.formatted = formatted_caption; + } + + MessageType::Audio(_) | MessageType::Video(_) | MessageType::File(_) => { + unimplemented!(); + } + + _ => panic!("unexpected event type for a caption"), + } + + self + } } impl From> for Raw @@ -413,6 +447,17 @@ impl EventFactory { self.event(poll_end_content) } + /// Creates a plain (unencrypted) image event content referencing the given + /// MXC ID. + pub fn image( + &self, + filename: String, + url: OwnedMxcUri, + ) -> EventBuilder { + let image_event_content = ImageMessageEventContent::plain(filename, url); + self.event(RoomMessageEventContent::new(MessageType::Image(image_event_content))) + } + /// Set the next server timestamp. /// /// Timestamps will continue to increase by 1 (millisecond) from that value. From c4ff07124b7821123b7fb62f42dba34d423f9d77 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 18 Nov 2024 12:27:52 +0100 Subject: [PATCH 559/979] feat(ffi): allow editing a media caption from the FFI layer --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 517554daa3f..740a4f5aef4 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -545,6 +545,7 @@ impl Timeline { .await { Ok(()) => Ok(()), + Err(timeline::Error::EventNotInTimeline(_)) => { // If we couldn't edit, assume it was an (remote) event that wasn't in the // timeline, and try to edit it via the room itself. @@ -560,7 +561,8 @@ impl Timeline { room.send_queue().send(edit_event).await?; Ok(()) } - Err(err) => Err(err)?, + + Err(err) => Err(err.into()), } } @@ -1278,6 +1280,7 @@ impl From for ruma::api::client::receipt::create_receipt::v3::Recei #[derive(Clone, uniffi::Enum)] pub enum EditedContent { RoomMessage { content: Arc }, + MediaCaption { caption: Option, formatted_caption: Option }, PollStart { poll_data: PollData }, } @@ -1288,6 +1291,12 @@ impl TryFrom for SdkEditedContent { EditedContent::RoomMessage { content } => { Ok(SdkEditedContent::RoomMessage((*content).clone())) } + EditedContent::MediaCaption { caption, formatted_caption } => { + Ok(SdkEditedContent::MediaCaption { + caption, + formatted_caption: formatted_caption.map(Into::into), + }) + } EditedContent::PollStart { poll_data } => { let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?; Ok(SdkEditedContent::PollStart { From fa47af3dd68c0a19d06644b2eaccc5cc64ac53d5 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 18 Nov 2024 14:14:43 +0100 Subject: [PATCH 560/979] refactor!(base): rename `StateStore::update_dependent_queued_request` to `mark_dependent_queued_requests_as_ready` --- crates/matrix-sdk-base/src/store/integration_tests.rs | 2 +- crates/matrix-sdk-base/src/store/memory_store.rs | 2 +- crates/matrix-sdk-base/src/store/traits.rs | 10 +++++----- crates/matrix-sdk-indexeddb/src/state_store/mod.rs | 2 +- crates/matrix-sdk-sqlite/src/state_store.rs | 2 +- crates/matrix-sdk/src/send_queue.rs | 4 +++- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 26412fa8aca..03feadc8369 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1478,7 +1478,7 @@ impl StateStoreIntegrationTests for DynStateStore { // Update the event id. let event_id = owned_event_id!("$1"); let num_updated = self - .update_dependent_queued_request( + .mark_dependent_queued_requests_as_ready( room_id, &txn0, SentRequestKey::Event(event_id.clone()), diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 623fba571b9..844e87fe2a9 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -917,7 +917,7 @@ impl StateStore for MemoryStore { Ok(()) } - async fn update_dependent_queued_request( + async fn mark_dependent_queued_requests_as_ready( &self, room: &RoomId, parent_txn_id: &TransactionId, diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 04dad0e24bd..ccc9707cd1c 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -424,15 +424,15 @@ pub trait StateStore: AsyncTraitDeps { content: DependentQueuedRequestKind, ) -> Result<(), Self::Error>; - /// Update a set of dependent send queue requests with a key identifying the - /// homeserver's response, effectively marking them as ready. + /// Mark a set of dependent send queue requests as ready, using a key + /// identifying the homeserver's response. /// /// ⚠ Beware! There's no verification applied that the parent key type is /// compatible with the dependent event type. The invalid state may be /// lazily filtered out in `load_dependent_queued_requests`. /// /// Returns the number of updated requests. - async fn update_dependent_queued_request( + async fn mark_dependent_queued_requests_as_ready( &self, room_id: &RoomId, parent_txn_id: &TransactionId, @@ -709,14 +709,14 @@ impl StateStore for EraseStateStoreError { .map_err(Into::into) } - async fn update_dependent_queued_request( + async fn mark_dependent_queued_requests_as_ready( &self, room_id: &RoomId, parent_txn_id: &TransactionId, sent_parent_key: SentRequestKey, ) -> Result { self.0 - .update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key) + .mark_dependent_queued_requests_as_ready(room_id, parent_txn_id, sent_parent_key) .await .map_err(Into::into) } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 6e6193e4314..82fbc391869 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -1594,7 +1594,7 @@ impl_state_store!({ Ok(()) } - async fn update_dependent_queued_request( + async fn mark_dependent_queued_requests_as_ready( &self, room_id: &RoomId, parent_txn_id: &TransactionId, diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index b67b7c459c0..1996dac65e5 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -1924,7 +1924,7 @@ impl StateStore for SqliteStateStore { .await } - async fn update_dependent_queued_request( + async fn mark_dependent_queued_requests_as_ready( &self, room_id: &RoomId, parent_txn_id: &TransactionId, diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 3ab04db900a..c9a8d530145 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -1077,7 +1077,9 @@ impl QueueStorage { let store = client.store(); // Update all dependent requests. - store.update_dependent_queued_request(&self.room_id, transaction_id, parent_key).await?; + store + .mark_dependent_queued_requests_as_ready(&self.room_id, transaction_id, parent_key) + .await?; let removed = store.remove_send_queue_request(&self.room_id, transaction_id).await?; From 0080f17c1ff85fb6c8683e93665e209559bc08c8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 18 Nov 2024 14:28:19 +0100 Subject: [PATCH 561/979] feat(base): add a way to update a dependent send queue request --- .../src/store/integration_tests.rs | 56 +++++++++++++++++++ .../matrix-sdk-base/src/store/memory_store.rs | 17 ++++++ crates/matrix-sdk-base/src/store/traits.rs | 22 ++++++++ .../src/state_store/mod.rs | 42 ++++++++++++++ crates/matrix-sdk-sqlite/src/error.rs | 3 + crates/matrix-sdk-sqlite/src/state_store.rs | 33 +++++++++++ 6 files changed, 173 insertions(+) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 03feadc8369..c6b3b5a7c3c 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -90,6 +90,8 @@ pub trait StateStoreIntegrationTests { async fn test_send_queue_priority(&self); /// Test operations related to send queue dependents. async fn test_send_queue_dependents(&self); + /// Test an update to a send queue dependent request. + async fn test_update_send_queue_dependent(&self); /// Test saving/restoring server capabilities. async fn test_server_capabilities_saving(&self); } @@ -1548,6 +1550,54 @@ impl StateStoreIntegrationTests for DynStateStore { let dependents = self.load_dependent_queued_requests(room_id).await.unwrap(); assert_eq!(dependents.len(), 2); } + + async fn test_update_send_queue_dependent(&self) { + let room_id = room_id!("!test_send_queue_dependents:localhost"); + + let txn = TransactionId::new(); + + // Save a dependent redaction for an event. + let child_txn = ChildTransactionId::new(); + + self.save_dependent_queued_request( + room_id, + &txn, + child_txn.clone(), + DependentQueuedRequestKind::RedactEvent, + ) + .await + .unwrap(); + + // It worked. + let dependents = self.load_dependent_queued_requests(room_id).await.unwrap(); + assert_eq!(dependents.len(), 1); + assert_eq!(dependents[0].parent_transaction_id, txn); + assert_eq!(dependents[0].own_transaction_id, child_txn); + assert!(dependents[0].parent_key.is_none()); + assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent); + + // Make it a reaction, instead of a redaction. + self.update_dependent_queued_request( + room_id, + &child_txn, + DependentQueuedRequestKind::ReactEvent { key: "👍".to_owned() }, + ) + .await + .unwrap(); + + // It worked. + let dependents = self.load_dependent_queued_requests(room_id).await.unwrap(); + assert_eq!(dependents.len(), 1); + assert_eq!(dependents[0].parent_transaction_id, txn); + assert_eq!(dependents[0].own_transaction_id, child_txn); + assert!(dependents[0].parent_key.is_none()); + assert_matches!( + &dependents[0].kind, + DependentQueuedRequestKind::ReactEvent { key } => { + assert_eq!(key, "👍"); + } + ); + } } /// Macro building to allow your StateStore implementation to run the entire @@ -1706,6 +1756,12 @@ macro_rules! statestore_integration_tests { let store = get_store().await.expect("creating store failed").into_state_store(); store.test_send_queue_dependents().await; } + + #[async_test] + async fn test_update_send_queue_dependent() { + let store = get_store().await.expect("creating store failed").into_state_store(); + store.test_update_send_queue_dependent().await; + } } }; } diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 844e87fe2a9..2c8e1d84943 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -933,6 +933,23 @@ impl StateStore for MemoryStore { Ok(num_updated) } + async fn update_dependent_queued_request( + &self, + room: &RoomId, + own_transaction_id: &ChildTransactionId, + new_content: DependentQueuedRequestKind, + ) -> Result { + let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap(); + let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default(); + for d in dependents.iter_mut() { + if d.own_transaction_id == *own_transaction_id { + d.kind = new_content; + return Ok(true); + } + } + Ok(false) + } + async fn remove_dependent_queued_request( &self, room: &RoomId, diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index ccc9707cd1c..6e34f4fe263 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -439,6 +439,16 @@ pub trait StateStore: AsyncTraitDeps { sent_parent_key: SentRequestKey, ) -> Result; + /// Update a dependent send queue request with the new content. + /// + /// Returns true if the request was found and could be updated. + async fn update_dependent_queued_request( + &self, + room_id: &RoomId, + own_transaction_id: &ChildTransactionId, + new_content: DependentQueuedRequestKind, + ) -> Result; + /// Remove a specific dependent send queue request by id. /// /// Returns true if the dependent send queue request has been indeed @@ -735,6 +745,18 @@ impl StateStore for EraseStateStoreError { ) -> Result, Self::Error> { self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into) } + + async fn update_dependent_queued_request( + &self, + room_id: &RoomId, + own_transaction_id: &ChildTransactionId, + new_content: DependentQueuedRequestKind, + ) -> Result { + self.0 + .update_dependent_queued_request(room_id, own_transaction_id, new_content) + .await + .map_err(Into::into) + } } /// Convenience functionality for state stores. diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 82fbc391869..b8ca7442b27 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -1594,6 +1594,48 @@ impl_state_store!({ Ok(()) } + async fn update_dependent_queued_request( + &self, + room_id: &RoomId, + own_transaction_id: &ChildTransactionId, + new_content: DependentQueuedRequestKind, + ) -> Result { + let encoded_key = self.encode_key(keys::DEPENDENT_SEND_QUEUE, room_id); + + let tx = self.inner.transaction_on_one_with_mode( + keys::DEPENDENT_SEND_QUEUE, + IdbTransactionMode::Readwrite, + )?; + + let obj = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?; + + // We store an encoded vector of the dependent requests. + // Reload the previous vector for this room, or create an empty one. + let prev = obj.get(&encoded_key)?.await?; + + let mut prev = prev.map_or_else( + || Ok(Vec::new()), + |val| self.deserialize_value::>(&val), + )?; + + // Modify the dependent request, if found. + let mut found = false; + for entry in prev.iter_mut() { + if entry.own_transaction_id == *own_transaction_id { + found = true; + entry.kind = new_content; + break; + } + } + + if found { + obj.put_key_val(&encoded_key, &self.serialize_value(&prev)?)?; + tx.await.into_result()?; + } + + Ok(found) + } + async fn mark_dependent_queued_requests_as_ready( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-sqlite/src/error.rs b/crates/matrix-sdk-sqlite/src/error.rs index 4a1eb5be8d5..ddfac2fbfec 100644 --- a/crates/matrix-sdk-sqlite/src/error.rs +++ b/crates/matrix-sdk-sqlite/src/error.rs @@ -101,6 +101,9 @@ pub enum Error { #[error("Redaction failed: {0}")] Redaction(#[source] ruma::canonical_json::RedactionError), + + #[error("An update keyed by unique ID touched more than one entry")] + InconsistentUpdate, } macro_rules! impl_from { diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 1996dac65e5..36ff843cc71 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -1924,6 +1924,39 @@ impl StateStore for SqliteStateStore { .await } + async fn update_dependent_queued_request( + &self, + room_id: &RoomId, + own_transaction_id: &ChildTransactionId, + new_content: DependentQueuedRequestKind, + ) -> Result { + let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id); + let content = self.serialize_json(&new_content)?; + + // See comment in `save_send_queue_event`. + let own_txn_id = own_transaction_id.to_string(); + + let num_updated = self + .acquire() + .await? + .with_transaction(move |txn| { + txn.prepare_cached( + r#"UPDATE dependent_send_queue_events + SET content = ? + WHERE own_transaction_id = ? + AND room_id = ?"#, + )? + .execute((content, own_txn_id, room_id)) + }) + .await?; + + if num_updated > 1 { + return Err(Error::InconsistentUpdate); + } + + Ok(num_updated == 1) + } + async fn mark_dependent_queued_requests_as_ready( &self, room_id: &RoomId, From 9e45111d8baa126fb03ac832ed1b20e97dc27302 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 18 Nov 2024 14:29:53 +0100 Subject: [PATCH 562/979] feat(send queue): allow updating caption while the media is being sent --- crates/matrix-sdk-ui/src/timeline/mod.rs | 11 +- crates/matrix-sdk/src/room/edit.rs | 107 ++++++++++------- crates/matrix-sdk/src/send_queue.rs | 51 +++++++- crates/matrix-sdk/src/send_queue/upload.rs | 132 ++++++++++++++++++++- 4 files changed, 243 insertions(+), 58 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 93f738185e3..da5dcbfef78 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -478,8 +478,15 @@ impl Timeline { } } - EditedContent::MediaCaption { caption: _, formatted_caption: _ } => { - todo!("bnjbvr you had one job"); + EditedContent::MediaCaption { caption, formatted_caption } => { + if handle + .edit_media_caption(caption, formatted_caption) + .await + .map_err(RoomSendQueueError::StorageError)? + { + return Ok(()); + } + return Err(EditError::InvalidLocalEchoState.into()); } }; diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index 4f79fb56140..2c4e4e64c89 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -151,28 +151,6 @@ impl<'a> EventSource for &'a Room { } } -/// Sets the caption of a media event content. -/// -/// Why a macro over a plain function: the event content types all differ from -/// each other, and it would require adding a trait and implementing it for all -/// event types instead of having this simple macro. -macro_rules! set_caption { - ($event:expr, $caption:expr) => { - let filename = $event.filename().to_owned(); - // As a reminder: - // - body and no filename set means the body is the filename - // - body and filename set means the body is the caption, and filename is the - // filename. - if let Some(caption) = $caption { - $event.filename = Some(filename); - $event.body = caption; - } else { - $event.filename = None; - $event.body = filename; - } - }; -} - async fn make_edit_event( source: S, room_id: &RoomId, @@ -235,30 +213,11 @@ async fn make_edit_event( let mut prev_content = original.content; - match &mut prev_content.msgtype { - MessageType::Audio(event) => { - set_caption!(event, caption); - event.formatted = formatted_caption; - } - MessageType::File(event) => { - set_caption!(event, caption); - event.formatted = formatted_caption; - } - MessageType::Image(event) => { - set_caption!(event, caption); - event.formatted = formatted_caption; - } - MessageType::Video(event) => { - set_caption!(event, caption); - event.formatted = formatted_caption; - } - - _ => { - return Err(EditError::IncompatibleEditType { - target: prev_content.msgtype.msgtype().to_owned(), - new_content: "caption for a media room message", - }) - } + if !update_media_caption(&mut prev_content, caption, formatted_caption) { + return Err(EditError::IncompatibleEditType { + target: prev_content.msgtype.msgtype().to_owned(), + new_content: "caption for a media room message", + }); } let replacement = prev_content.make_replacement( @@ -293,6 +252,62 @@ async fn make_edit_event( } } +/// Sets the caption of a media event content. +/// +/// Why a macro over a plain function: the event content types all differ from +/// each other, and it would require adding a trait and implementing it for all +/// event types instead of having this simple macro. +macro_rules! set_caption { + ($event:expr, $caption:expr) => { + let filename = $event.filename().to_owned(); + // As a reminder: + // - body and no filename set means the body is the filename + // - body and filename set means the body is the caption, and filename is the + // filename. + if let Some(caption) = $caption { + $event.filename = Some(filename); + $event.body = caption; + } else { + $event.filename = None; + $event.body = filename; + } + }; +} + +/// Sets the caption of a [`RoomMessageEventContent`]. +/// +/// Returns true if the event represented a media event (and thus the captions +/// could be updated), false otherwise. +pub(crate) fn update_media_caption( + content: &mut RoomMessageEventContent, + caption: Option, + formatted_caption: Option, +) -> bool { + match &mut content.msgtype { + MessageType::Audio(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + true + } + MessageType::File(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + true + } + MessageType::Image(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + true + } + MessageType::Video(event) => { + set_caption!(event, caption); + event.formatted = formatted_caption; + true + } + _ => false, + } +} + /// Try to find the original replied-to event content, in a best-effort manner. async fn extract_replied_to( source: S, diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index c9a8d530145..1b9aec6d57c 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -155,7 +155,10 @@ use ruma::{ events::{ reaction::ReactionEventContent, relation::Annotation, - room::{message::RoomMessageEventContent, MediaSource}, + room::{ + message::{FormattedBody, RoomMessageEventContent}, + MediaSource, + }, AnyMessageLikeEventContent, EventContent as _, }, serde::Raw, @@ -1850,10 +1853,13 @@ pub enum RoomSendQueueStorageError { #[error("The client is shutting down.")] ClientShuttingDown, - /// An operation not implemented yet on a send handle. - // TODO: remove this - #[error("This operation is not implemented yet for media uploads")] + /// An operation not implemented on a send handle. + #[error("This operation is not implemented for media uploads")] OperationNotImplementedYet, + + /// Trying to edit a media caption for something that's not a media. + #[error("Can't edit a media caption when the underlying event isn't a media")] + InvalidMediaCaptionEdit, } /// Extra transaction IDs useful during an upload. @@ -1982,6 +1988,43 @@ impl SendHandle { .await } + /// Edits the content of a local echo with a media caption. + /// + /// Will fail if the event to be sent, represented by this send handle, + /// wasn't a media. + pub async fn edit_media_caption( + &self, + caption: Option, + formatted_caption: Option, + ) -> Result { + if let Some(new_content) = self + .room + .inner + .queue + .edit_media_caption(&self.transaction_id, caption, formatted_caption) + .await? + { + trace!("successful edit of media caption"); + + // Wake up the queue, in case the room was asleep before the edit. + self.room.inner.notifier.notify_one(); + + let new_content = SerializableEventContent::new(&new_content) + .map_err(RoomSendQueueStorageError::JsonSerialization)?; + + // Propagate a replaced update too. + let _ = self.room.inner.updates.send(RoomSendQueueUpdate::ReplacedLocalEvent { + transaction_id: self.transaction_id.clone(), + new_content, + }); + + Ok(true) + } else { + debug!("local echo doesn't exist anymore, can't edit media caption"); + Ok(false) + } + } + /// Unwedge a local echo identified by its transaction identifier and try to /// resend it. pub async fn unwedge(&self) -> Result<(), RoomSendQueueError> { diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index e363574458b..ea4dea09734 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -17,17 +17,20 @@ use matrix_sdk_base::{ media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, store::{ - ChildTransactionId, FinishUploadThumbnailInfo, QueuedRequestKind, SentMediaInfo, - SentRequestKey, SerializableEventContent, + ChildTransactionId, DependentQueuedRequestKind, FinishUploadThumbnailInfo, + QueuedRequestKind, SentMediaInfo, SentRequestKey, SerializableEventContent, }, RoomState, }; use mime::Mime; use ruma::{ assign, - events::room::{ - message::{MessageType, RoomMessageEventContent}, - MediaSource, ThumbnailInfo, + events::{ + room::{ + message::{FormattedBody, MessageType, RoomMessageEventContent}, + MediaSource, ThumbnailInfo, + }, + AnyMessageLikeEventContent, }, uint, OwnedMxcUri, OwnedTransactionId, TransactionId, UInt, }; @@ -36,6 +39,7 @@ use tracing::{debug, error, instrument, trace, warn, Span}; use super::{QueueStorage, RoomSendQueue, RoomSendQueueError}; use crate::{ attachment::AttachmentConfig, + room::edit::update_media_caption, send_queue::{ LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle, @@ -298,7 +302,6 @@ impl QueueStorage { let from_req = make_local_file_media_request(&file_upload_txn); trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store"); - let cache_store = client .event_cache_store() .lock() @@ -542,4 +545,121 @@ impl QueueStorage { debug!("successfully aborted!"); Ok(true) } + + #[instrument(skip(self, caption, formatted_caption))] + pub(super) async fn edit_media_caption( + &self, + txn: &TransactionId, + caption: Option, + formatted_caption: Option, + ) -> Result, RoomSendQueueStorageError> { + // This error will be popular here. + use RoomSendQueueStorageError::InvalidMediaCaptionEdit; + + let guard = self.store.lock().await; + let client = guard.client()?; + let store = client.store(); + + // The media event can be in one of three states: + // - still stored as a dependent request, + // - stored as a queued request, active (aka it's being sent). + // - stored as a queued request, not active yet (aka it's not being sent yet), + // + // We'll handle each of these cases one by one. + + { + // If the event can be found as a dependent event, update the captions, save it + // back into the database, and return early. + let dependent_requests = store.load_dependent_queued_requests(&self.room_id).await?; + + if let Some(found) = + dependent_requests.into_iter().find(|req| *req.own_transaction_id == *txn) + { + trace!("found the caption to edit in a dependent request"); + + let DependentQueuedRequestKind::FinishUpload { + mut local_echo, + file_upload, + thumbnail_info, + } = found.kind + else { + return Err(InvalidMediaCaptionEdit); + }; + + if !update_media_caption(&mut local_echo, caption, formatted_caption) { + return Err(InvalidMediaCaptionEdit); + } + + let new_dependent_request = DependentQueuedRequestKind::FinishUpload { + local_echo: local_echo.clone(), + file_upload, + thumbnail_info, + }; + store + .update_dependent_queued_request( + &self.room_id, + &found.own_transaction_id, + new_dependent_request, + ) + .await?; + + trace!("caption successfully updated"); + return Ok(Some(local_echo.into())); + } + } + + let requests = store.load_send_queue_requests(&self.room_id).await?; + let Some(found) = requests.into_iter().find(|req| req.transaction_id == *txn) else { + // Couldn't be found anymore, it's not possible to update captions. + return Ok(None); + }; + + trace!("found the caption to edit as a request"); + + let QueuedRequestKind::Event { content: serialized_content } = found.kind else { + return Err(InvalidMediaCaptionEdit); + }; + + let deserialized = serialized_content.deserialize()?; + let AnyMessageLikeEventContent::RoomMessage(mut content) = deserialized else { + return Err(InvalidMediaCaptionEdit); + }; + + if !update_media_caption(&mut content, caption, formatted_caption) { + return Err(InvalidMediaCaptionEdit); + } + + let any_content: AnyMessageLikeEventContent = content.into(); + let new_serialized = SerializableEventContent::new(&any_content.clone())?; + + // If the request is active (being sent), send a dependent request. + if let Some(being_sent) = guard.being_sent.as_ref() { + if being_sent.transaction_id == *txn { + // Record a dependent request to edit, and exit. + store + .save_dependent_queued_request( + &self.room_id, + txn, + ChildTransactionId::new(), + DependentQueuedRequestKind::EditEvent { new_content: new_serialized }, + ) + .await?; + + trace!("media event was being sent, pushed a dependent edit"); + return Ok(Some(any_content)); + } + } + + // The request is not active: edit the local echo. + store + .update_send_queue_request( + &self.room_id, + txn, + QueuedRequestKind::Event { content: new_serialized }, + ) + .await?; + + trace!("media event was not being sent, updated local echo"); + Ok(Some(any_content)) + } } From 60893d279765cb03bc4fa6a91d0696577e106a7a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 18 Nov 2024 16:27:28 +0100 Subject: [PATCH 563/979] test(send_queue): add tests for editing a caption while media not sent yet test(timeline): add an integration test for sending an attachment test(timeline): add tests for multiple caption edits and local reaction to a media upload --- .../tests/integration/send_queue.rs | 303 +++++++++++++++++- 1 file changed, 302 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 49fc6cd670f..eb737895c0d 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1,5 +1,6 @@ use std::{ops::Not as _, sync::Arc, time::Duration}; +use as_variant::as_variant; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, Thumbnail}, @@ -29,7 +30,7 @@ use ruma::{ }, AnyMessageLikeEventContent, EventContent as _, Mentions, }, - mxc_uri, owned_user_id, room_id, + mxc_uri, owned_mxc_uri, owned_user_id, room_id, serde::Raw, uint, MxcUri, OwnedEventId, OwnedTransactionId, TransactionId, }; @@ -2726,3 +2727,303 @@ async fn test_cancel_upload_while_sending_event() { // That's all, folks! assert!(watch.is_empty()); } + +#[async_test] +async fn test_update_caption_while_sending_media() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + // File upload will take a second. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(1)).set_body_json( + json!({ + "content_uri": "mxc://sdk.rs/media" + }), + )) + .mock_once() + .named("file upload") + .mount() + .await; + + // Sending of the media event will succeed. + mock.mock_room_send() + .ok(event_id!("$media")) + .mock_once() + .named("send event") + .mock_once() + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_no_thumbnail(&q).await; + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(local_content) = content.msgtype); + assert_eq!(local_content.filename(), filename); + + // We can edit the caption while the file is being uploaded. + let edited = upload_handle.edit_media_caption(Some("caption".to_owned()), None).await.unwrap(); + assert!(edited); + + { + let new_content = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(image) = new_content.msgtype); + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), Some("caption")); + assert!(image.formatted_caption().is_none()); + } + + // Then the media is uploaded. + sleep(Duration::from_secs(1)).await; + assert_update!(watch => uploaded { related_to = upload_txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + // Then the media event is updated with the MXC ID. + { + let edit_msg = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(image) = edit_msg.msgtype); + assert_let!(MediaSource::Plain(new_uri) = &image.source); + assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/media")); + + // Still has the new caption. + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), Some("caption")); + assert!(image.formatted_caption().is_none()); + } + + // Then the event is sent. + assert_update!(watch => sent { txn = upload_txn, }); + + // That's all, folks! + assert!(watch.is_empty()); +} + +#[async_test] +async fn test_update_caption_before_event_is_sent() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + // File upload will take a second. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(1)).set_body_json( + json!({ + "content_uri": "mxc://sdk.rs/media" + }), + )) + .mock_once() + .named("file upload") + .mount() + .await; + + // Sending of the media event will succeed. + mock.mock_room_send() + .ok(event_id!("$media")) + .mock_once() + .named("send event") + .mock_once() + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_no_thumbnail(&q).await; + + // Let the upload request start. + sleep(Duration::from_millis(300)).await; + + // Stop the send queue before upload is done. This will stall sending of the + // media event. + q.set_enabled(false); + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(local_content) = content.msgtype); + assert_eq!(local_content.filename(), filename); + + // Wait for the media to be uploaded. + sleep(Duration::from_secs(1)).await; + assert_update!(watch => uploaded { related_to = upload_txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + // The media event is updated with the remote MXC ID. + let mxc = { + let new_content = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(image) = new_content.msgtype); + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), None); + assert!(image.formatted_caption().is_none()); + + let mxc = as_variant!(image.source, MediaSource::Plain).unwrap(); + assert!(!mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); + mxc + }; + + assert!(watch.is_empty()); + + // We can edit the caption here. + let edited = upload_handle.edit_media_caption(Some("caption".to_owned()), None).await.unwrap(); + assert!(edited); + + // The media event is updated with the captions. + { + let edit_msg = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(image) = edit_msg.msgtype); + + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), Some("caption")); + assert!(image.formatted_caption().is_none()); + + // But kept the mxc. + let new_mxc = as_variant!(image.source, MediaSource::Plain).unwrap(); + assert_eq!(new_mxc, mxc); + } + + // Re-enable the send queue. + q.set_enabled(true); + + // Then the event is sent. + assert_update!(watch => sent { txn = upload_txn, }); + + // That's all, folks! + assert!(watch.is_empty()); +} + +#[async_test] +async fn test_update_caption_while_sending_media_event() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + // File upload will resolve immediately. + mock.mock_upload() + .ok(mxc_uri!("mxc://sdk.rs/media")) + .mock_once() + .named("file upload") + .mount() + .await; + + // Sending of the media event will take one second. + mock.mock_room_send() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(1)).set_body_json( + json!({ + "event_id": "$1" + }), + )) + .mock_once() + .named("send event") + .mock_once() + .mount() + .await; + + // There will be an edit event sent too; this one doesn't need to wait. + mock.mock_room_send() + .ok(event_id!("$edit")) + .mock_once() + .named("edit event") + .mock_once() + .mount() + .await; + + // The /event endpoint is used to retrieve the original event, during creation + // of the edit event. + mock.mock_room_event() + .room(room_id) + .ok(EventFactory::new() + .image("surprise.jpeg.exe".to_owned(), owned_mxc_uri!("mxc://sdk.rs/media")) + .sender(client.user_id().unwrap()) + .room(room_id) + .into_timeline()) + .expect(1) + .named("room_event") + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_no_thumbnail(&q).await; + + // See local echo. + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(local_content) = content.msgtype); + assert_eq!(local_content.filename(), filename); + + // Wait for the media to be uploaded. + assert_update!(watch => uploaded { related_to = upload_txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + // The media event is updated with the remote MXC ID. + let mxc = { + let new_content = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(image) = new_content.msgtype); + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), None); + assert!(image.formatted_caption().is_none()); + + let mxc = as_variant!(image.source, MediaSource::Plain).unwrap(); + assert!(!mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); + mxc + }; + + // We can edit the caption while the event is beint sent. + let edited = upload_handle.edit_media_caption(Some("caption".to_owned()), None).await.unwrap(); + assert!(edited); + + // The media event is updated with the captions. + { + let edit_msg = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(image) = edit_msg.msgtype); + + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), Some("caption")); + assert!(image.formatted_caption().is_none()); + + // But kept the mxc. + let new_mxc = as_variant!(image.source, MediaSource::Plain).unwrap(); + assert_eq!(new_mxc, mxc); + } + + // Then the event is sent. + sleep(Duration::from_secs(1)).await; + assert_update!(watch => sent { txn = upload_txn, }); + + // Then the edit event is set, with another transaction id we don't know about. + assert_update!(watch => sent {}); + + // That's all, folks! + assert!(watch.is_empty()); +} From 639833acf1e07b4a24647f98fce64bca1e635ec4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 16:22:01 +0100 Subject: [PATCH 564/979] task: move test_utils.rs to test_utils/mod.rs This is more in line with what we're doing in the SDK in general. --- crates/matrix-sdk/src/{test_utils.rs => test_utils/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/matrix-sdk/src/{test_utils.rs => test_utils/mod.rs} (100%) diff --git a/crates/matrix-sdk/src/test_utils.rs b/crates/matrix-sdk/src/test_utils/mod.rs similarity index 100% rename from crates/matrix-sdk/src/test_utils.rs rename to crates/matrix-sdk/src/test_utils/mod.rs From 03f0c3a0017169769c40276ad924b451dccd56cb Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 16:27:41 +0100 Subject: [PATCH 565/979] task: move the `MockClientBuilder` to its own mock file --- crates/matrix-sdk/src/test_utils/client.rs | 83 ++++++++++++++++++++++ crates/matrix-sdk/src/test_utils/mocks.rs | 69 ++---------------- crates/matrix-sdk/src/test_utils/mod.rs | 1 + 3 files changed, 88 insertions(+), 65 deletions(-) create mode 100644 crates/matrix-sdk/src/test_utils/client.rs diff --git a/crates/matrix-sdk/src/test_utils/client.rs b/crates/matrix-sdk/src/test_utils/client.rs new file mode 100644 index 00000000000..bfbbf539ef5 --- /dev/null +++ b/crates/matrix-sdk/src/test_utils/client.rs @@ -0,0 +1,83 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Augmented [`ClientBuilder`] that can set up an already logged-in user. + +use matrix_sdk_base::{store::StoreConfig, SessionMeta}; +use ruma::{api::MatrixVersion, device_id, user_id}; + +use crate::{ + config::RequestConfig, + matrix_auth::{MatrixSession, MatrixSessionTokens}, + Client, ClientBuilder, +}; + +/// An augmented [`ClientBuilder`] that also allows for handling session login. +#[allow(missing_debug_implementations)] +pub struct MockClientBuilder { + builder: ClientBuilder, + logged_in: bool, +} + +impl MockClientBuilder { + /// Create a new [`MockClientBuilder`] connected to the given homeserver, + /// using Matrix V1.12, and which will not attempt any network retry (by + /// default). + pub(crate) fn new(homeserver: String) -> Self { + let default_builder = Client::builder() + .homeserver_url(homeserver) + .server_versions([MatrixVersion::V1_12]) + .request_config(RequestConfig::new().disable_retry()); + + Self { builder: default_builder, logged_in: true } + } + + /// Doesn't log-in a user. + /// + /// Authenticated requests will fail if this is called. + pub fn unlogged(mut self) -> Self { + self.logged_in = false; + self + } + + /// Provides another [`StoreConfig`] for the underlying [`ClientBuilder`]. + pub fn store_config(mut self, store_config: StoreConfig) -> Self { + self.builder = self.builder.store_config(store_config); + self + } + + /// Finish building the client into the final [`Client`] instance. + pub async fn build(self) -> Client { + let client = self.builder.build().await.expect("building client failed"); + + if self.logged_in { + client + .matrix_auth() + .restore_session(MatrixSession { + meta: SessionMeta { + user_id: user_id!("@example:localhost").to_owned(), + device_id: device_id!("DEVICEID").to_owned(), + }, + tokens: MatrixSessionTokens { + access_token: "1234".to_owned(), + refresh_token: None, + }, + }) + .await + .unwrap(); + } + + client + } +} diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 9ca28630f43..53cdbcee5dc 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -22,15 +22,12 @@ use std::{ sync::{Arc, Mutex}, }; -use matrix_sdk_base::{deserialized_responses::TimelineEvent, store::StoreConfig, SessionMeta}; +use matrix_sdk_base::deserialized_responses::TimelineEvent; use matrix_sdk_test::{ test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, }; -use ruma::{ - api::MatrixVersion, device_id, directory::PublicRoomsChunk, user_id, MxcUri, OwnedEventId, - OwnedRoomId, RoomId, ServerName, -}; +use ruma::{directory::PublicRoomsChunk, MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName}; use serde::Deserialize; use serde_json::json; use wiremock::{ @@ -38,11 +35,8 @@ use wiremock::{ Mock, MockBuilder, MockGuard, MockServer, Request, Respond, ResponseTemplate, Times, }; -use crate::{ - config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, - Client, ClientBuilder, OwnedServerName, Room, -}; +use super::client::MockClientBuilder; +use crate::{Client, OwnedServerName, Room}; /// A [`wiremock`] [`MockServer`] along with useful methods to help mocking /// Matrix client-server API endpoints easily. @@ -1135,58 +1129,3 @@ impl<'a> MockEndpoint<'a, PublicRoomsEndpoint> { MatrixMock { server: self.server, mock } } } - -/// An augmented [`ClientBuilder`] that also allows for handling session login. -pub struct MockClientBuilder { - builder: ClientBuilder, - logged_in: bool, -} - -impl MockClientBuilder { - fn new(homeserver: String) -> Self { - let default_builder = Client::builder() - .homeserver_url(homeserver) - .server_versions([MatrixVersion::V1_12]) - .request_config(RequestConfig::new().disable_retry()); - - Self { builder: default_builder, logged_in: true } - } - - /// Doesn't log-in a user. - /// - /// Authenticated requests will fail if this is called. - pub fn unlogged(mut self) -> Self { - self.logged_in = false; - self - } - - /// Provides another [`StoreConfig`] for the underlying [`ClientBuilder`]. - pub fn store_config(mut self, store_config: StoreConfig) -> Self { - self.builder = self.builder.store_config(store_config); - self - } - - /// Finish building the client into the final [`Client`] instance. - pub async fn build(self) -> Client { - let client = self.builder.build().await.expect("building client failed"); - - if self.logged_in { - client - .matrix_auth() - .restore_session(MatrixSession { - meta: SessionMeta { - user_id: user_id!("@example:localhost").to_owned(), - device_id: device_id!("DEVICEID").to_owned(), - }, - tokens: MatrixSessionTokens { - access_token: "1234".to_owned(), - refresh_token: None, - }, - }) - .await - .unwrap(); - } - - client - } -} diff --git a/crates/matrix-sdk/src/test_utils/mod.rs b/crates/matrix-sdk/src/test_utils/mod.rs index 0a6996ad756..900a60af036 100644 --- a/crates/matrix-sdk/src/test_utils/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mod.rs @@ -14,6 +14,7 @@ use url::Url; pub mod events; +pub mod client; #[cfg(not(target_arch = "wasm32"))] pub mod mocks; From af3ce4b32b083c3a58b1705f8d3c854262aa62d7 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 16:53:08 +0100 Subject: [PATCH 566/979] task: remove the dependency from common to test The (matrix-sdk-)common crate used the (matrix-sdk-)test crate only to benefit from the `async_test` proc macro, which is conveniently defined in another crate. My goal is to make `EventFactory`, at this point in the commit history, defined in the main SDK crate, available in the test crate. `EventFactory` makes use of some types defined in common, so there's a circular dependency at the moment. To split this circular dependency, I've changed the common crate to depend on the test-macro crate directly; now the test crate can depend on the common crate, and everybody's happy. --- Cargo.lock | 1 + crates/matrix-sdk-common/Cargo.toml | 8 +++++++- crates/matrix-sdk-common/src/executor.rs | 2 +- crates/matrix-sdk-common/src/js_tracing.rs | 2 +- crates/matrix-sdk-common/src/store_locks.rs | 2 +- crates/matrix-sdk-common/src/timeout.rs | 2 +- crates/matrix-sdk-common/src/tracing_timer.rs | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 981239a578e..cd2a9412829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3001,6 +3001,7 @@ dependencies = [ "eyeball-im", "futures-core", "futures-util", + "getrandom", "gloo-timers", "imbl", "js-sys", diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index c55b7a39583..1d9b0ea5c3f 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -44,10 +44,16 @@ wasm-bindgen = "0.2.84" [dev-dependencies] assert_matches = { workspace = true } proptest = { version = "1.4.0", default-features = false, features = ["std"] } -matrix-sdk-test = { workspace = true } +matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" } wasm-bindgen-test = "0.3.33" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +# Enable the test macro. +tokio = { workspace = true, features = ["rt", "macros"] } + [target.'cfg(target_arch = "wasm32")'.dev-dependencies] +# Enable the JS feature for getrandom. +getrandom = { version = "0.2.6", default-features = false, features = ["js"] } js-sys = "0.3.64" [lints] diff --git a/crates/matrix-sdk-common/src/executor.rs b/crates/matrix-sdk-common/src/executor.rs index b1cb1a7bf13..dcc8fa60ce1 100644 --- a/crates/matrix-sdk-common/src/executor.rs +++ b/crates/matrix-sdk-common/src/executor.rs @@ -81,7 +81,7 @@ impl Future for JoinHandle { #[cfg(test)] mod tests { use assert_matches::assert_matches; - use matrix_sdk_test::async_test; + use matrix_sdk_test_macros::async_test; use super::spawn; diff --git a/crates/matrix-sdk-common/src/js_tracing.rs b/crates/matrix-sdk-common/src/js_tracing.rs index 730938a6b94..3668d8645d8 100644 --- a/crates/matrix-sdk-common/src/js_tracing.rs +++ b/crates/matrix-sdk-common/src/js_tracing.rs @@ -351,7 +351,7 @@ pub fn make_tracing_subscriber(logger: Option) -> JsLoggingSubscriber #[cfg(test)] pub(crate) mod tests { - use matrix_sdk_test::async_test; + use matrix_sdk_test_macros::async_test; use tracing::{debug, subscriber::with_default}; use wasm_bindgen::{JsCast, JsValue}; diff --git a/crates/matrix-sdk-common/src/store_locks.rs b/crates/matrix-sdk-common/src/store_locks.rs index 13350c1fa25..e31acc5ca0b 100644 --- a/crates/matrix-sdk-common/src/store_locks.rs +++ b/crates/matrix-sdk-common/src/store_locks.rs @@ -343,7 +343,7 @@ mod tests { }; use assert_matches::assert_matches; - use matrix_sdk_test::async_test; + use matrix_sdk_test_macros::async_test; use tokio::{ spawn, time::{sleep, Duration}, diff --git a/crates/matrix-sdk-common/src/timeout.rs b/crates/matrix-sdk-common/src/timeout.rs index 266c49e16b2..c56c1940687 100644 --- a/crates/matrix-sdk-common/src/timeout.rs +++ b/crates/matrix-sdk-common/src/timeout.rs @@ -62,7 +62,7 @@ where pub(crate) mod tests { use std::{future, time::Duration}; - use matrix_sdk_test::async_test; + use matrix_sdk_test_macros::async_test; use super::timeout; diff --git a/crates/matrix-sdk-common/src/tracing_timer.rs b/crates/matrix-sdk-common/src/tracing_timer.rs index 7ec5f35ad39..91ec4b9dc10 100644 --- a/crates/matrix-sdk-common/src/tracing_timer.rs +++ b/crates/matrix-sdk-common/src/tracing_timer.rs @@ -105,7 +105,7 @@ macro_rules! timer { #[cfg(test)] mod tests { #[cfg(not(target_arch = "wasm32"))] - #[matrix_sdk_test::async_test] + #[matrix_sdk_test_macros::async_test] async fn test_timer_name() { use tracing::{span, Level}; From 9a9730d59ece8dd9ed9ef4ea19bd0e7ad7a4101b Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 19 Nov 2024 16:53:50 +0100 Subject: [PATCH 567/979] task: move the `EventFactory` to the `matrix-sdk-test` crate This makes it available to the crypto crate, by lowering it into the local dependency tree. --- Cargo.lock | 4 +++- benchmarks/benches/room_bench.rs | 9 +++++---- crates/matrix-sdk-ui/src/timeline/event_item/mod.rs | 6 ++++-- crates/matrix-sdk-ui/src/timeline/tests/echo.rs | 7 ++----- crates/matrix-sdk-ui/src/timeline/tests/mod.rs | 5 +++-- crates/matrix-sdk-ui/src/timeline/tests/reactions.rs | 4 ++-- .../src/timeline/tests/read_receipts.rs | 3 +-- .../matrix-sdk-ui/tests/integration/timeline/echo.rs | 10 ++++------ .../matrix-sdk-ui/tests/integration/timeline/edit.rs | 7 +++---- .../tests/integration/timeline/focus_event.rs | 8 ++++---- .../tests/integration/timeline/media.rs | 6 ++---- .../matrix-sdk-ui/tests/integration/timeline/mod.rs | 5 ++--- .../tests/integration/timeline/pinned_event.rs | 12 ++++++------ .../tests/integration/timeline/reactions.rs | 6 ++---- .../tests/integration/timeline/replies.rs | 9 +++------ .../tests/integration/timeline/subscribe.rs | 9 +++------ crates/matrix-sdk/src/event_cache/deduplicator.rs | 2 +- crates/matrix-sdk/src/event_cache/mod.rs | 4 ++-- crates/matrix-sdk/src/event_cache/paginator.rs | 4 ++-- crates/matrix-sdk/src/event_cache/room/events.rs | 2 +- crates/matrix-sdk/src/event_cache/room/mod.rs | 4 ++-- crates/matrix-sdk/src/room/edit.rs | 4 ++-- crates/matrix-sdk/src/test_utils/mod.rs | 2 -- crates/matrix-sdk/tests/integration/event_cache.rs | 5 +++-- crates/matrix-sdk/tests/integration/room/common.rs | 11 ++++------- crates/matrix-sdk/tests/integration/room/joined.rs | 3 ++- crates/matrix-sdk/tests/integration/send_queue.rs | 10 +++++----- testing/matrix-sdk-test/Cargo.toml | 5 ++++- .../matrix-sdk-test/src/event_factory.rs | 10 ++++------ testing/matrix-sdk-test/src/lib.rs | 1 + 30 files changed, 82 insertions(+), 95 deletions(-) rename crates/matrix-sdk/src/test_utils/events.rs => testing/matrix-sdk-test/src/event_factory.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index cd2a9412829..b6be9f73202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3005,7 +3005,7 @@ dependencies = [ "gloo-timers", "imbl", "js-sys", - "matrix-sdk-test", + "matrix-sdk-test-macros", "proptest", "ruma", "serde", @@ -3275,9 +3275,11 @@ dependencies = [ name = "matrix-sdk-test" version = "0.7.0" dependencies = [ + "as_variant", "ctor", "getrandom", "http", + "matrix-sdk-common", "matrix-sdk-test-macros", "once_cell", "ruma", diff --git a/benchmarks/benches/room_bench.rs b/benchmarks/benches/room_bench.rs index 57342857285..feebf4019a5 100644 --- a/benchmarks/benches/room_bench.rs +++ b/benchmarks/benches/room_bench.rs @@ -2,15 +2,16 @@ use std::{sync::Arc, time::Duration}; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use matrix_sdk::{ - config::SyncSettings, - test_utils::{events::EventFactory, logged_in_client_with_server}, - utils::IntoRawStateEventContent, + config::SyncSettings, test_utils::logged_in_client_with_server, utils::IntoRawStateEventContent, }; use matrix_sdk_base::{ store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore, }; use matrix_sdk_sqlite::SqliteStateStore; -use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder}; +use matrix_sdk_test::{ + event_factory::EventFactory, EventBuilder, JoinedRoomBuilder, StateTestEvent, + SyncResponseBuilder, +}; use matrix_sdk_ui::{timeline::TimelineFocus, Timeline}; use ruma::{ api::client::membership::get_member_events, 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 6860d0cb600..b90613d53d0 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -726,12 +726,14 @@ impl ReactionsByKeyBySender { mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; - use matrix_sdk::test_utils::{events::EventFactory, logged_in_client}; + use matrix_sdk::test_utils::logged_in_client; use matrix_sdk_base::{ deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http, MinimalStateEvent, OriginalMinimalStateEvent, }; - use matrix_sdk_test::{async_test, sync_state_event, sync_timeline_event}; + use matrix_sdk_test::{ + async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event, + }; use ruma::{ event_id, events::{ diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index a5475b6eb06..b68ccedc5b1 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -16,12 +16,9 @@ use std::sync::Arc; use assert_matches::assert_matches; use eyeball_im::VectorDiff; -use matrix_sdk::{ - assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate, - test_utils::events::EventFactory, -}; +use matrix_sdk::{assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate}; use matrix_sdk_base::store::QueueWedgeError; -use matrix_sdk_test::{async_test, ALICE, BOB}; +use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB}; use ruma::{ event_id, events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 356a2ad2228..e212a82d6be 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -31,11 +31,12 @@ use matrix_sdk::{ event_cache::paginator::{PaginableRoom, PaginatorError}, room::{EventWithContextResponse, Messages, MessagesOptions}, send_queue::RoomSendQueueUpdate, - test_utils::events::EventFactory, BoxFuture, }; use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo, RoomState}; -use matrix_sdk_test::{EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID}; +use matrix_sdk_test::{ + event_factory::EventFactory, EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID, +}; use ruma::{ event_id, events::{ diff --git a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs index bb0a8ab51da..d1e92e4ca49 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs @@ -18,8 +18,8 @@ use assert_matches2::{assert_let, assert_matches}; use eyeball_im::VectorDiff; use futures_core::Stream; use futures_util::{FutureExt as _, StreamExt as _}; -use matrix_sdk::{deserialized_responses::SyncTimelineEvent, test_utils::events::EventFactory}; -use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB}; +use matrix_sdk::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_test::{async_test, event_factory::EventFactory, sync_timeline_event, ALICE, BOB}; use ruma::{ event_id, events::AnyMessageLikeEventContent, server_name, uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs index 6f0c2e02a17..2da7584f949 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs @@ -15,8 +15,7 @@ use std::sync::Arc; use eyeball_im::VectorDiff; -use matrix_sdk::test_utils::events::EventFactory; -use matrix_sdk_test::{async_test, ALICE, BOB, CAROL}; +use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB, CAROL}; use ruma::{ event_id, events::{ diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs index 97366d6e9a6..69db503f269 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs @@ -19,14 +19,12 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::{ - assert_next_matches_with_timeout, - config::SyncSettings, - executor::spawn, - ruma::MilliSecondsSinceUnixEpoch, - test_utils::{events::EventFactory, logged_in_client_with_server}, + assert_next_matches_with_timeout, config::SyncSettings, executor::spawn, + ruma::MilliSecondsSinceUnixEpoch, test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, + async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, + SyncResponseBuilder, }; use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent}; use ruma::{ diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 3d848cc2116..ddafabe5cdb 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -20,13 +20,12 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ - config::SyncSettings, - room::edit::EditedContent, - test_utils::{events::EventFactory, logged_in_client_with_server}, + config::SyncSettings, room::edit::EditedContent, test_utils::logged_in_client_with_server, Client, }; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, + async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, + SyncResponseBuilder, ALICE, BOB, }; use matrix_sdk_ui::{ timeline::{ diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs index fa47850034e..240586a7ad0 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs @@ -20,12 +20,12 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::{ - assert_next_matches_with_timeout, - config::SyncSettings, - test_utils::{events::EventFactory, logged_in_client_with_server}, + assert_next_matches_with_timeout, config::SyncSettings, + test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, + async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, + SyncResponseBuilder, ALICE, BOB, }; use matrix_sdk_ui::{timeline::TimelineFocus, Timeline}; use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id}; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs index a6065e5f3d9..e11ea3a8283 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -19,11 +19,9 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ - assert_let_timeout, - attachment::AttachmentConfig, - test_utils::{events::EventFactory, mocks::MatrixMockServer}, + assert_let_timeout, attachment::AttachmentConfig, test_utils::mocks::MatrixMockServer, }; -use matrix_sdk_test::{async_test, JoinedRoomBuilder, ALICE}; +use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE}; use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent}; use ruma::{ event_id, diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index a70f3a499ff..df1319c7066 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -19,12 +19,11 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::{ - assert_let_timeout, - config::SyncSettings, - test_utils::{events::EventFactory, logged_in_client_with_server}, + assert_let_timeout, config::SyncSettings, test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{ async_test, + event_factory::EventFactory, mocks::{mock_encryption_state, mock_redaction}, sync_timeline_event, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, SyncResponseBuilder, BOB, diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index b67661a73d0..044b8cc354d 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -3,14 +3,14 @@ use std::time::Duration; use assert_matches::assert_matches; use eyeball_im::VectorDiff; use matrix_sdk::{ - assert_next_matches_with_timeout, - config::SyncSettings, - sync::SyncResponse, - test_utils::{events::EventFactory, logged_in_client_with_server}, - Client, + assert_next_matches_with_timeout, config::SyncSettings, sync::SyncResponse, + test_utils::logged_in_client_with_server, Client, }; use matrix_sdk_base::deserialized_responses::TimelineEvent; -use matrix_sdk_test::{async_test, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, BOB}; +use matrix_sdk_test::{ + async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent, + SyncResponseBuilder, BOB, +}; use matrix_sdk_ui::{ timeline::{RoomExt, TimelineFocus, TimelineItemContent}, Timeline, diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs index 671750aea23..cbc8c04fe50 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs @@ -17,12 +17,10 @@ use std::{sync::Mutex, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use eyeball_im::VectorDiff; use futures_util::{FutureExt as _, StreamExt as _}; -use matrix_sdk::{ - assert_next_matches_with_timeout, - test_utils::{events::EventFactory, logged_in_client_with_server}, -}; +use matrix_sdk::{assert_next_matches_with_timeout, test_utils::logged_in_client_with_server}; use matrix_sdk_test::{ async_test, + event_factory::EventFactory, mocks::{mock_encryption_state, mock_redaction}, JoinedRoomBuilder, SyncResponseBuilder, ALICE, }; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs index 08b9cd2a638..580bdc92860 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs @@ -4,14 +4,11 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; -use matrix_sdk::{ - config::SyncSettings, - test_utils::{events::EventFactory, logged_in_client_with_server}, -}; +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; use matrix_sdk_base::timeout::timeout; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, - ALICE, BOB, CAROL, + async_test, event_factory::EventFactory, mocks::mock_encryption_state, EventBuilder, + JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, CAROL, }; use matrix_sdk_ui::timeline::{ Error as TimelineError, EventSendState, RoomExt, TimelineDetails, TimelineItemContent, diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs index 4a11af83569..4989d51c3b2 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs @@ -18,13 +18,10 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::{pin_mut, StreamExt}; -use matrix_sdk::{ - config::SyncSettings, - test_utils::{events::EventFactory, logged_in_client_with_server}, -}; +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, sync_timeline_event, EventBuilder, - GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, + async_test, event_factory::EventFactory, mocks::mock_encryption_state, sync_timeline_event, + EventBuilder, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, }; use matrix_sdk_ui::timeline::{RoomExt, TimelineDetails, TimelineItemContent}; use ruma::{ diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs index 57d4126afef..9a6e8d933eb 100644 --- a/crates/matrix-sdk/src/event_cache/deduplicator.rs +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -142,10 +142,10 @@ pub enum Decoration { mod tests { use assert_matches2::{assert_let, assert_matches}; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_test::event_factory::EventFactory; use ruma::{owned_event_id, user_id, EventId}; use super::*; - use crate::test_utils::events::EventFactory; fn sync_timeline_event(event_id: &EventId) -> SyncTimelineEvent { EventFactory::new() diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 8f0c2f19212..035b6b0f631 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -502,12 +502,12 @@ mod tests { use assert_matches::assert_matches; use futures_util::FutureExt as _; use matrix_sdk_base::sync::{JoinedRoomUpdate, RoomUpdates, Timeline}; - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, event_factory::EventFactory}; use ruma::{event_id, room_id, serde::Raw, user_id}; use serde_json::json; use super::{EventCacheError, RoomEventCacheUpdate}; - use crate::test_utils::{assert_event_matches_msg, events::EventFactory, logged_in_client}; + use crate::test_utils::{assert_event_matches_msg, logged_in_client}; #[async_test] async fn test_must_explicitly_subscribe() { diff --git a/crates/matrix-sdk/src/event_cache/paginator.rs b/crates/matrix-sdk/src/event_cache/paginator.rs index 259074d2e88..280d033c50e 100644 --- a/crates/matrix-sdk/src/event_cache/paginator.rs +++ b/crates/matrix-sdk/src/event_cache/paginator.rs @@ -559,7 +559,7 @@ mod tests { use futures_core::Future; use futures_util::FutureExt as _; use matrix_sdk_base::deserialized_responses::TimelineEvent; - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, event_factory::EventFactory}; use once_cell::sync::Lazy; use ruma::{api::Direction, event_id, room_id, uint, user_id, EventId, RoomId, UInt, UserId}; use tokio::{ @@ -572,7 +572,7 @@ mod tests { use crate::{ event_cache::paginator::Paginator, room::{EventWithContextResponse, Messages, MessagesOptions}, - test_utils::{assert_event_matches_msg, events::EventFactory}, + test_utils::assert_event_matches_msg, }; #[derive(Clone)] diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index d9168bdca63..50813c3f0e1 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -316,10 +316,10 @@ impl RoomEvents { #[cfg(test)] mod tests { use assert_matches2::assert_let; + use matrix_sdk_test::event_factory::EventFactory; use ruma::{user_id, EventId, OwnedEventId}; use super::*; - use crate::test_utils::events::EventFactory; macro_rules! assert_events_eq { ( $events_iterator:expr, [ $( ( $event_id:ident at ( $chunk_identifier:literal, $index:literal ) ) ),* $(,)? ] ) => { diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 7a3e8476869..7b007547016 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -559,14 +559,14 @@ impl RoomEventCacheState { #[cfg(test)] mod tests { use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, event_factory::EventFactory}; use ruma::{ event_id, events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation}, room_id, user_id, RoomId, }; - use crate::test_utils::{events::EventFactory, logged_in_client}; + use crate::test_utils::logged_in_client; #[async_test] async fn test_event_with_redaction_relation() { diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index 2c4e4e64c89..fcb3533559f 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -353,7 +353,7 @@ mod tests { use assert_matches2::{assert_let, assert_matches}; use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, event_factory::EventFactory}; use ruma::{ event_id, events::{ @@ -367,7 +367,7 @@ mod tests { use serde_json::json; use super::{make_edit_event, EditError, EventSource}; - use crate::{room::edit::EditedContent, test_utils::events::EventFactory}; + use crate::room::edit::EditedContent; #[derive(Default)] struct TestEventCache { diff --git a/crates/matrix-sdk/src/test_utils/mod.rs b/crates/matrix-sdk/src/test_utils/mod.rs index 900a60af036..649e0501639 100644 --- a/crates/matrix-sdk/src/test_utils/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mod.rs @@ -12,8 +12,6 @@ use ruma::{ }; use url::Url; -pub mod events; - pub mod client; #[cfg(not(target_arch = "wasm32"))] pub mod mocks; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 40c03c67d79..f6fc42dd2ad 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -6,10 +6,11 @@ use matrix_sdk::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, - test_utils::{assert_event_matches_msg, events::EventFactory, logged_in_client_with_server}, + test_utils::{assert_event_matches_msg, logged_in_client_with_server}, }; use matrix_sdk_test::{ - async_test, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, + async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, + SyncResponseBuilder, }; use ruma::{event_id, events::AnyTimelineEvent, room_id, serde::Raw, user_id}; use serde_json::json; diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index 33efb813eac..f525f497436 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -2,14 +2,11 @@ use std::{iter, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use js_int::uint; -use matrix_sdk::{ - config::SyncSettings, room::RoomMember, test_utils::events::EventFactory, RoomDisplayName, - RoomMemberships, -}; +use matrix_sdk::{config::SyncSettings, room::RoomMember, RoomDisplayName, RoomMemberships}; use matrix_sdk_test::{ - async_test, bulk_room_members, sync_state_event, sync_timeline_event, test_json, - GlobalAccountDataTestEvent, JoinedRoomBuilder, LeftRoomBuilder, StateTestEvent, - SyncResponseBuilder, BOB, DEFAULT_TEST_ROOM_ID, + async_test, bulk_room_members, event_factory::EventFactory, sync_state_event, + sync_timeline_event, test_json, GlobalAccountDataTestEvent, JoinedRoomBuilder, LeftRoomBuilder, + StateTestEvent, SyncResponseBuilder, BOB, DEFAULT_TEST_ROOM_ID, }; use ruma::{ event_id, diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 440d1a9030d..322b0269488 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -7,11 +7,12 @@ use futures_util::future::join_all; use matrix_sdk::{ config::SyncSettings, room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, - test_utils::{events::EventFactory, mocks::MatrixMockServer}, + test_utils::mocks::MatrixMockServer, }; use matrix_sdk_base::RoomState; use matrix_sdk_test::{ async_test, + event_factory::EventFactory, mocks::{mock_encryption_state, mock_redaction}, test_json::{self, sync::CUSTOM_ROOM_POWER_LEVELS}, EphemeralTestEvent, GlobalAccountDataTestEvent, JoinedRoomBuilder, StateTestEvent, diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index eb737895c0d..a0c2c9e412b 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -10,13 +10,13 @@ use matrix_sdk::{ LocalEcho, LocalEchoContent, RoomSendQueue, RoomSendQueueError, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle, }, - test_utils::{ - events::EventFactory, - mocks::{MatrixMock, MatrixMockServer}, - }, + test_utils::mocks::{MatrixMock, MatrixMockServer}, Client, MemoryStore, }; -use matrix_sdk_test::{async_test, InvitedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder}; +use matrix_sdk_test::{ + async_test, event_factory::EventFactory, InvitedRoomBuilder, KnockedRoomBuilder, + LeftRoomBuilder, +}; use ruma::{ event_id, events::{ diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index 7c1aa7391dd..d18256db996 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -16,10 +16,13 @@ test = false doctest = false [dependencies] +as_variant = { workspace = true } http = { workspace = true } +matrix-sdk-common = { path = "../../crates/matrix-sdk-common" } matrix-sdk-test-macros = { version = "0.7.0", path = "../matrix-sdk-test-macros" } once_cell = { workspace = true } -ruma = { workspace = true, features = ["rand"] } +# Enabling the unstable feature for polls support. +ruma = { workspace = true, features = ["rand", "unstable-msc3381"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/matrix-sdk/src/test_utils/events.rs b/testing/matrix-sdk-test/src/event_factory.rs similarity index 97% rename from crates/matrix-sdk/src/test_utils/events.rs rename to testing/matrix-sdk-test/src/event_factory.rs index 6f447a64bcc..1a8b8dbe8df 100644 --- a/crates/matrix-sdk/src/test_utils/events.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -17,8 +17,9 @@ use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; use as_variant::as_variant; -use matrix_sdk_base::deserialized_responses::{SyncTimelineEvent, TimelineEvent}; -use matrix_sdk_common::deserialized_responses::UnableToDecryptReason; +use matrix_sdk_common::deserialized_responses::{ + SyncTimelineEvent, TimelineEvent, UnableToDecryptInfo, UnableToDecryptReason, +}; use ruma::{ events::{ message::TextContentBlock, @@ -200,10 +201,7 @@ impl EventBuilder { SyncTimelineEvent::new_utd_event( self.into(), - crate::deserialized_responses::UnableToDecryptInfo { - session_id, - reason: UnableToDecryptReason::MissingMegolmSession, - }, + UnableToDecryptInfo { session_id, reason: UnableToDecryptReason::MissingMegolmSession }, ) } } diff --git a/testing/matrix-sdk-test/src/lib.rs b/testing/matrix-sdk-test/src/lib.rs index 1b06e142d56..5a3c5d721e8 100644 --- a/testing/matrix-sdk-test/src/lib.rs +++ b/testing/matrix-sdk-test/src/lib.rs @@ -114,6 +114,7 @@ mod event_builder; #[cfg(not(target_arch = "wasm32"))] pub mod mocks; +pub mod event_factory; pub mod notification_settings; mod sync_builder; pub mod test_json; From 1f563c964c1551500db839793dad9745cfb10da7 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 20 Nov 2024 13:07:04 +0100 Subject: [PATCH 568/979] task: add manual Sync impl for VerificationCache to avoid overflowing evaluation requirements --- crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-crypto/Cargo.toml | 5 +++++ crates/matrix-sdk-crypto/src/verification/cache.rs | 13 +++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 65a3246d62e..c312798fb85 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -30,7 +30,7 @@ uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"] # Private feature, see # https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory # details. -test-send-sync = [] +test-send-sync = ["matrix-sdk-crypto?/test-send-sync"] # "message-ids" feature doesn't do anything and is deprecated. message-ids = [] diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 12830bb60ca..de18989829e 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -23,6 +23,11 @@ experimental-algorithms = [] uniffi = ["dep:uniffi"] _disable-minimum-rotation-period-ms = [] +# Private feature, see +# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory +# details. +test-send-sync = [] + # "message-ids" feature doesn't do anything and is deprecated. message-ids = [] diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index c847abbad3a..8f770ba9cb2 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -33,6 +33,19 @@ pub struct VerificationCache { inner: Arc, } +// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. +#[cfg(not(feature = "test-send-sync"))] +unsafe impl Sync for VerificationCache {} + +#[cfg(feature = "test-send-sync")] +#[test] +// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. +fn test_send_sync_for_room() { + fn assert_send_sync() {} + + assert_send_sync::(); +} + #[derive(Debug, Default)] struct VerificationCacheInner { verification: StdRwLock>>, From e5ca44bb04bccbc64472fab22a3549b98d471244 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Nov 2024 10:24:29 +0100 Subject: [PATCH 569/979] feat(base): Add `EventCacheStore::handle_linked_chunk_updates`. This patch adds the `handle_linked_chunk_updates` method on the `EventCacheStore` trait. Part of https://github.com/matrix-org/matrix-rust-sdk/issues/3280. --- .../src/event_cache/store/memory_store.rs | 15 +++++++++++-- .../src/event_cache/store/traits.rs | 22 +++++++++++++++++-- .../matrix-sdk-common/src/linked_chunk/mod.rs | 4 ++-- .../src/linked_chunk/updates.rs | 5 +++++ .../src/event_cache_store.rs | 10 ++++++++- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 1b5debbccee..bd764cad0db 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -16,12 +16,16 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti use async_trait::async_trait; use matrix_sdk_common::{ - ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, + linked_chunk::Update, ring_buffer::RingBuffer, + store_locks::memory_store_helper::try_take_leased_lock, }; use ruma::{MxcUri, OwnedMxcUri}; use super::{EventCacheStore, EventCacheStoreError, Result}; -use crate::media::{MediaRequestParameters, UniqueKey as _}; +use crate::{ + event_cache::{Event, Gap}, + media::{MediaRequestParameters, UniqueKey as _}, +}; /// In-memory, non-persistent implementation of the `EventCacheStore`. /// @@ -66,6 +70,13 @@ impl EventCacheStore for MemoryStore { Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) } + async fn handle_linked_chunk_updates( + &self, + _updates: &[Update], + ) -> Result<(), Self::Error> { + todo!() + } + async fn add_media_content( &self, request: &MediaRequestParameters, diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index e52ad8b8b2e..dde80830973 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -15,11 +15,14 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; -use matrix_sdk_common::AsyncTraitDeps; +use matrix_sdk_common::{linked_chunk::Update, AsyncTraitDeps}; use ruma::MxcUri; use super::EventCacheStoreError; -use crate::media::MediaRequestParameters; +use crate::{ + event_cache::{Event, Gap}, + media::MediaRequestParameters, +}; /// An abstract trait that can be used to implement different store backends /// for the event cache of the SDK. @@ -37,6 +40,14 @@ pub trait EventCacheStore: AsyncTraitDeps { holder: &str, ) -> Result; + /// An [`Update`] reflects an operation that has happened inside a linked + /// chunk. The linked chunk is used by the event cache to store the events + /// in-memory. This method aims at forwarding this update inside this store. + async fn handle_linked_chunk_updates( + &self, + updates: &[Update], + ) -> Result<(), Self::Error>; + /// Add a media file's content in the media store. /// /// # Arguments @@ -131,6 +142,13 @@ impl EventCacheStore for EraseEventCacheStoreError { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into) } + async fn handle_linked_chunk_updates( + &self, + updates: &[Update], + ) -> Result<(), Self::Error> { + self.0.handle_linked_chunk_updates(updates).await.map_err(Into::into) + } + async fn add_media_content( &self, request: &MediaRequestParameters, diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index b2894f86469..eeba29be302 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -103,8 +103,8 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, }; -use as_vector::*; -use updates::*; +pub use as_vector::*; +pub use updates::*; /// Errors of [`LinkedChunk`]. #[derive(thiserror::Error, Debug)] diff --git a/crates/matrix-sdk-common/src/linked_chunk/updates.rs b/crates/matrix-sdk-common/src/linked_chunk/updates.rs index 5a143b94486..97d21231ed1 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/updates.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/updates.rs @@ -29,6 +29,9 @@ use super::{ChunkIdentifier, Position}; /// /// These updates are useful to store a `LinkedChunk` in another form of /// storage, like a database or something similar. +/// +/// [`LinkedChunk`]: super::LinkedChunk +/// [`LinkedChunk::updates`]: super::LinkedChunk::updates #[derive(Debug, Clone, PartialEq)] pub enum Update { /// A new chunk of kind Items has been created. @@ -101,6 +104,8 @@ pub enum Update { /// A collection of [`Update`]s that can be observed. /// /// Get a value for this type with [`LinkedChunk::updates`]. +/// +/// [`LinkedChunk::updates`]: super::LinkedChunk::updates #[derive(Debug)] pub struct ObservableUpdates { pub(super) inner: Arc>>, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index c5a21fb0a6b..dc74999e227 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -3,7 +3,8 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc}; use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ - event_cache::store::EventCacheStore, + event_cache::{store::EventCacheStore, Event, Gap}, + linked_chunk::Update, media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; @@ -182,6 +183,13 @@ impl EventCacheStore for SqliteEventCacheStore { Ok(num_touched == 1) } + async fn handle_linked_chunk_updates( + &self, + _updates: &[Update], + ) -> Result<(), Self::Error> { + todo!() + } + async fn add_media_content( &self, request: &MediaRequestParameters, From bf86b168d71c40384221f931137f4a68e802df59 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:25:32 +0000 Subject: [PATCH 570/979] feat(timeline): mark media events as editable in the timeline (#4303) This PR makes audio, file, image and video messages be editable so that the timeline signals when it is possible to use #4277/#4300 for editing captions. --- crates/matrix-sdk-ui/src/timeline/event_item/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 b90613d53d0..a10f9fc95a6 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -368,7 +368,15 @@ impl EventTimelineItem { match self.content() { TimelineItemContent::Message(message) => { - matches!(message.msgtype(), MessageType::Text(_) | MessageType::Emote(_)) + matches!( + message.msgtype(), + MessageType::Text(_) + | MessageType::Emote(_) + | MessageType::Audio(_) + | MessageType::File(_) + | MessageType::Image(_) + | MessageType::Video(_) + ) } TimelineItemContent::Poll(poll) => { poll.response_data.is_empty() && poll.end_event_timestamp.is_none() From d2f255d6134150519ae04d269897b6ce3fcc97a4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 20 Nov 2024 15:15:47 +0100 Subject: [PATCH 571/979] feat(ffi): add a new function helper to create a caption edit It has the same semantics used when creating a caption (if no formatted caption is provided, assume a provided caption is markdown and use that as the formatted caption). --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 740a4f5aef4..af449c112a0 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1308,6 +1308,23 @@ impl TryFrom for SdkEditedContent { } } +/// Create a caption edit. +/// +/// If no `formatted_caption` is provided, then it's assumed the `caption` +/// represents valid Markdown that can be used as the formatted caption. +#[matrix_sdk_ffi_macros::export] +fn create_caption_edit( + caption: Option, + formatted_caption: Option, +) -> EditedContent { + let formatted_caption = + formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); + EditedContent::MediaCaption { + caption, + formatted_caption: formatted_caption.as_ref().map(Into::into), + } +} + /// Wrapper to retrieve some timeline item info lazily. #[derive(Clone, uniffi::Object)] pub struct LazyTimelineItemProvider(Arc); From bc70f3c0515a0b5b13b33e63df36425ccb10b13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 21 Nov 2024 13:22:53 +0100 Subject: [PATCH 572/979] refactor: Clean up the Room::compute_display_name() method --- crates/matrix-sdk-base/src/rooms/normal.rs | 133 +++++++++++++-------- 1 file changed, 82 insertions(+), 51 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 64e4b59f2fa..d80da192610 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -573,63 +573,61 @@ impl Room { /// /// [spec]: pub async fn compute_display_name(&self) -> StoreResult { - let update_cache = |new_val: RoomDisplayName| { - self.inner.update_if(|info| { - if info.cached_display_name.as_ref() != Some(&new_val) { - info.cached_display_name = Some(new_val.clone()); - true - } else { - false - } - }); - new_val - }; + enum DisplayNameOrSummary { + Summary(RoomSummary), + DisplayName(RoomDisplayName), + } - let summary = { + let display_name_or_summary = { let inner = self.inner.read(); - if let Some(name) = inner.name() { - let name = name.trim().to_owned(); - drop(inner); // drop the lock on `self.inner` to avoid deadlocking in `update_cache`. - return Ok(update_cache(RoomDisplayName::Named(name))); + match (inner.name(), inner.canonical_alias()) { + (Some(name), _) => { + let name = RoomDisplayName::Named(name.trim().to_owned()); + DisplayNameOrSummary::DisplayName(name) + } + (None, Some(alias)) => { + let name = RoomDisplayName::Aliased(alias.alias().trim().to_owned()); + DisplayNameOrSummary::DisplayName(name) + } + // We can't directly compute the display name from the summary here because Rust + // thinks that the `inner` lock is still held even if we explicitly call `drop()` + // on it. So we introduced the DisplayNameOrSummary type and do the computation in + // two steps. + (None, None) => DisplayNameOrSummary::Summary(inner.summary.clone()), } + }; - if let Some(alias) = inner.canonical_alias() { - let alias = alias.alias().trim().to_owned(); - drop(inner); // See above comment. - return Ok(update_cache(RoomDisplayName::Aliased(alias))); + let display_name = match display_name_or_summary { + DisplayNameOrSummary::Summary(summary) => { + self.compute_display_name_from_summary(summary).await? } - - inner.summary.clone() + DisplayNameOrSummary::DisplayName(display_name) => display_name, }; - // From here, use some heroes to compute the room's name. - let own_user_id = self.own_user_id().as_str(); - - let (heroes, num_joined_invited_guess) = if !summary.room_heroes.is_empty() { - let mut names = Vec::with_capacity(summary.room_heroes.len()); - for hero in &summary.room_heroes { - if hero.user_id == own_user_id { - continue; - } - if let Some(display_name) = &hero.display_name { - names.push(display_name.clone()); - continue; - } - match self.get_member(&hero.user_id).await { - Ok(Some(member)) => { - names.push(member.name().to_owned()); - } - Ok(None) => { - warn!("Ignoring hero, no member info for {}", hero.user_id); - } - Err(error) => { - warn!("Ignoring hero, error getting member: {}", error); - } - } + // Update the cached display name before we return the newly computed value. + self.inner.update_if(|info| { + if info.cached_display_name.as_ref() != Some(&display_name) { + info.cached_display_name = Some(display_name.clone()); + true + } else { + false } + }); - (names, None) + Ok(display_name) + } + + /// Compute a [`RoomDisplayName`] from the given [`RoomSummary`]. + async fn compute_display_name_from_summary( + &self, + summary: RoomSummary, + ) -> StoreResult { + let summary_member_count = summary.joined_member_count + summary.invited_member_count; + + let (heroes, num_joined_invited_guess) = if !summary.room_heroes.is_empty() { + let heroes = self.extract_heroes(&summary.room_heroes).await?; + (heroes, None) } else { let (heroes, num_joined_invited) = self.compute_summary().await?; (heroes, Some(num_joined_invited)) @@ -639,7 +637,7 @@ impl Room { // when we were invited we don't have a proper summary, we have to do best // guessing heroes.len() as u64 + 1 - } else if summary.joined_member_count == 0 && summary.invited_member_count == 0 { + } else if summary_member_count == 0 { if let Some(num_joined_invited) = num_joined_invited_guess { num_joined_invited } else { @@ -649,7 +647,7 @@ impl Room { .len() as u64 } } else { - summary.joined_member_count + summary.invited_member_count + summary_member_count }; debug!( @@ -660,10 +658,43 @@ impl Room { "Calculating name for a room based on heroes", ); - Ok(update_cache(compute_display_name_from_heroes( + let display_name = compute_display_name_from_heroes( num_joined_invited, heroes.iter().map(|hero| hero.as_str()).collect(), - ))) + ); + + Ok(display_name) + } + + /// Extract and collect the display names of the room heroes from a + /// [`RoomSummary`]. + /// + /// Returns the display names as a list of strings. + async fn extract_heroes(&self, heroes: &[RoomHero]) -> StoreResult> { + let own_user_id = self.own_user_id().as_str(); + + let mut names = Vec::with_capacity(heroes.len()); + let heroes = heroes.iter().filter(|hero| hero.user_id != own_user_id); + + for hero in heroes { + if let Some(display_name) = &hero.display_name { + names.push(display_name.clone()); + } else { + match self.get_member(&hero.user_id).await { + Ok(Some(member)) => { + names.push(member.name().to_owned()); + } + Ok(None) => { + warn!("Ignoring hero, no member info for {}", hero.user_id); + } + Err(error) => { + warn!("Ignoring hero, error getting member: {}", error); + } + } + } + } + + Ok(names) } /// Compute the room summary with the data present in the store. From 48fbda844fbccbbded9fb3a4fec099ffd5ac6129 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 21 Nov 2024 11:03:54 +0100 Subject: [PATCH 573/979] fix(oidc): make sure we keep track of an ongoing OIDC refresh up to the end There's a lock making sure we're not doing multiple refreshes of an OIDC token at the same time. Unfortunately, this lock could be dropped, if the task spawned by the inner function was detached. The lock must be held throughout the entire detached task's lifetime, which this refactoring ensures, by setting the lock's result after calling the inner function. --- crates/matrix-sdk/src/authentication/mod.rs | 4 +- crates/matrix-sdk/src/client/builder/mod.rs | 2 +- crates/matrix-sdk/src/oidc/mod.rs | 190 ++++++++++---------- 3 files changed, 103 insertions(+), 93 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/mod.rs b/crates/matrix-sdk/src/authentication/mod.rs index e7d06352d0f..81df8af3bed 100644 --- a/crates/matrix-sdk/src/authentication/mod.rs +++ b/crates/matrix-sdk/src/authentication/mod.rs @@ -17,6 +17,8 @@ // TODO:(pixlwave) Move AuthenticationService from the FFI into this module. // TODO:(poljar) Move the oidc and matrix_auth modules under this module. +use std::sync::Arc; + use as_variant::as_variant; use matrix_sdk_base::SessionMeta; use tokio::sync::{broadcast, Mutex, OnceCell}; @@ -58,7 +60,7 @@ pub(crate) struct AuthCtx { pub(crate) handle_refresh_tokens: bool, /// Lock making sure we're only doing one token refresh at a time. - pub(crate) refresh_token_lock: Mutex>, + pub(crate) refresh_token_lock: Arc>>, /// Session change publisher. Allows the subscriber to handle changes to the /// session such as logging out when the access token is invalid or diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index f64ca277730..86b189a1f38 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -521,7 +521,7 @@ impl ClientBuilder { let auth_ctx = Arc::new(AuthCtx { handle_refresh_tokens: self.handle_refresh_tokens, - refresh_token_lock: Mutex::new(Ok(())), + refresh_token_lock: Arc::new(Mutex::new(Ok(()))), session_change_sender: broadcast::Sender::new(1), auth_data: OnceCell::default(), reload_session_callback: OnceCell::default(), diff --git a/crates/matrix-sdk/src/oidc/mod.rs b/crates/matrix-sdk/src/oidc/mod.rs index ab686b23a96..aba26c6f7c5 100644 --- a/crates/matrix-sdk/src/oidc/mod.rs +++ b/crates/matrix-sdk/src/oidc/mod.rs @@ -1292,88 +1292,64 @@ impl Oidc { } async fn refresh_access_token_inner( - &self, + self, refresh_token: String, + provider_metadata: VerifiedProviderMetadata, + credentials: ClientCredentials, + client_metadata: VerifiedClientMetadata, latest_id_token: Option>, - lock: Option, + cross_process_lock: Option, ) -> Result<(), OidcError> { - // Do not interrupt refresh access token requests and processing, by detaching - // the request sending and response processing. - - let provider_metadata = self.provider_metadata().await?; - - let this = self.clone(); - let data = self.data().ok_or(OidcError::NotAuthenticated)?; - let credentials = data.credentials.clone(); - let metadata = data.metadata.clone(); - - spawn(async move { - trace!( - "Token refresh: attempting to refresh with refresh_token {:x}", - hash_str(&refresh_token) - ); + trace!( + "Token refresh: attempting to refresh with refresh_token {:x}", + hash_str(&refresh_token) + ); - match this - .backend - .refresh_access_token( - provider_metadata, - credentials, - &metadata, - refresh_token.clone(), - latest_id_token.clone(), - ) - .await - .map_err(OidcError::from) - { - Ok(new_tokens) => { - trace!( - "Token refresh: new refresh_token: {} / access_token: {:x}", - new_tokens - .refresh_token - .as_deref() - .map(|token| format!("{:x}", hash_str(token))) - .unwrap_or_else(|| "".to_owned()), - hash_str(&new_tokens.access_token) - ); + let new_tokens = self + .backend + .refresh_access_token( + provider_metadata, + credentials, + &client_metadata, + refresh_token.clone(), + latest_id_token.clone(), + ) + .await + .map_err(OidcError::from)?; + + trace!( + "Token refresh: new refresh_token: {} / access_token: {:x}", + new_tokens + .refresh_token + .as_deref() + .map(|token| format!("{:x}", hash_str(token))) + .unwrap_or_else(|| "".to_owned()), + hash_str(&new_tokens.access_token) + ); - let tokens = OidcSessionTokens { - access_token: new_tokens.access_token, - refresh_token: new_tokens.refresh_token.clone().or(Some(refresh_token)), - latest_id_token, - }; - - this.set_session_tokens(tokens.clone()); - - // Call the save_session_callback if set, while the optional lock is being held. - if let Some(save_session_callback) = - this.client.inner.auth_ctx.save_session_callback.get() - { - // Satisfies the save_session_callback invariant: set_session_tokens has - // been called just above. - if let Err(err) = save_session_callback(this.client.clone()) { - error!("when saving session after refresh: {err}"); - } - } + let tokens = OidcSessionTokens { + access_token: new_tokens.access_token, + refresh_token: new_tokens.refresh_token.clone().or(Some(refresh_token)), + latest_id_token, + }; - if let Some(mut lock) = lock { - lock.save_in_memory_and_db(&tokens).await?; - } + self.set_session_tokens(tokens.clone()); - _ = this - .client - .inner - .auth_ctx - .session_change_sender - .send(SessionChange::TokensRefreshed); + // Call the save_session_callback if set, while the optional lock is being held. + if let Some(save_session_callback) = self.client.inner.auth_ctx.save_session_callback.get() + { + // Satisfies the save_session_callback invariant: set_session_tokens has + // been called just above. + if let Err(err) = save_session_callback(self.client.clone()) { + error!("when saving session after refresh: {err}"); + } + } - Ok(()) - } + if let Some(mut lock) = cross_process_lock { + lock.save_in_memory_and_db(&tokens).await?; + } - Err(err) => Err(err), - } - }) - .await - .expect("joining")?; + _ = self.client.inner.auth_ctx.session_change_sender.send(SessionChange::TokensRefreshed); Ok(()) } @@ -1393,10 +1369,6 @@ impl Oidc { /// /// [`ClientBuilder::handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens() pub async fn refresh_access_token(&self) -> Result<(), RefreshTokenError> { - let client = &self.client; - - let refresh_status_lock = client.inner.auth_ctx.refresh_token_lock.try_lock(); - macro_rules! fail { ($lock:expr, $err:expr) => { let error = $err; @@ -1405,6 +1377,10 @@ impl Oidc { }; } + let client = &self.client; + + let refresh_status_lock = client.inner.auth_ctx.refresh_token_lock.clone().try_lock_owned(); + let Ok(mut refresh_status_guard) = refresh_status_lock else { // There's already a request to refresh happening in the same process. Wait for // it to finish. @@ -1446,22 +1422,54 @@ impl Oidc { fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired); }; - match self - .refresh_access_token_inner( - refresh_token, - session_tokens.latest_id_token, - cross_process_guard, - ) - .await - { - Ok(()) => { - *refresh_status_guard = Ok(()); - Ok(()) + let provider_metadata = match self.provider_metadata().await { + Ok(metadata) => metadata, + Err(err) => { + let err = Arc::new(err); + fail!(refresh_status_guard, RefreshTokenError::Oidc(err)); } - Err(error) => { - fail!(refresh_status_guard, RefreshTokenError::Oidc(error.into())); + }; + + let Some(auth_data) = self.data() else { + fail!( + refresh_status_guard, + RefreshTokenError::Oidc(Arc::new(OidcError::NotAuthenticated)) + ); + }; + + let credentials = auth_data.credentials.clone(); + let client_metadata = auth_data.metadata.clone(); + + // Do not interrupt refresh access token requests and processing, by detaching + // the request sending and response processing. + // Make sure to keep the `refresh_status_guard` during the entire processing. + + let this = self.clone(); + + spawn(async move { + match this + .refresh_access_token_inner( + refresh_token, + provider_metadata, + credentials, + client_metadata, + session_tokens.latest_id_token, + cross_process_guard, + ) + .await + { + Ok(()) => { + *refresh_status_guard = Ok(()); + Ok(()) + } + Err(err) => { + let err = RefreshTokenError::Oidc(Arc::new(err)); + fail!(refresh_status_guard, err); + } } - } + }) + .await + .expect("joining") } /// Log out from the currently authenticated session. From 6b0987385e2ce055920d198b59c61201b8e2fa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 21 Nov 2024 17:01:50 +0100 Subject: [PATCH 574/979] refactor(room_preview): make `RoomPreview` use the local known data only for joined rooms When instantiating a room preview, previously it would try to just check if the room exists locally either as joined, invited, knocked, left, etc., and then retrieve the info we cached about it. While this seems fine for most cases, it turns out for non-joined rooms, the info we have locally will **always** be the one we received when the invite/knock/leave action took place and it'll never be updated, so we may have the case where we knock into a room, never receive a response, someone changes the join rule of the room to something else and we'll think about this room as a 'request to join' room until we clear the local cache. To prevent that, we can only use the local data for joined rooms, which are constantly updated, and try to use the room summary API and other fallbacks for the rest, even if they're rooms known to us. --- crates/matrix-sdk/src/client/mod.rs | 14 ++++++++--- crates/matrix-sdk/src/room_preview.rs | 7 +++--- .../tests/integration/room_preview.rs | 25 +++++++++++++++++-- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 9be1fcd7b5c..a16603c028f 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -1142,8 +1142,8 @@ impl Client { self.base_client().get_room(room_id).map(|room| Room::new(self.clone(), room)) } - /// Gets the preview of a room, whether the current user knows it (because - /// they've joined/left/been invited to it) or not. + /// Gets the preview of a room, whether the current user has joined it or + /// not. pub async fn get_room_preview( &self, room_or_alias_id: &RoomOrAliasId, @@ -1155,10 +1155,16 @@ impl Client { }; if let Some(room) = self.get_room(&room_id) { - return Ok(RoomPreview::from_known(&room).await); + // The cached data can only be trusted if the room is joined: for invite and + // knock rooms, no updates will be received for the rooms after the invite/knock + // action took place so we may have very out to date data for important fields + // such as `join_rule` + if room.state() == RoomState::Joined { + return Ok(RoomPreview::from_joined(&room).await); + } } - RoomPreview::from_unknown(self, room_id, room_or_alias_id, via).await + RoomPreview::from_not_joined(self, room_id, room_or_alias_id, via).await } /// Resolve a room alias to a room id and a list of servers which know diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 9238b9eeb74..198d335ef66 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -124,9 +124,8 @@ impl RoomPreview { } } - /// Create a room preview from a known room (i.e. one we've been invited to, - /// we've joined or we've left). - pub(crate) async fn from_known(room: &Room) -> Self { + /// Create a room preview from a known room we've joined. + pub(crate) async fn from_joined(room: &Room) -> Self { let is_direct = room.is_direct().await.ok(); let display_name = room.compute_display_name().await.ok().map(|name| name.to_string()); @@ -142,7 +141,7 @@ impl RoomPreview { } #[instrument(skip(client))] - pub(crate) async fn from_unknown( + pub(crate) async fn from_not_joined( client: &Client, room_id: OwnedRoomId, room_or_alias_id: &RoomOrAliasId, diff --git a/crates/matrix-sdk/tests/integration/room_preview.rs b/crates/matrix-sdk/tests/integration/room_preview.rs index 1d78439c1c9..85e887e2e26 100644 --- a/crates/matrix-sdk/tests/integration/room_preview.rs +++ b/crates/matrix-sdk/tests/integration/room_preview.rs @@ -9,7 +9,9 @@ use matrix_sdk_test::{ }; #[cfg(feature = "experimental-sliding-sync")] use ruma::{api::client::sync::sync_events::v5::response::Hero, assign}; -use ruma::{owned_user_id, room_id, space::SpaceRoomJoinRule, RoomId}; +use ruma::{ + events::room::member::MembershipState, owned_user_id, room_id, space::SpaceRoomJoinRule, RoomId, +}; use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, @@ -30,6 +32,14 @@ async fn test_room_preview_leave_invited() { client.sync_once(SyncSettings::default()).await.unwrap(); server.reset().await; + mock_unknown_summary( + room_id, + None, + SpaceRoomJoinRule::Knock, + Some(MembershipState::Invite), + &server, + ) + .await; mock_leave(room_id, &server).await; let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); @@ -52,6 +62,14 @@ async fn test_room_preview_leave_knocked() { client.sync_once(SyncSettings::default()).await.unwrap(); server.reset().await; + mock_unknown_summary( + room_id, + None, + SpaceRoomJoinRule::Knock, + Some(MembershipState::Knock), + &server, + ) + .await; mock_leave(room_id, &server).await; let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); @@ -91,7 +109,7 @@ async fn test_room_preview_leave_unknown_room_fails() { let (client, server) = logged_in_client_with_server().await; let room_id = room_id!("!room:localhost"); - mock_unknown_summary(room_id, None, SpaceRoomJoinRule::Knock, &server).await; + mock_unknown_summary(room_id, None, SpaceRoomJoinRule::Knock, None, &server).await; let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); assert!(room_preview.state.is_none()); @@ -142,6 +160,7 @@ async fn mock_unknown_summary( room_id: &RoomId, alias: Option, join_rule: SpaceRoomJoinRule, + membership: Option, server: &MockServer, ) { let body = if let Some(alias) = alias { @@ -152,6 +171,7 @@ async fn mock_unknown_summary( "num_joined_members": 1, "world_readable": true, "join_rule": join_rule, + "membership": membership, }) } else { json!({ @@ -160,6 +180,7 @@ async fn mock_unknown_summary( "num_joined_members": 1, "world_readable": true, "join_rule": join_rule, + "membership": membership, }) }; Mock::given(method("GET")) From fa93daabd2cdcf62a05c8eefd7da9116809dd062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 22 Nov 2024 09:52:30 +0100 Subject: [PATCH 575/979] feat(ffi): Add `RoomInfo::join_rule` field to bindings Breaking-Change: Add `RoomInfo::join_rule` field, remove `RoomInfo::is_public` in the FFI crate, as they contain the same info. --- bindings/matrix-sdk-ffi/src/client.rs | 73 +++++++++++++++++++++--- bindings/matrix-sdk-ffi/src/room_info.rs | 12 +++- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 47012ebaffc..0a50f9716fc 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -55,7 +55,12 @@ use ruma::{ }, events::{ ignored_user_list::IgnoredUserListEventContent, - room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent}, + room::{ + join_rules::{ + AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent, + }, + power_levels::RoomPowerLevelsEventContent, + }, GlobalAccountDataEventType, }, push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat}, @@ -1917,9 +1922,13 @@ pub enum AllowRule { /// Only a member of the `room_id` Room can join the one this rule is used /// in. RoomMembership { room_id: String }, + + /// A custom allow rule implementation, containing its JSON representation + /// as a `String`. + Custom { json: String }, } -impl TryFrom for ruma::events::room::join_rules::JoinRule { +impl TryFrom for RumaJoinRule { type Error = ClientError; fn try_from(value: JoinRule) -> Result { @@ -1929,11 +1938,11 @@ impl TryFrom for ruma::events::room::join_rules::JoinRule { JoinRule::Knock => Ok(Self::Knock), JoinRule::Private => Ok(Self::Private), JoinRule::Restricted { rules } => { - let rules = allow_rules_from(rules)?; + let rules = ruma_allow_rules_from_ffi(rules)?; Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules))) } JoinRule::KnockRestricted { rules } => { - let rules = allow_rules_from(rules)?; + let rules = ruma_allow_rules_from_ffi(rules)?; Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules))) } JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?), @@ -1941,12 +1950,10 @@ impl TryFrom for ruma::events::room::join_rules::JoinRule { } } -fn allow_rules_from( - value: Vec, -) -> Result, ClientError> { +fn ruma_allow_rules_from_ffi(value: Vec) -> Result, ClientError> { let mut ret = Vec::with_capacity(value.len()); for rule in value { - let rule: Result = rule.try_into(); + let rule: Result = rule.try_into(); match rule { Ok(rule) => ret.push(rule), Err(error) => return Err(error), @@ -1955,7 +1962,7 @@ fn allow_rules_from( Ok(ret) } -impl TryFrom for ruma::events::room::join_rules::AllowRule { +impl TryFrom for RumaAllowRule { type Error = ClientError; fn try_from(value: AllowRule) -> Result { @@ -1966,6 +1973,54 @@ impl TryFrom for ruma::events::room::join_rules::AllowRule { room_id, ))) } + AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))), + } + } +} + +impl TryFrom for JoinRule { + type Error = String; + fn try_from(value: RumaJoinRule) -> Result { + match value { + RumaJoinRule::Knock => Ok(JoinRule::Knock), + RumaJoinRule::Public => Ok(JoinRule::Public), + RumaJoinRule::Private => Ok(JoinRule::Private), + RumaJoinRule::KnockRestricted(restricted) => { + let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::, + Self::Error, + >>( + )?; + Ok(JoinRule::KnockRestricted { rules }) + } + RumaJoinRule::Restricted(restricted) => { + let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::, + Self::Error, + >>( + )?; + Ok(JoinRule::Restricted { rules }) + } + RumaJoinRule::Invite => Ok(JoinRule::Invite), + RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }), + _ => Err(format!("Unknown JoinRule: {:?}", value)), + } + } +} + +impl TryFrom for AllowRule { + type Error = String; + fn try_from(value: RumaAllowRule) -> Result { + match value { + RumaAllowRule::RoomMembership(membership) => { + Ok(AllowRule::RoomMembership { room_id: membership.room_id.to_string() }) + } + RumaAllowRule::_Custom(repr) => { + let json = serde_json::to_string(&repr) + .map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?; + Ok(Self::Custom { json }) + } + _ => Err(format!("Invalid AllowRule: {:?}", value)), } } } diff --git a/bindings/matrix-sdk-ffi/src/room_info.rs b/bindings/matrix-sdk-ffi/src/room_info.rs index 96de331fd10..1b0f27e7114 100644 --- a/bindings/matrix-sdk-ffi/src/room_info.rs +++ b/bindings/matrix-sdk-ffi/src/room_info.rs @@ -1,8 +1,10 @@ use std::collections::HashMap; use matrix_sdk::RoomState; +use tracing::warn; use crate::{ + client::JoinRule, notification_settings::RoomNotificationMode, room::{Membership, RoomHero}, room_member::RoomMember, @@ -54,8 +56,10 @@ pub struct RoomInfo { /// Events causing mentions/highlights for the user, according to their /// notification settings. num_unread_mentions: u64, - /// The currently pinned event ids + /// The currently pinned event ids. pinned_event_ids: Vec, + /// The join rule for this room, if known. + join_rule: Option, } impl RoomInfo { @@ -70,6 +74,11 @@ impl RoomInfo { let pinned_event_ids = room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect(); + let join_rule = room.join_rule().try_into(); + if let Err(e) = &join_rule { + warn!("Failed to parse join rule: {:?}", e); + } + Ok(Self { id: room.room_id().to_string(), creator: room.creator().as_ref().map(ToString::to_string), @@ -118,6 +127,7 @@ impl RoomInfo { num_unread_notifications: room.num_unread_notifications(), num_unread_mentions: room.num_unread_mentions(), pinned_event_ids, + join_rule: join_rule.ok(), }) } } From 38a15afc9c9e26df57eb4815810cb16bfb819d00 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Fri, 22 Nov 2024 17:17:43 +0100 Subject: [PATCH 576/979] build (apple): add dynamic type to debug package --- bindings/apple/Debug-Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/apple/Debug-Package.swift b/bindings/apple/Debug-Package.swift index c98d2b068c4..ab927e4f7e0 100644 --- a/bindings/apple/Debug-Package.swift +++ b/bindings/apple/Debug-Package.swift @@ -13,6 +13,7 @@ let package = Package( ], products: [ .library(name: "MatrixRustSDK", + type: .dynamic, targets: ["MatrixRustSDK"]), ], targets: [ From ddd737e4d8f489aac9cea013e7f3fd9e85aec52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 28 Jul 2023 16:38:19 +0200 Subject: [PATCH 577/979] docs: Add a tutorial to the crypto crate Changelog: Add a tutorial describing how to add end-to-end encryption support to an existing library. --- Cargo.lock | 1 + Cargo.toml | 1 + crates/matrix-sdk-crypto/Cargo.toml | 1 + crates/matrix-sdk-crypto/src/lib.rs | 943 ++++++++++++++++++++++++++++ crates/matrix-sdk/Cargo.toml | 2 +- 5 files changed, 947 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b6be9f73202..a71427865f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3027,6 +3027,7 @@ version = "0.8.0" dependencies = [ "aes", "anyhow", + "aquamarine", "as_variant", "assert_matches", "assert_matches2", diff --git a/Cargo.toml b/Cargo.toml index 4f076807b33..63687ba8f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ rust-version = "1.76" [workspace.dependencies] anyhow = "1.0.68" +aquamarine = "0.6.0" assert-json-diff = "2" assert_matches = "1.5.0" assert_matches2 = "0.1.1" diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index de18989829e..e97c18ca6c5 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -36,6 +36,7 @@ testing = ["matrix-sdk-test"] [dependencies] aes = "0.8.1" +aquamarine = { workspace = true } as_variant = { workspace = true } async-trait = { workspace = true } bs58 = { version = "0.5.0" } diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 9a2afa8633b..0f0b109e9af 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -1,4 +1,5 @@ // Copyright 2020 The Matrix.org Foundation C.I.C. +// Copyright 2024 Damir Jelić // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -156,3 +157,945 @@ pub enum RoomEventDecryptionResult { /// We were unable to decrypt the event UnableToDecrypt(UnableToDecryptInfo), } + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// A step by step guide that explains how to include [end-to-end-encryption] +/// support in a [Matrix] client library. +/// +/// This crate implements a [sans-network-io](https://sans-io.readthedocs.io/) +/// state machine that allows you to add [end-to-end-encryption] support to a +/// [Matrix] client library. +/// +/// This guide aims to provide a comprehensive understanding of end-to-end +/// encryption in Matrix without any prior knowledge requirements. However, it +/// is recommended that the reader has a basic understanding of Matrix and its +/// [client-server specification] for a more informed and efficient learning +/// experience. +/// +/// The [introductory](#introduction) section provides a simplified explanation +/// of end-to-end encryption and its implementation in Matrix for those who may +/// not have prior knowledge. If you already have a solid understanding of +/// end-to-end encryption, including the [Olm] and [Megolm] protocols, you may +/// choose to skip directly to the [Getting Started](#getting-started) section. +/// +/// # Table of Contents +/// 1. [Introduction](#introduction) +/// 2. [Getting started](#getting-started) +/// 3. [Decrypting room events](#decryption) +/// 4. [Encrypting room events](#encryption) +/// 5. [Interactively verifying devices and user identities](#verification) +/// +/// # Introduction +/// +/// Welcome to the first part of this guide, where we will introduce the +/// fundamental concepts of end-to-end encryption and its implementation in +/// Matrix. +/// +/// This section will provide a clear and concise overview of what +/// end-to-end encryption is and why it is important for secure communication. +/// You will also learn about how Matrix uses end-to-end encryption to protect +/// the privacy and security of its users' communications. Whether you are new +/// to the topic or simply want to improve your understanding, this section will +/// serve as a solid foundation for the rest of the guide. +/// +/// Let's dive in! +/// +/// ## Notation +/// +/// ## End-to-end-encryption +/// +/// End-to-end encryption (E2EE) is a method of secure communication where only +/// the communicating devices, also known as "the ends," can read the data being +/// transmitted. This means that the data is encrypted on one device, and can +/// only be decrypted on the other device. The server is used only as a +/// transport mechanism to deliver messages between devices. +/// +/// The following chart displays how communication between two clients using a +/// server in the middle usually works. +/// +/// ```mermaid +/// flowchart LR +/// alice[Alice] +/// bob[Bob] +/// subgraph Homeserver +/// direction LR +/// outbox[Alice outbox] +/// inbox[Bob inbox] +/// outbox -. unencrypted .-> inbox +/// end +/// +/// alice -- encrypted --> outbox +/// inbox -- encrypted --> bob +/// ``` +/// +/// The next chart, instead, displays how the same flow is happening in a +/// end-to-end-encrypted world. +/// +/// ```mermaid +/// flowchart LR +/// alice[Alice] +/// bob[Bob] +/// subgraph Homeserver +/// direction LR +/// outbox[Alice outbox] +/// inbox[Bob inbox] +/// outbox == encrypted ==> inbox +/// end +/// +/// alice == encrypted ==> outbox +/// inbox == encrypted ==> bob +/// ``` +/// +/// Note that the path from the outbox to the inbox is now encrypted as well. +/// +/// Alice and Bob have created a secure communication channel +/// through which they can exchange messages confidentially, without the risk of +/// the server accessing the contents of their messages. +/// +/// ## Publishing cryptographic identities of devices +/// +/// If Alice and Bob want to establish a secure channel over which they can +/// exchange messages, they first need learn about each others cryptographic +/// identities. This is achieved by using the homeserver as a public key +/// directory. +/// +/// A public key directory is used to store and distribute public keys of users +/// in an end-to-end encrypted system. The basic idea behind a public key +/// directory is that it allows users to easily discover and download the public +/// keys of other users with whom they wish to establish an end-to-end encrypted +/// communication. +/// +/// Each user generates a pair of public and private keys. The user then uploads +/// their public key to the public key directory. Other users can then search +/// the directory to find the public key of the user they wish to communicate +/// with, and download it to their own device. +/// +/// ```mermaid +/// flowchart LR +/// alice[Alice] +/// subgraph homeserver[Homeserver] +/// direction LR +/// directory[(Public key directory)] +/// end +/// bob[Bob] +/// +/// alice -- upload keys --> directory +/// directory -- download keys --> bob +/// ``` +/// +/// Once a user has the other user's public key, they can use it to establish an +/// end-to-end encrypted channel using a [key-agreement protocol]. +/// +/// ## Using the Triple Diffie-Hellman key-agreement protocol +/// +/// In the triple Diffie-Hellman key agreement protocol (3DH in short), each +/// user generates a long-term identity key pair and a set of one-time prekeys. +/// When two users want to establish a shared secret key, they exchange their +/// public identity keys and one of their prekeys. These public keys are then +/// used in a [Diffie-Hellman] key exchange to compute a shared secret key. +/// +/// The use of one-time prekeys ensures that the shared secret key is different +/// for each session, even if the same identity keys are used. +/// +/// ```mermaid +/// flowchart LR +/// subgraph alice_keys[Alice Keys] +/// direction TB +/// alice_key[Alice's identity key] +/// alice_base_key[Alice's one-time key] +/// end +/// +/// subgraph bob_keys[Bob Keys] +/// direction TB +/// bob_key[Bob's identity key] +/// bob_one_time[Bob's one-time key] +/// end +/// +/// alice_key <--> bob_one_time +/// alice_base_key <--> bob_one_time +/// alice_base_key <--> bob_key +/// ``` +/// +/// Similar to [X3DH] (Extended Triple Diffie-Hellman) key agreement protocol +/// +/// ## Speeding up encryption for large groups +/// +/// In the previous section we learned how to utilize a key agreement protocol +/// to establish secure 1-to-1 encrypted communication channels. These channels +/// allow us to encrypt a message for each device separately. +/// +/// One critical property of these channels is that, if you want to send a +/// message to a group of devices, we'll need to encrypt the message for each +/// device individually. +/// +/// TODO Explain how megolm fits into this +/// +/// # Getting started +/// +/// Before we start writing any code, let us get familiar with the basic +/// principle upon which this library is built. +/// +/// The central piece of the library is the [`OlmMachine`] which acts as a state +/// machine which consumes data that gets received from the homeserver and +/// outputs data which should be sent to the homeserver. +/// +/// ## Push/pull mechanism +/// +/// The [`OlmMachine`] at the heart of it acts as a state machine that operates +/// in a push/pull manner. HTTP responses which were received from the +/// homeserver get forwarded into the [`OlmMachine`] and in turn the internal +/// state gets updated which produces HTTP requests that need to be sent to the +/// homeserver. +/// +/// In a manner, we're pulling data from the server, we update our internal +/// state based on the data and in turn push data back to the server. +/// +/// ```mermaid +/// flowchart LR +/// homeserver[Homeserver] +/// client[OlmMachine] +/// +/// homeserver -- pull --> client +/// client -- push --> homeserver +/// ``` +/// +/// ## Initializing the state machine +/// +/// ``` +/// use anyhow::Result; +/// use matrix_sdk_crypto::OlmMachine; +/// use ruma::user_id; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// let user_id = user_id!("@alice:localhost"); +/// let device_id = "DEVICEID".into(); +/// +/// let machine = OlmMachine::new(user_id, device_id).await; +/// # Ok(()) +/// # } +/// ``` +/// +/// This will create a [`OlmMachine`] that does not persist any data TODO +/// ```ignore +/// use anyhow::Result; +/// use matrix_sdk_crypto::OlmMachine; +/// use matrix_sdk_sqlite::SqliteCryptoStore; +/// use ruma::user_id; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// let user_id = user_id!("@alice:localhost"); +/// let device_id = "DEVICEID".into(); +/// +/// let store = SqliteCryptoStore::open("/home/example/matrix-client/", None).await?; +/// +/// let machine = OlmMachine::with_store(user_id, device_id, store).await; +/// # Ok(()) +/// # } +/// ``` +/// +/// # Decryption +/// +/// In the world of encrypted communication, it is common to start with the +/// encryption step when implementing a protocol. However, in the case of adding +/// end-to-end encryption support to a Matrix client library, a simpler approach +/// is to first focus on the decryption process. This is because there are +/// already Matrix clients in existence that support encryption, which means +/// that our client library can simply receive encrypted messages and then +/// decrypt them. +/// +/// In this section, we will guide you through the minimal steps +/// necessary to get the decryption process up and running using the +/// matrix-sdk-crypto Rust crate. By the end of this section you should have a +/// Matrix client that is able to decrypt room events that other clients have +/// sent. +/// +/// To enable decryption the following three steps are needed: +/// +/// 1. [The cryptographic identity of your device needs to be published to the +/// homeserver](#uploading-identity-and-one-time-keys). +/// 2. [Decryption keys coming in from other devices need to be processed and +/// stored](#receiving-room-keys-and-related-changes). +/// 3. [Individual messages need to be decrypted](#decrypting-room-events). +/// +/// The simplified flowchart +/// ```mermaid +/// graph TD +/// sync[Sync with the homeserver] +/// receive_changes[Push E2EE related changes into the state machine] +/// send_outgoing_requests[Send all outgoing requests to the homeserver] +/// decrypt[Process the rest of the sync] +/// +/// sync --> receive_changes; +/// receive_changes --> send_outgoing_requests; +/// send_outgoing_requests --> decrypt; +/// decrypt -- repeat --> sync; +/// ``` +/// +/// ## Uploading identity and one-time keys. +/// +/// To enable end-to-end encryption in a Matrix client, the first step is to +/// announce the support for it to other users in the network. This is done by +/// publishing the client's long-term device keys and a set of one-time prekeys +/// to the Matrix homeserver. The homeserver then makes this information +/// available to other devices in the network. +/// +/// The long-term device keys and one-time prekeys allow other devices to +/// encrypt messages specifically for your device. +/// +/// To achieve this, you will need to extract any requests that need to be sent +/// to the homeserver from the [`OlmMachine`] and send them to the homeserver. +/// The following snippet showcases how to achieve this using the +/// [`OlmMachine::outgoing_requests()`] method: +/// +/// ```no_run +/// # use std::collections::BTreeMap; +/// # use ruma::api::client::keys::upload_keys::v3::Response; +/// # use anyhow::Result; +/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest}; +/// # async fn send_request(request: OutgoingRequest) -> Result { +/// # let response = unimplemented!(); +/// # Ok(response) +/// # } +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # let machine: OlmMachine = unimplemented!(); +/// // Get all the outgoing requests. +/// let outgoing_requests = machine.outgoing_requests().await?; +/// +/// // Send each request to the server and push the response into the state machine. +/// // You can safely send these requests out in parallel. +/// for request in outgoing_requests { +/// let request_id = request.request_id(); +/// // Send the request to the server and await a response. +/// let response = send_request(request).await?; +/// // Push the response into the state machine. +/// machine.mark_request_as_sent(&request_id, &response).await?; +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// #### 🔒 Locking rule +/// +/// It's important to note that the outgoing requests method in the +/// [`OlmMachine`], while thread-safe, may return the same request multiple +/// times if it is called multiple times before the request has been marked as +/// sent. To prevent this issue, it is advisable to encapsulate the outgoing +/// request handling logic into a separate helper method and protect it from +/// being called multiple times concurrently using a lock. +/// +/// This helps to ensure that the request is only handled once and prevents +/// multiple identical requests from being sent. +/// +/// Additionally, if an error occurs while sending a request using the +/// [`OlmMachine::outgoing_requests()`] method, the request will be +/// naturally retried the next time the method is called. +/// +/// A more complete example, which uses a helper method, might look like this: +/// ```no_run +/// # use std::collections::BTreeMap; +/// # use ruma::api::client::keys::upload_keys::v3::Response; +/// # use anyhow::Result; +/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest}; +/// # async fn send_request(request: &OutgoingRequest) -> Result { +/// # let response = unimplemented!(); +/// # Ok(response) +/// # } +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// struct Client { +/// outgoing_requests_lock: tokio::sync::Mutex<()>, +/// olm_machine: OlmMachine, +/// } +/// +/// async fn process_outgoing_requests(client: &Client) -> Result<()> { +/// // Let's acquire a lock so we know that we don't send out the same request out multiple +/// // times. +/// let guard = client.outgoing_requests_lock.lock().await; +/// +/// for request in client.olm_machine.outgoing_requests().await? { +/// let request_id = request.request_id(); +/// +/// match send_request(&request).await { +/// Ok(response) => { +/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?; +/// } +/// Err(error) => { +/// // It's OK to ignore transient HTTP errors since requests will be retried. +/// eprintln!( +/// "Error while sending out a end-to-end encryption \ +/// related request: {error:?}" +/// ); +/// } +/// } +/// } +/// +/// Ok(()) +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Once we have the helper method that processes our outgoing requests we can +/// structure our sync method as follows: +/// +/// ```no_run +/// # use anyhow::Result; +/// # use matrix_sdk_crypto::OlmMachine; +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # struct Client { +/// # outgoing_requests_lock: tokio::sync::Mutex<()>, +/// # olm_machine: OlmMachine, +/// # } +/// # async fn process_outgoing_requests(client: &Client) -> Result<()> { +/// # unimplemented!(); +/// # } +/// # async fn send_out_sync_request(client: &Client) -> Result<()> { +/// # unimplemented!(); +/// # } +/// async fn sync(client: &Client) -> Result<()> { +/// // This is happening at the top of the method so we advertise our +/// // end-to-end encryption capabilities as soon as possible. +/// process_outgoing_requests(client).await?; +/// +/// // We can sync with the homeserver now. +/// let response = send_out_sync_request(client).await?; +/// +/// // Process the sync response here. +/// +/// Ok(()) +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// ## Receiving room keys and related changes +/// +/// The next step in our implementation is to forward messages that were sent +/// directly to the client's device, and state updates about the one-time +/// prekeys, to the [`OlmMachine`]. This is achieved using +/// the [`OlmMachine::receive_sync_changes()`] method. +/// +/// The method performs two tasks: +/// +/// 1. It processes and, if necessary, decrypts each [to-device] event that was +/// pushed into it, and returns the decrypted events. The original events are +/// replaced with their decrypted versions. +/// +/// 2. It produces internal state changes that may trigger the creation of new +/// outgoing requests. For example, if the server informs the client that its +/// one-time prekeys have been depleted, the OlmMachine will create an +/// outgoing request to replenish them. +/// +/// Our updated sync method now looks like this: +/// +/// ```no_run +/// # use anyhow::Result; +/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine}; +/// # use ruma::api::client::sync::sync_events::v3::Response; +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # struct Client { +/// # outgoing_requests_lock: tokio::sync::Mutex<()>, +/// # olm_machine: OlmMachine, +/// # } +/// # async fn process_outgoing_requests(client: &Client) -> Result<()> { +/// # unimplemented!(); +/// # } +/// # async fn send_out_sync_request(client: &Client) -> Result { +/// # unimplemented!(); +/// # } +/// async fn sync(client: &Client) -> Result<()> { +/// process_outgoing_requests(client).await?; +/// +/// let response = send_out_sync_request(client).await?; +/// +/// let sync_changes = EncryptionSyncChanges { +/// to_device_events: response.to_device.events, +/// changed_devices: &response.device_lists, +/// one_time_keys_counts: &response.device_one_time_keys_count, +/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(), +/// next_batch_token: Some(response.next_batch), +/// }; +/// +/// // Push the sync changes into the OlmMachine, make sure that this is +/// // happening before the `next_batch` token of the sync is persisted. +/// let to_device_events = client +/// .olm_machine +/// .receive_sync_changes(sync_changes) +/// .await?; +/// +/// // Send the outgoing requests out that the sync changes produced. +/// process_outgoing_requests(client).await?; +/// +/// // Process the rest of the sync response here. +/// +/// Ok(()) +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// It is important to note that the names of the fields in the response shown +/// in the example match the names of the fields specified in the [sync] +/// response specification. +/// +/// It is critical to note that due to the ephemeral nature of to-device +/// events[[1]], it is important to process these events before persisting the +/// `next_batch` sync token. This is because if the `next_batch` sync token is +/// persisted before processing the to-device events, some messages might be +/// lost, leading to decryption failures. +/// +/// ## Decrypting room events +/// +/// The final step in the decryption process is to decrypt the room events that +/// are received from the server. To do this, the encrypted events must be +/// passed to the [`OlmMachine`], which will use the keys that were previously +/// exchanged between devices to decrypt the events. The decrypted events can +/// then be processed and displayed to the user in the Matrix client. +/// +/// Room message [events] can be decrypted using the +/// [`OlmMachine::decrypt_room_event()`] method: +/// +/// ```no_run +/// # use std::collections::BTreeMap; +/// # use anyhow::Result; +/// # use matrix_sdk_crypto::{OlmMachine, DecryptionSettings, TrustRequirement}; +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # let encrypted = unimplemented!(); +/// # let room_id = unimplemented!(); +/// # let machine: OlmMachine = unimplemented!(); +/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; +/// // Decrypt your room events now. +/// let decrypted = machine +/// .decrypt_room_event(encrypted, room_id, &settings) +/// .await?; +/// # Ok(()) +/// # } +/// ``` +/// It's worth mentioning that the [`OlmMachine::decrypt_room_event()`] method +/// is designed to be thread-safe and can be safely called concurrently. This +/// means that room message [events] can be processed in parallel, improving the +/// overall efficiency of the end-to-end encryption implementation. +/// +/// By allowing room message [events] to be processed concurrently, the client's +/// implementation can take full advantage of the capabilities of modern +/// hardware and achieve better performance, especially when dealing with a +/// large number of messages at once. +/// +/// # Encryption +/// +/// In this section of the guide, we will focus on enabling the encryption of +/// messages in our Matrix client library. Up until this point, we have been +/// discussing the process of decrypting messages that have been encrypted by +/// other devices. Now, we will shift our focus to the process of encrypting +/// messages on the client side, so that they can be securely transmitted over +/// the Matrix network to other devices. +/// +/// This section will guide you through the steps required to set up the +/// encryption process, including establishing the necessary sessions and +/// encrypting messages using the Megolm group session. The specific steps are +/// outlined bellow: +/// +/// 1. [Cryptographic devices of other users need to be +/// discovered](#tracking-users) +/// +/// 2. [Secure channels between the devices need to be +/// established](#establishing-end-to-end-encrypted-channels) +/// +/// 3. [A room key needs to be exchanged with the group](#exchanging-room-keys) +/// +/// 4. [Individual messages need to be encrypted using the room +/// key](#encrypting-room-events) +/// +/// The process for enabling encryption in a two-device scenario is also +/// depicted in the following sequence diagram: +/// +/// ```mermaid +/// sequenceDiagram +/// actor Alice +/// participant Homeserver +/// actor Bob +/// +/// Alice->>Homeserver: Download Bob's one-time prekey +/// Homeserver->>Alice: Bob's one-time prekey +/// Alice->>Alice: Encrypt the room key +/// Alice->>Homeserver: Send the room key to each of Bob's devices +/// Homeserver->>Bob: Deliver the room key +/// Alice->>Alice: Encrypt the message +/// Alice->>Homeserver: Send the encrypted message +/// Homeserver->>Bob: Deliver the encrypted message +/// ``` +/// +/// In the following subsections, we will provide a step-by-step guide on how to +/// enable the encryption of messages using the OlmMachine. We will outline the +/// specific method calls and usage patterns that are required to establish the +/// necessary sessions, encrypt messages, and send them over the Matrix network. +/// +/// ## Tracking users +/// +/// The first step in the process of encrypting a message and sending it to a +/// device is to discover the devices that the recipient user has. This can be +/// achieved by sending a request to the homeserver to retrieve a list of the +/// recipient's device keys. The response to this request will include the +/// device keys for all of the devices that belong to the recipient, as well as +/// information about their current status and whether or not they support +/// end-to-end encryption. +/// +/// The process for discovering and keeping track of devices for a user is +/// outlined in the Matrix specification in the "[Tracking the device list for a +/// user]" section. +/// +/// A simplified sequence diagram of the process can also be found bellow. +/// +/// ```mermaid +/// sequenceDiagram +/// actor Alice +/// participant Homeserver +/// +/// Alice->>Homeserver: Sync with the homeserver +/// Homeserver->>Alice: Users whose device list has changed +/// Alice->>Alice: Mark user's devicel list as outdated +/// Alice->>Homeserver: Ask the server for the new device list of all the outdated users +/// Alice->>Alice: Update the local device list and mark the users as up-to-date +/// ``` +/// +/// The OlmMachine refers to users whose devices we are tracking as "tracked +/// users" and utilizes the [`OlmMachine::update_tracked_users()`] method to +/// start considering users to be tracked. Keeping the above diagram in mind, we +/// can now update our sync method as follows: +/// +/// ```no_run +/// # use anyhow::Result; +/// # use std::ops::Deref; +/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine}; +/// # use ruma::api::client::sync::sync_events::v3::{Response, JoinedRoom}; +/// # use ruma::{OwnedUserId, serde::Raw, events::AnySyncStateEvent}; +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # struct Client { +/// # outgoing_requests_lock: tokio::sync::Mutex<()>, +/// # olm_machine: OlmMachine, +/// # } +/// # async fn process_outgoing_requests(client: &Client) -> Result<()> { +/// # unimplemented!(); +/// # } +/// # async fn send_out_sync_request(client: &Client) -> Result { +/// # unimplemented!(); +/// # } +/// # fn is_member_event_of_a_joined_user(event: &Raw) -> bool { +/// # true +/// # } +/// # fn get_user_id(event: &Raw) -> OwnedUserId { +/// # unimplemented!(); +/// # } +/// # fn is_room_encrypted(room: &JoinedRoom) -> bool { +/// # true +/// # } +/// async fn sync(client: &Client) -> Result<()> { +/// process_outgoing_requests(client).await?; +/// +/// let response = send_out_sync_request(client).await?; +/// +/// let sync_changes = EncryptionSyncChanges { +/// to_device_events: response.to_device.events, +/// changed_devices: &response.device_lists, +/// one_time_keys_counts: &response.device_one_time_keys_count, +/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(), +/// next_batch_token: Some(response.next_batch), +/// }; +/// +/// // Push the sync changes into the OlmMachine, make sure that this is +/// // happening before the `next_batch` token of the sync is persisted. +/// let to_device_events = client +/// .olm_machine +/// .receive_sync_changes(sync_changes) +/// .await?; +/// +/// // Send the outgoing requests out that the sync changes produced. +/// process_outgoing_requests(client).await?; +/// +/// // Collect all the joined and invited users of our end-to-end encrypted rooms here. +/// let mut users = Vec::new(); +/// +/// for (_, room) in &response.rooms.join { +/// // For simplicity reasons we're only looking at the state field of a joined room, but +/// // the events in the timeline are important as well. +/// for event in &room.state.events { +/// if is_member_event_of_a_joined_user(event) && is_room_encrypted(room) { +/// let user_id = get_user_id(event); +/// users.push(user_id); +/// } +/// } +/// } +/// +/// // Mark all the users that we consider to be in a end-to-end encrypted room with us to be +/// // tracked. We need to know about all the devices each user has so we can later encrypt +/// // messages for each of their devices. +/// client.olm_machine.update_tracked_users(users.iter().map(Deref::deref)).await?; +/// +/// // Process the rest of the sync response here. +/// +/// Ok(()) +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Now that we have discovered the devices of the users we'd like to +/// communicate with in an end-to-end encrypted manner, we can start considering +/// encrypting messages for those devices. This concludes the sync processing +/// method, we are now ready to move on to the next section, which will explain +/// how to begin the encryption process. +/// +/// ## Establishing end-to-end encrypted channels +/// +/// In the [Triple +/// Diffie-Hellman](#using-the-triple-diffie-hellman-key-agreement-protocol) +/// section, we described the need for two Curve25519 keys from the recipient +/// device to establish a 1-to-1 secure channel: the long-term identity key of a +/// device and a one-time prekey. In the previous section, we started tracking +/// the device keys, including the long-term identity key that we need. The next +/// step is to download the one-time prekey on an on-demand basis and establish +/// the 1-to-1 secure channel. +/// +/// To accomplish this, we can use the [`OlmMachine::get_missing_sessions()`] +/// method in bulk, which will claim the one-time prekey for all the devices of +/// a user that we're not already sharing a 1-to-1 encrypted channel with. +/// +/// #### 🔒 Locking rule +/// +/// As with the [`OlmMachine::outgoing_requests()`] method, it is necessary to +/// protect this method with a lock, otherwise we will be creating more 1-to-1 +/// encrypted channels than necessary. +/// +/// ```no_run +/// # use std::collections::{BTreeMap, HashSet}; +/// # use std::ops::Deref; +/// # use anyhow::Result; +/// # use ruma::UserId; +/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request}; +/// # use matrix_sdk_crypto::OlmMachine; +/// # async fn send_request(request: &Request) -> Result { +/// # let response = unimplemented!(); +/// # Ok(response) +/// # } +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # let users: HashSet<&UserId> = HashSet::new(); +/// # let machine: OlmMachine = unimplemented!(); +/// // Mark all the users that are part of an encrypted room as tracked +/// if let Some((request_id, request)) = +/// machine.get_missing_sessions(users.iter().map(Deref::deref)).await? +/// { +/// let response = send_request(&request).await?; +/// machine.mark_request_as_sent(&request_id, &response).await?; +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// With the ability to exchange messages directly with devices, we can now +/// start sharing room keys over the 1-to-1 encrypted channel. +/// +/// ## Exchanging room keys +/// +/// To exchange a room key with our group, we will once again take a bulk +/// approach. The [`OlmMachine::share_room_key()`] method is used to accomplish +/// this step. This method will create a new room key, if necessary, and encrypt +/// it for each device belonging to the users provided as an argument. It will +/// then output an array of sendToDevice requests that we must send to the +/// server, and mark the requests as sent. +/// +/// #### 🔒 Locking rule +/// +/// Like some of the previous methods, OlmMachine::share_room_key() needs to be +/// protected by a lock to prevent the possibility of creating and sending +/// multiple room keys simultaneously for the same group. The lock can be +/// implemented on a per-room basis, which allows for parallel room key +/// exchanges across different rooms. +/// +/// ```no_run +/// # use std::collections::{BTreeMap, HashSet}; +/// # use std::ops::Deref; +/// # use anyhow::Result; +/// # use ruma::UserId; +/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request}; +/// # use matrix_sdk_crypto::{OlmMachine, requests::ToDeviceRequest, EncryptionSettings}; +/// # async fn send_request(request: &ToDeviceRequest) -> Result { +/// # let response = unimplemented!(); +/// # Ok(response) +/// # } +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # let users: HashSet<&UserId> = HashSet::new(); +/// # let room_id = unimplemented!(); +/// # let settings = EncryptionSettings::default(); +/// # let machine: OlmMachine = unimplemented!(); +/// // Let's share a room key with our group. +/// let requests = machine.share_room_key( +/// room_id, +/// users.iter().map(Deref::deref), +/// EncryptionSettings::default(), +/// ).await?; +/// +/// // Make sure each request is sent out +/// for request in requests { +/// let request_id = &request.txn_id; +/// let response = send_request(&request).await?; +/// machine.mark_request_as_sent(&request_id, &response).await?; +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// In order to ensure that room keys are rotated and exchanged when needed, the +/// [`OlmMachine::share_room_key()`] method should be called before sending +/// each room message in an end-to-end encrypted room. If a room key has +/// already been exchanged, the method becomes a no-op. +/// +/// ## Encrypting room events +/// +/// After the room key has been successfully shared, a plaintext can be +/// encrypted. +/// +/// ```no_run +/// # use anyhow::Result; +/// # use matrix_sdk_crypto::{DecryptionSettings, OlmMachine, TrustRequirement}; +/// # use ruma::events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent}; +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # let room_id = unimplemented!(); +/// # let event = unimplemented!(); +/// # let machine: OlmMachine = unimplemented!(); +/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; +/// let content = AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain("It's a secret to everybody.")); +/// let encrypted_content = machine.encrypt_room_event(room_id, content).await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// ## Appendix: Combining the session creation and room key exchange +/// +/// The steps from the previous three sections should combined into a single +/// method that is used to send messages. +/// +/// ```no_run +/// # use std::collections::{BTreeMap, HashSet}; +/// # use std::ops::Deref; +/// # use anyhow::Result; +/// # use serde_json::json; +/// # use ruma::{UserId, RoomId, serde::Raw}; +/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request}; +/// # use matrix_sdk_crypto::{EncryptionSettings, OlmMachine, ToDeviceRequest}; +/// # use tokio::sync::MutexGuard; +/// # async fn send_request(request: &Request) -> Result { +/// # let response = unimplemented!(); +/// # Ok(response) +/// # } +/// # async fn send_to_device_request(request: &ToDeviceRequest) -> Result { +/// # let response = unimplemented!(); +/// # Ok(response) +/// # } +/// # async fn acquire_per_room_lock(room_id: &RoomId) -> MutexGuard<()> { +/// # unimplemented!(); +/// # } +/// # async fn get_joined_members(room_id: &RoomId) -> Vec<&UserId> { +/// # unimplemented!(); +/// # } +/// # fn is_room_encrypted(room_id: &RoomId) -> bool { +/// # true +/// # } +/// # #[tokio::main] +/// # async fn main() -> Result<()> { +/// # let users: HashSet<&UserId> = HashSet::new(); +/// # let machine: OlmMachine = unimplemented!(); +/// struct Client { +/// session_establishment_lock: tokio::sync::Mutex<()>, +/// olm_machine: OlmMachine, +/// } +/// +/// async fn establish_sessions(client: &Client, users: &[&UserId]) -> Result<()> { +/// if let Some((request_id, request)) = +/// client.olm_machine.get_missing_sessions(users.iter().map(Deref::deref)).await? +/// { +/// let response = send_request(&request).await?; +/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?; +/// } +/// +/// Ok(()) +/// } +/// +/// async fn share_room_key(machine: &OlmMachine, room_id: &RoomId, users: &[&UserId]) -> Result<()> { +/// let _lock = acquire_per_room_lock(room_id).await; +/// +/// let requests = machine.share_room_key( +/// room_id, +/// users.iter().map(Deref::deref), +/// EncryptionSettings::default(), +/// ).await?; +/// +/// // Make sure each request is sent out +/// for request in requests { +/// let request_id = &request.txn_id; +/// let response = send_to_device_request(&request).await?; +/// machine.mark_request_as_sent(&request_id, &response).await?; +/// } +/// +/// Ok(()) +/// } +/// +/// async fn send_message(client: &Client, room_id: &RoomId, message: &str) -> Result<()> { +/// let mut content = json!({ +/// "body": message, +/// "msgtype": "m.text", +/// }); +/// +/// if is_room_encrypted(room_id) { +/// let content = Raw::new(&json!({ +/// "body": message, +/// "msgtype": "m.text", +/// }))?.cast(); +/// +/// let users = get_joined_members(room_id).await; +/// +/// establish_sessions(client, &users).await?; +/// share_room_key(&client.olm_machine, room_id, &users).await?; +/// +/// let encrypted = client +/// .olm_machine +/// .encrypt_room_event_raw(room_id, "m.room.message", &content) +/// .await?; +/// } +/// +/// Ok(()) +/// } +/// # Ok(()) +/// # } +/// ``` + +/// +/// TODO +/// +/// [Matrix]: https://matrix.org/ +/// [Olm]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md +/// [Diffie-Hellman]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange +/// [Megolm]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md +/// [end-to-end-encryption]: https://en.wikipedia.org/wiki/End-to-end_encryption +/// [homeserver]: https://spec.matrix.org/unstable/#architecture +/// [key-agreement protocol]: https://en.wikipedia.org/wiki/Key-agreement_protocol +/// [client-server specification]: https://matrix.org/docs/spec/client_server/ +/// [forward secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy +/// [replay attacks]: https://en.wikipedia.org/wiki/Replay_attack +/// [Tracking the device list for a user]: https://spec.matrix.org/unstable/client-server-api/#tracking-the-device-list-for-a-user +/// [X3DH]: https://signal.org/docs/specifications/x3dh/ +/// [to-device]: https://spec.matrix.org/unstable/client-server-api/#send-to-device-messaging +/// [sync]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3sync +/// [events]: https://spec.matrix.org/unstable/client-server-api/#events +/// +/// [1]: https://spec.matrix.org/unstable/client-server-api/#server-behaviour-4 +pub mod tutorial {} diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index b983f4312d7..2bf75fcb7ff 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -66,7 +66,7 @@ docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode"] [dependencies] anyhow = { workspace = true, optional = true } anymap2 = "0.13.0" -aquamarine = "0.6.0" +aquamarine = { workspace = true } assert_matches2 = { workspace = true, optional = true } as_variant = { workspace = true } async-channel = "2.2.1" From e55a1c7e00d2693d87d798f09852161d5cbf0495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 28 Jul 2023 16:38:30 +0200 Subject: [PATCH 578/979] chore: Rework the crypto crate README --- crates/matrix-sdk-crypto/README.md | 48 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-crypto/README.md b/crates/matrix-sdk-crypto/README.md index 8f92c787bc8..154c9f0fed7 100644 --- a/crates/matrix-sdk-crypto/README.md +++ b/crates/matrix-sdk-crypto/README.md @@ -1,13 +1,11 @@ -A no-network-IO implementation of a state machine that handles E2EE for -[Matrix] clients. - -# Usage +A no-network-IO implementation of a state machine that handles end-to-end +encryption for [Matrix] clients. If you're just trying to write a Matrix client or bot in Rust, you're probably looking for [matrix-sdk] instead. -However, if you're looking to add E2EE to an existing Matrix client or library, -read on. +However, if you're looking to add end-to-end encryption to an existing Matrix +client or library, read on. The state machine works in a push/pull manner: @@ -52,28 +50,42 @@ async fn main() -> Result<(), OlmError> { Ok(()) } ``` +It is recommended to use the [tutorial] to understand how end-to-end encryption +works in Matrix and how to add end-to-end encryption support in your Matrix +client library. [Matrix]: https://matrix.org/ [matrix-sdk]: https://github.com/matrix-org/matrix-rust-sdk/ -# Room key forwarding algorithm +# Crate Feature Flags -The decision tree below visualizes the way this crate decides whether a message -key ("room key") will be [forwarded][forwarded_room_key] to a requester upon a -key request, provided the `automatic-room-key-forwarding` feature is enabled. -Key forwarding is sometimes also referred to as key *gossiping*. +The following crate feature flags are available: -[forwarded_room_key]: +| Feature | Default | Description | +| ------------------- | :-----: | -------------------------------------------------------------------------------------------------------------------------- | +| `qrcode` | No | Enables QR code based interactive verification | +| `js` | No | Enables JavaScript API usage for things like the current system time on WASM (does nothing on other targets) | +| `testing` | No | Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. | -![](https://raw.githubusercontent.com/matrix-org/matrix-rust-sdk/main/contrib/key-sharing-algorithm/model.png) +* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. +* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys. +# Enabling logging -# Crate Feature Flags +Users of the `matrix-sdk-crypto` crate can enable log output by depending on the +`tracing-subscriber` crate and including the following line in their +application (e.g. at the start of `main`): -The following crate feature flags are available: +```no_compile +tracing_subscriber::fmt::init(); +``` -* `qrcode`: Enbles QRcode generation and reading code +The log output is controlled via the `RUST_LOG` environment variable by +setting it to one of the `error`, `warn`, `info`, `debug` or `trace` levels. +The output is printed to stdout. -* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. +The `RUST_LOG` variable also supports a more advanced syntax for filtering +log output more precisely, for instance with crate-level granularity. For +more information on this, check out the [tracing-subscriber documentation]. -* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys. +[tracing-subscriber documentation]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/ From 079ec023b7655eac39efed64db84be41f8034261 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 20 Nov 2024 15:27:56 +0100 Subject: [PATCH 579/979] task(oidc): add logs when refreshing an OIDC token --- crates/matrix-sdk/src/oidc/mod.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/oidc/mod.rs b/crates/matrix-sdk/src/oidc/mod.rs index aba26c6f7c5..7f87bd34713 100644 --- a/crates/matrix-sdk/src/oidc/mod.rs +++ b/crates/matrix-sdk/src/oidc/mod.rs @@ -197,7 +197,7 @@ use serde::{Deserialize, Serialize}; use sha2::Digest as _; use thiserror::Error; use tokio::{spawn, sync::Mutex}; -use tracing::{error, trace, warn}; +use tracing::{debug, error, info, instrument, trace, warn}; use url::Url; mod auth_code_builder; @@ -1368,6 +1368,7 @@ impl Oidc { /// or the same [`RefreshTokenError`], if it failed. /// /// [`ClientBuilder::handle_refresh_tokens()`]: crate::ClientBuilder::handle_refresh_tokens() + #[instrument(skip_all)] pub async fn refresh_access_token(&self) -> Result<(), RefreshTokenError> { macro_rules! fail { ($lock:expr, $err:expr) => { @@ -1382,11 +1383,16 @@ impl Oidc { let refresh_status_lock = client.inner.auth_ctx.refresh_token_lock.clone().try_lock_owned(); let Ok(mut refresh_status_guard) = refresh_status_lock else { + debug!("another refresh is happening, waiting for result."); // There's already a request to refresh happening in the same process. Wait for // it to finish. - return client.inner.auth_ctx.refresh_token_lock.lock().await.clone(); + let res = client.inner.auth_ctx.refresh_token_lock.lock().await.clone(); + debug!("other refresh is a {}", if res.is_ok() { "success" } else { "failure " }); + return res; }; + debug!("no other refresh happening in background, starting."); + let cross_process_guard = if let Some(manager) = self.ctx().cross_process_token_refresh_manager.get() { let mut cross_process_guard = match manager @@ -1396,6 +1402,7 @@ impl Oidc { { Ok(guard) => guard, Err(err) => { + warn!("couldn't acquire cross-process lock (timeout)"); fail!(refresh_status_guard, err); } }; @@ -1406,6 +1413,7 @@ impl Oidc { .map_err(|err| RefreshTokenError::Oidc(Arc::new(err.into())))?; // Optimistic exit: assume that the underlying process did update fast enough. // In the worst case, we'll do another refresh Soon™. + info!("other process handled refresh for us, assuming success"); *refresh_status_guard = Ok(()); return Ok(()); } @@ -1416,9 +1424,12 @@ impl Oidc { }; let Some(session_tokens) = self.session_tokens() else { + warn!("invalid state: missing session tokens"); fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired); }; + let Some(refresh_token) = session_tokens.refresh_token else { + warn!("invalid state: missing session tokens"); fail!(refresh_status_guard, RefreshTokenError::RefreshTokenRequired); }; @@ -1426,11 +1437,13 @@ impl Oidc { Ok(metadata) => metadata, Err(err) => { let err = Arc::new(err); + warn!("couldn't get provider metadata: {err}"); fail!(refresh_status_guard, RefreshTokenError::Oidc(err)); } }; let Some(auth_data) = self.data() else { + warn!("invalid state: missing auth data"); fail!( refresh_status_guard, RefreshTokenError::Oidc(Arc::new(OidcError::NotAuthenticated)) @@ -1459,11 +1472,14 @@ impl Oidc { .await { Ok(()) => { + debug!("success refreshing a token"); *refresh_status_guard = Ok(()); Ok(()) } + Err(err) => { let err = RefreshTokenError::Oidc(Arc::new(err)); + warn!("error refreshing an oidc token: {err}"); fail!(refresh_status_guard, err); } } From 9d6ffa951f7f4fac9a0c2c2cf15edb6cc7d64d5b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 11:01:44 +0100 Subject: [PATCH 580/979] doc(sdk): Specify how the `Client::observe_events` works. --- crates/matrix-sdk/src/client/mod.rs | 8 ++++++++ crates/matrix-sdk/src/event_handler/mod.rs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index a16603c028f..9c4b948b81b 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -807,6 +807,10 @@ impl Client { /// implements a [`Stream`]. The `Stream::Item` will be of type `(Ev, /// Ctx)`. /// + /// Be careful that only the most recent value can be observed. Subscribers + /// are notified when a new value is sent, but there is no guarantee + /// that they will see all values. + /// /// # Example /// /// Let's see a classical usage: @@ -879,6 +883,10 @@ impl Client { /// This method works the same way as [`Client::observe_events`], except /// that the observability will only be applied for events in the room with /// the specified ID. See that method for more details. + /// + /// Be careful that only the most recent value can be observed. Subscribers + /// are notified when a new value is sent, but there is no guarantee + /// that they will see all values. pub fn observe_room_events( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk/src/event_handler/mod.rs b/crates/matrix-sdk/src/event_handler/mod.rs index 83bffe2559f..ec1676f3931 100644 --- a/crates/matrix-sdk/src/event_handler/mod.rs +++ b/crates/matrix-sdk/src/event_handler/mod.rs @@ -541,6 +541,9 @@ impl_event_handler!(A, B, C, D, E, F, G, H); /// An observer of events (may be tailored to a room). /// +/// Only the most recent value can be observed. Subscribers are notified when a +/// new value is sent, but there is no guarantee that they will see all values. +/// /// To create such observer, use [`Client::observe_events`] or /// [`Client::observe_room_events`]. #[derive(Debug)] From edc93e62b445fd092a1763afeb51fef32f682f36 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 21 Nov 2024 12:41:39 +0100 Subject: [PATCH 581/979] task(sdk): expose the `SqliteEventCacheStore` from the SDK crate And use it in multiverse. --- crates/matrix-sdk/src/lib.rs | 2 +- labs/multiverse/src/main.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 370061fa8de..0c7864d3abe 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -80,7 +80,7 @@ pub use http_client::TransmissionProgress; #[cfg(all(feature = "e2e-encryption", feature = "sqlite"))] pub use matrix_sdk_sqlite::SqliteCryptoStore; #[cfg(feature = "sqlite")] -pub use matrix_sdk_sqlite::SqliteStateStore; +pub use matrix_sdk_sqlite::{SqliteEventCacheStore, SqliteStateStore}; pub use media::Media; pub use pusher::Pusher; pub use room::Room; diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 4b3c1d51952..f0deb997416 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -25,7 +25,7 @@ use matrix_sdk::{ events::room::message::{MessageType, RoomMessageEventContent}, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, }, - AuthSession, Client, ServerName, SqliteCryptoStore, SqliteStateStore, + AuthSession, Client, ServerName, SqliteCryptoStore, SqliteEventCacheStore, SqliteStateStore, }; use matrix_sdk_ui::{ room_list_service::{self, filters::new_filter_non_left}, @@ -949,10 +949,11 @@ async fn configure_client(server_name: String, config_path: String) -> anyhow::R let mut client_builder = Client::builder() .store_config( StoreConfig::new("multiverse".to_owned()) - .crypto_store( - SqliteCryptoStore::open(config_path.join("crypto.sqlite"), None).await?, - ) - .state_store(SqliteStateStore::open(config_path.join("state.sqlite"), None).await?), + .crypto_store(SqliteCryptoStore::open(config_path.join("crypto"), None).await?) + .state_store(SqliteStateStore::open(config_path.join("state"), None).await?) + .event_cache_store( + SqliteEventCacheStore::open(config_path.join("cache"), None).await?, + ), ) .server_name(&server_name) .with_encryption_settings(EncryptionSettings { From 2e975d9b19369619efb8393bc80292ae60080f90 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 21 Nov 2024 14:23:55 +0100 Subject: [PATCH 582/979] fix(base): all `EventCacheStoreLock` must refer to the same underlying cross-process lock And not duplicate it once per `EventCacheStoreLock`. --- crates/matrix-sdk-base/src/event_cache/store/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/mod.rs b/crates/matrix-sdk-base/src/event_cache/store/mod.rs index 76b7032c195..de84b89a5d8 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/mod.rs @@ -43,7 +43,7 @@ pub use self::{ #[derive(Clone)] pub struct EventCacheStoreLock { /// The inner cross process lock that is used to lock the `EventCacheStore`. - cross_process_lock: CrossProcessStoreLock, + cross_process_lock: Arc>, /// The store itself. /// @@ -70,11 +70,11 @@ impl EventCacheStoreLock { let store = store.into_event_cache_store(); Self { - cross_process_lock: CrossProcessStoreLock::new( + cross_process_lock: Arc::new(CrossProcessStoreLock::new( LockableEventCacheStore(store.clone()), "default".to_owned(), holder, - ), + )), store, } } From 912b121d27045a763314b8b9daee3dc636baab02 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 21 Nov 2024 16:54:43 +0100 Subject: [PATCH 583/979] feat(timeline): make more errors transparent --- crates/matrix-sdk-ui/src/timeline/error.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index 53149ee4a83..8794b467666 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -54,15 +54,15 @@ pub enum Error { UnknownEncryptionState, /// Something went wrong with the room event cache. - #[error("Something went wrong with the room event cache.")] + #[error(transparent)] EventCacheError(#[from] EventCacheError), /// An error happened during pagination. - #[error("An error happened during pagination.")] + #[error(transparent)] PaginationError(#[from] PaginationError), /// An error happened during pagination. - #[error("An error happened when loading pinned events.")] + #[error(transparent)] PinnedEventsError(#[from] PinnedEventsLoaderError), /// An error happened while operating the room's send queue. From fb5d8f29ac29b6464131707f5c98f03e6f119042 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 20 Nov 2024 16:06:16 +0100 Subject: [PATCH 584/979] feat(common): Implement `RelationalLinkedChunk`. A `RelationalLinkedChunk` is like a `LinkedChunk` but with a relational layout, similar to what we would have in a database. This is used by memory stores. The idea is to have a data layout that is similar for memory stores and for relational database stores, to represent a `LinkedChunk`. This type is also designed to receive `Update`. Applying `Update`s directly on a `LinkedChunk` is not ideal and particularly not trivial as the `Update`s do _not_ match the internal data layout of the `LinkedChunk`, they have been designed for storages, like a relational database for example. This type is not as performant as `LinkedChunk` (in terms of memory layout, CPU caches etc.). It is only designed to be used in memory stores, which are mostly used for test purposes or light usages of the SDK. --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 15 +- .../src/linked_chunk/relational.rs | 450 ++++++++++++++++++ 2 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 crates/matrix-sdk-common/src/linked_chunk/relational.rs diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index eeba29be302..a250cbab4d6 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -93,6 +93,7 @@ macro_rules! assert_items_eq { } mod as_vector; +pub mod relational; mod updates; use std::{ @@ -933,7 +934,7 @@ impl ChunkIdentifierGenerator { /// Learn more with [`ChunkIdentifierGenerator`]. #[derive(Copy, Clone, Debug, PartialEq)] #[repr(transparent)] -pub struct ChunkIdentifier(u64); +pub struct ChunkIdentifier(pub(super) u64); impl PartialEq for ChunkIdentifier { fn eq(&self, other: &u64) -> bool { @@ -945,7 +946,7 @@ impl PartialEq for ChunkIdentifier { /// /// It's a pair of a chunk position and an item index. #[derive(Copy, Clone, Debug, PartialEq)] -pub struct Position(ChunkIdentifier, usize); +pub struct Position(pub(super) ChunkIdentifier, pub(super) usize); impl Position { /// Get the chunk identifier of the item. @@ -966,6 +967,16 @@ impl Position { pub fn decrement_index(&mut self) { self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0"); } + + /// Increment the index part (see [`Self::index`]), i.e. add 1. + /// + /// # Panic + /// + /// This method will panic if it will overflow, i.e. if the index is larger + /// than `usize::MAX`. + pub fn increment_index(&mut self) { + self.1 = self.1.checked_add(1).expect("Cannot increment the index because it's too large"); + } } /// An iterator over a [`LinkedChunk`] that traverses the chunk in backward diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs new file mode 100644 index 00000000000..f4d528bff23 --- /dev/null +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -0,0 +1,450 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation for a _relational linked chunk_, see +//! [`RelationalLinkedChunk`]. + +use crate::linked_chunk::{ChunkIdentifier, Position, Update}; + +/// A row of the [`RelationalLinkedChunk::chunks`]. +#[derive(Debug, PartialEq)] +struct ChunkRow { + previous_chunk: Option, + chunk: ChunkIdentifier, + next_chunk: Option, +} + +/// A row of the [`RelationalLinkedChunk::items`]. +#[derive(Debug, PartialEq)] +struct ItemRow { + position: Position, + item: Either, +} + +/// Kind of item. +#[derive(Debug, PartialEq)] +enum Either { + /// The content is an item. + Item(Item), + + /// The content is a gap. + Gap(Gap), +} + +/// A [`LinkedChunk`] but with a relational layout, similar to what we +/// would have in a database. +/// +/// This is used by memory stores. The idea is to have a data layout that is +/// similar for memory stores and for relational database stores, to represent a +/// [`LinkedChunk`]. +/// +/// This type is also designed to receive [`Update`]. Applying `Update`s +/// directly on a [`LinkedChunk`] is not ideal and particularly not trivial as +/// the `Update`s do _not_ match the internal data layout of the `LinkedChunk`, +/// they are been designed for storages, like a relational database for example. +/// +/// This type is not as performant as [`LinkedChunk`] (in terms of memory +/// layout, CPU caches etc.). It is only designed to be used in memory stores, +/// which are mostly used for test purposes or light usage of the SDK. +/// +/// [`LinkedChunk`]: super::LinkedChunk +#[derive(Debug)] +pub struct RelationalLinkedChunk { + /// Chunks. + chunks: Vec, + + /// Items. + items: Vec>, +} + +impl RelationalLinkedChunk { + /// Create a new relational linked chunk. + pub fn new() -> Self { + Self { chunks: Vec::new(), items: Vec::new() } + } + + /// Apply [`Update`]s. That's the only way to write data inside this + /// relational linked chunk. + pub fn apply_updates(&mut self, updates: &[Update]) + where + Item: Clone, + Gap: Clone, + { + for update in updates { + match update { + Update::NewItemsChunk { previous, new, next } => { + insert_chunk(&mut self.chunks, previous, new, next); + } + + Update::NewGapChunk { previous, new, next, gap } => { + insert_chunk(&mut self.chunks, previous, new, next); + self.items.push(ItemRow { + position: Position(*new, 0), + item: Either::Gap(gap.clone()), + }); + } + + Update::RemoveChunk(chunk_identifier) => { + remove_chunk(&mut self.chunks, chunk_identifier); + + let indices_to_remove = self + .items + .iter() + .enumerate() + .filter_map(|(nth, ItemRow { position, .. })| { + (position.chunk_identifier() == *chunk_identifier).then_some(nth) + }) + .collect::>(); + + for index_to_remove in indices_to_remove.into_iter().rev() { + self.items.remove(index_to_remove); + } + } + + Update::PushItems { at, items } => { + let mut at = *at; + + for item in items { + self.items.push(ItemRow { position: at, item: Either::Item(item.clone()) }); + at.increment_index(); + } + } + + Update::RemoveItem { at } => { + let mut entry_to_remove = None; + + for (nth, ItemRow { position, .. }) in self.items.iter_mut().enumerate() { + // Find the item to remove. + if position == at { + debug_assert!(entry_to_remove.is_none(), "Found the same entry twice"); + + entry_to_remove = Some(nth); + } + + // Update all items that come _after_ `at` to shift their index. + if position.chunk_identifier() == at.chunk_identifier() + && position.index() > at.index() + { + position.decrement_index(); + } + } + + self.items.remove(entry_to_remove.expect("Remove an unknown item")); + } + + Update::DetachLastItems { at } => { + let indices_to_remove = self + .items + .iter() + .enumerate() + .filter_map(|(nth, ItemRow { position, .. })| { + (position.chunk_identifier() == at.chunk_identifier() + && position.index() >= at.index()) + .then_some(nth) + }) + .collect::>(); + + for index_to_remove in indices_to_remove.into_iter().rev() { + self.items.remove(index_to_remove); + } + } + + Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ } + } + } + + fn insert_chunk( + chunks: &mut Vec, + previous: &Option, + new: &ChunkIdentifier, + next: &Option, + ) { + // Find the previous chunk, and update its next chunk. + if let Some(previous) = previous { + let entry_for_previous_chunk = chunks + .iter_mut() + .find(|ChunkRow { chunk, .. }| chunk == previous) + .expect("Previous chunk should be present"); + + // Insert the chunk. + entry_for_previous_chunk.next_chunk = Some(*new); + } + + // Find the next chunk, and update its previous chunk. + if let Some(next) = next { + let entry_for_next_chunk = chunks + .iter_mut() + .find(|ChunkRow { chunk, .. }| chunk == next) + .expect("Next chunk should be present"); + + // Insert the chunk. + entry_for_next_chunk.previous_chunk = Some(*new); + } + + // Insert the chunk. + chunks.push(ChunkRow { previous_chunk: *previous, chunk: *new, next_chunk: *next }); + } + + fn remove_chunk(chunks: &mut Vec, chunk_to_remove: &ChunkIdentifier) { + let entry_nth_to_remove = chunks + .iter() + .enumerate() + .find_map(|(nth, ChunkRow { chunk, .. })| (chunk == chunk_to_remove).then_some(nth)) + .expect("Remove an unknown chunk"); + + let ChunkRow { previous_chunk: previous, next_chunk: next, .. } = + chunks.remove(entry_nth_to_remove); + + // Find the previous chunk, and update its next chunk. + if let Some(previous) = previous { + let entry_for_previous_chunk = chunks + .iter_mut() + .find(|ChunkRow { chunk, .. }| *chunk == previous) + .expect("Previous chunk should be present"); + + // Insert the chunk. + entry_for_previous_chunk.next_chunk = next; + } + + // Find the next chunk, and update its previous chunk. + if let Some(next) = next { + let entry_for_next_chunk = chunks + .iter_mut() + .find(|ChunkRow { chunk, .. }| *chunk == next) + .expect("Next chunk should be present"); + + // Insert the chunk. + entry_for_next_chunk.previous_chunk = previous; + } + } + } +} + +impl Default for RelationalLinkedChunk { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::{ChunkIdentifier as CId, *}; + + #[test] + fn test_new_items_chunk() { + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates(&[ + // 0 + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // 1 after 0 + Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + // 2 before 0 + Update::NewItemsChunk { previous: None, new: CId(2), next: Some(CId(0)) }, + // 3 between 2 and 0 + Update::NewItemsChunk { previous: Some(CId(2)), new: CId(3), next: Some(CId(0)) }, + ]); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { previous_chunk: Some(CId(3)), chunk: CId(0), next_chunk: Some(CId(1)) }, + ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: None }, + ChunkRow { previous_chunk: None, chunk: CId(2), next_chunk: Some(CId(3)) }, + ChunkRow { previous_chunk: Some(CId(2)), chunk: CId(3), next_chunk: Some(CId(0)) }, + ], + ); + // Items have not been modified. + assert!(relational_linked_chunk.items.is_empty()); + } + + #[test] + fn test_new_gap_chunk() { + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates(&[ + // 0 + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // 1 after 0 + Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, + // 2 after 1 + Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, + ]); + + // Chunks are correctly links. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(1)) }, + ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: Some(CId(2)) }, + ChunkRow { previous_chunk: Some(CId(1)), chunk: CId(2), next_chunk: None }, + ], + ); + // Items contains the gap. + assert_eq!( + relational_linked_chunk.items, + &[ItemRow { position: Position(CId(1), 0), item: Either::Gap(()) }], + ); + } + + #[test] + fn test_remove_chunk() { + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates(&[ + // 0 + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // 1 after 0 + Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, + // 2 after 1 + Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, + // remove 1 + Update::RemoveChunk(CId(1)), + ]); + + // Chunks are correctly links. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(2)) }, + ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(2), next_chunk: None }, + ], + ); + // Items no longer contains the gap. + assert!(relational_linked_chunk.items.is_empty()); + } + + #[test] + fn test_push_items() { + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates(&[ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // new items on 0 + Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c'] }, + // new chunk (to test new items are pushed in the correct chunk) + Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + // new items on 1 + Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, + // new items on 0 again + Update::PushItems { at: Position(CId(0), 3), items: vec!['d', 'e'] }, + ]); + + // Chunks are correctly links. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(1)) }, + ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: None }, + ], + ); + // Items contains the pushed items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { position: Position(CId(0), 0), item: Either::Item('a') }, + ItemRow { position: Position(CId(0), 1), item: Either::Item('b') }, + ItemRow { position: Position(CId(0), 2), item: Either::Item('c') }, + ItemRow { position: Position(CId(1), 0), item: Either::Item('x') }, + ItemRow { position: Position(CId(1), 1), item: Either::Item('y') }, + ItemRow { position: Position(CId(1), 2), item: Either::Item('z') }, + ItemRow { position: Position(CId(0), 3), item: Either::Item('d') }, + ItemRow { position: Position(CId(0), 4), item: Either::Item('e') }, + ], + ); + } + + #[test] + fn test_remove_item() { + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates(&[ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // new items on 0 + Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, + // remove an item: 'a' + Update::RemoveItem { at: Position(CId(0), 0) }, + // remove an item: 'd' + Update::RemoveItem { at: Position(CId(0), 2) }, + ]); + + // Chunks are correctly links. + assert_eq!( + relational_linked_chunk.chunks, + &[ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: None }], + ); + // Items contains the pushed items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { position: Position(CId(0), 0), item: Either::Item('b') }, + ItemRow { position: Position(CId(0), 1), item: Either::Item('c') }, + ItemRow { position: Position(CId(0), 2), item: Either::Item('e') }, + ], + ); + } + + #[test] + fn test_detach_last_items() { + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates(&[ + // new chunk + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // new chunk + Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + // new items on 0 + Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, + // new items on 1 + Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, + // detach last items on 0 + Update::DetachLastItems { at: Position(CId(0), 2) }, + ]); + + // Chunks are correctly links. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(1)) }, + ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: None }, + ], + ); + // Items contains the pushed items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { position: Position(CId(0), 0), item: Either::Item('a') }, + ItemRow { position: Position(CId(0), 1), item: Either::Item('b') }, + ItemRow { position: Position(CId(1), 0), item: Either::Item('x') }, + ItemRow { position: Position(CId(1), 1), item: Either::Item('y') }, + ItemRow { position: Position(CId(1), 2), item: Either::Item('z') }, + ], + ); + } + + #[test] + fn test_start_and_end_reattach_items() { + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk + .apply_updates(&[Update::StartReattachItems, Update::EndReattachItems]); + + // Nothing happened. + assert!(relational_linked_chunk.chunks.is_empty()); + assert!(relational_linked_chunk.items.is_empty()); + } +} From 88363d8033b1c43a9338badb66f96167fdda64c6 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 20 Nov 2024 16:09:03 +0100 Subject: [PATCH 585/979] feat(base): `MemoryStore` uses `RelationalLinkedChunk` to store events. That's it. --- .../src/event_cache/store/memory_store.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index bd764cad0db..8b8b14356b0 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -16,7 +16,8 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::Update, ring_buffer::RingBuffer, + linked_chunk::{relational::RelationalLinkedChunk, Update}, + ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; use ruma::{MxcUri, OwnedMxcUri}; @@ -35,6 +36,7 @@ use crate::{ pub struct MemoryStore { media: StdRwLock)>>, leases: StdRwLock>, + events: StdRwLock>, } // SAFETY: `new_unchecked` is safe because 20 is not zero. @@ -45,6 +47,7 @@ impl Default for MemoryStore { Self { media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)), leases: Default::default(), + events: StdRwLock::new(RelationalLinkedChunk::new()), } } } @@ -72,9 +75,11 @@ impl EventCacheStore for MemoryStore { async fn handle_linked_chunk_updates( &self, - _updates: &[Update], + updates: &[Update], ) -> Result<(), Self::Error> { - todo!() + self.events.write().unwrap().apply_updates(updates); + + Ok(()) } async fn add_media_content( From 5519442ad8d4cc4ef5e4008fda3b401831a08704 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 20 Nov 2024 16:13:50 +0100 Subject: [PATCH 586/979] doc(common): Fix a typo. --- crates/matrix-sdk-common/src/linked_chunk/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index a250cbab4d6..479474c83e1 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -872,12 +872,12 @@ impl Drop for LinkedChunk { } /// A [`LinkedChunk`] can be safely sent over thread boundaries if `Item: Send` -/// and `Gap: Send`. The only unsafe part if around the `NonNull`, but the API +/// and `Gap: Send`. The only unsafe part is around the `NonNull`, but the API /// and the lifetimes to deref them are designed safely. unsafe impl Send for LinkedChunk {} /// A [`LinkedChunk`] can be safely share between threads if `Item: Sync` and -/// `Gap: Sync`. The only unsafe part if around the `NonNull`, but the API and +/// `Gap: Sync`. The only unsafe part is around the `NonNull`, but the API and /// the lifetimes to deref them are designed safely. unsafe impl Sync for LinkedChunk {} From fe52b4cb78ff73875e2f5ce3a53f403eeae03b24 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 09:51:25 +0100 Subject: [PATCH 587/979] feat(common): `EventCacheStore::handle_linked_chunk_updates` takes a `&RoomId`. --- .../src/event_cache/store/memory_store.rs | 5 +- .../src/event_cache/store/traits.rs | 6 +- .../src/linked_chunk/relational.rs | 157 +++++++++++------- .../src/event_cache_store.rs | 3 +- 4 files changed, 102 insertions(+), 69 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 8b8b14356b0..c7cf9f7501f 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -20,7 +20,7 @@ use matrix_sdk_common::{ ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; -use ruma::{MxcUri, OwnedMxcUri}; +use ruma::{MxcUri, OwnedMxcUri, RoomId}; use super::{EventCacheStore, EventCacheStoreError, Result}; use crate::{ @@ -75,9 +75,10 @@ impl EventCacheStore for MemoryStore { async fn handle_linked_chunk_updates( &self, + room_id: &RoomId, updates: &[Update], ) -> Result<(), Self::Error> { - self.events.write().unwrap().apply_updates(updates); + self.events.write().unwrap().apply_updates(room_id, updates); Ok(()) } diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index dde80830973..2584b8881ed 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -16,7 +16,7 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; use matrix_sdk_common::{linked_chunk::Update, AsyncTraitDeps}; -use ruma::MxcUri; +use ruma::{MxcUri, RoomId}; use super::EventCacheStoreError; use crate::{ @@ -45,6 +45,7 @@ pub trait EventCacheStore: AsyncTraitDeps { /// in-memory. This method aims at forwarding this update inside this store. async fn handle_linked_chunk_updates( &self, + room_id: &RoomId, updates: &[Update], ) -> Result<(), Self::Error>; @@ -144,9 +145,10 @@ impl EventCacheStore for EraseEventCacheStoreError { async fn handle_linked_chunk_updates( &self, + room_id: &RoomId, updates: &[Update], ) -> Result<(), Self::Error> { - self.0.handle_linked_chunk_updates(updates).await.map_err(Into::into) + self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into) } async fn add_media_content( diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index f4d528bff23..d410b2fa8db 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -15,6 +15,8 @@ //! Implementation for a _relational linked chunk_, see //! [`RelationalLinkedChunk`]. +use ruma::RoomId; + use crate::linked_chunk::{ChunkIdentifier, Position, Update}; /// A row of the [`RelationalLinkedChunk::chunks`]. @@ -76,7 +78,7 @@ impl RelationalLinkedChunk { /// Apply [`Update`]s. That's the only way to write data inside this /// relational linked chunk. - pub fn apply_updates(&mut self, updates: &[Update]) + pub fn apply_updates(&mut self, _room_id: &RoomId, updates: &[Update]) where Item: Clone, Gap: Clone, @@ -239,22 +241,28 @@ impl Default for RelationalLinkedChunk { #[cfg(test)] mod tests { + use ruma::room_id; + use super::{ChunkIdentifier as CId, *}; #[test] fn test_new_items_chunk() { + let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); - relational_linked_chunk.apply_updates(&[ - // 0 - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, - // 1 after 0 - Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, - // 2 before 0 - Update::NewItemsChunk { previous: None, new: CId(2), next: Some(CId(0)) }, - // 3 between 2 and 0 - Update::NewItemsChunk { previous: Some(CId(2)), new: CId(3), next: Some(CId(0)) }, - ]); + relational_linked_chunk.apply_updates( + room_id, + &[ + // 0 + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // 1 after 0 + Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + // 2 before 0 + Update::NewItemsChunk { previous: None, new: CId(2), next: Some(CId(0)) }, + // 3 between 2 and 0 + Update::NewItemsChunk { previous: Some(CId(2)), new: CId(3), next: Some(CId(0)) }, + ], + ); // Chunks are correctly linked. assert_eq!( @@ -272,16 +280,20 @@ mod tests { #[test] fn test_new_gap_chunk() { + let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); - relational_linked_chunk.apply_updates(&[ - // 0 - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, - // 1 after 0 - Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, - // 2 after 1 - Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, - ]); + relational_linked_chunk.apply_updates( + room_id, + &[ + // 0 + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // 1 after 0 + Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, + // 2 after 1 + Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, + ], + ); // Chunks are correctly links. assert_eq!( @@ -301,18 +313,22 @@ mod tests { #[test] fn test_remove_chunk() { + let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); - relational_linked_chunk.apply_updates(&[ - // 0 - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, - // 1 after 0 - Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, - // 2 after 1 - Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, - // remove 1 - Update::RemoveChunk(CId(1)), - ]); + relational_linked_chunk.apply_updates( + room_id, + &[ + // 0 + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // 1 after 0 + Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, + // 2 after 1 + Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, + // remove 1 + Update::RemoveChunk(CId(1)), + ], + ); // Chunks are correctly links. assert_eq!( @@ -328,20 +344,24 @@ mod tests { #[test] fn test_push_items() { + let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); - relational_linked_chunk.apply_updates(&[ - // new chunk (this is not mandatory for this test, but let's try to be realistic) - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, - // new items on 0 - Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c'] }, - // new chunk (to test new items are pushed in the correct chunk) - Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, - // new items on 1 - Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, - // new items on 0 again - Update::PushItems { at: Position(CId(0), 3), items: vec!['d', 'e'] }, - ]); + relational_linked_chunk.apply_updates( + room_id, + &[ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // new items on 0 + Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c'] }, + // new chunk (to test new items are pushed in the correct chunk) + Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + // new items on 1 + Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, + // new items on 0 again + Update::PushItems { at: Position(CId(0), 3), items: vec!['d', 'e'] }, + ], + ); // Chunks are correctly links. assert_eq!( @@ -369,18 +389,22 @@ mod tests { #[test] fn test_remove_item() { + let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); - relational_linked_chunk.apply_updates(&[ - // new chunk (this is not mandatory for this test, but let's try to be realistic) - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, - // new items on 0 - Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, - // remove an item: 'a' - Update::RemoveItem { at: Position(CId(0), 0) }, - // remove an item: 'd' - Update::RemoveItem { at: Position(CId(0), 2) }, - ]); + relational_linked_chunk.apply_updates( + room_id, + &[ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // new items on 0 + Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, + // remove an item: 'a' + Update::RemoveItem { at: Position(CId(0), 0) }, + // remove an item: 'd' + Update::RemoveItem { at: Position(CId(0), 2) }, + ], + ); // Chunks are correctly links. assert_eq!( @@ -400,20 +424,24 @@ mod tests { #[test] fn test_detach_last_items() { + let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); - relational_linked_chunk.apply_updates(&[ - // new chunk - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, - // new chunk - Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, - // new items on 0 - Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, - // new items on 1 - Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, - // detach last items on 0 - Update::DetachLastItems { at: Position(CId(0), 2) }, - ]); + relational_linked_chunk.apply_updates( + room_id, + &[ + // new chunk + Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + // new chunk + Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + // new items on 0 + Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, + // new items on 1 + Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, + // detach last items on 0 + Update::DetachLastItems { at: Position(CId(0), 2) }, + ], + ); // Chunks are correctly links. assert_eq!( @@ -438,10 +466,11 @@ mod tests { #[test] fn test_start_and_end_reattach_items() { + let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); relational_linked_chunk - .apply_updates(&[Update::StartReattachItems, Update::EndReattachItems]); + .apply_updates(room_id, &[Update::StartReattachItems, Update::EndReattachItems]); // Nothing happened. assert!(relational_linked_chunk.chunks.is_empty()); diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index dc74999e227..f6b62e76216 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -8,7 +8,7 @@ use matrix_sdk_base::{ media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; -use ruma::MilliSecondsSinceUnixEpoch; +use ruma::{MilliSecondsSinceUnixEpoch, RoomId}; use rusqlite::OptionalExtension; use tokio::fs; use tracing::debug; @@ -185,6 +185,7 @@ impl EventCacheStore for SqliteEventCacheStore { async fn handle_linked_chunk_updates( &self, + _room_id: &RoomId, _updates: &[Update], ) -> Result<(), Self::Error> { todo!() From 1dbb494b946ae39b6fb9f7f90770ed249c471e75 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 10:16:00 +0100 Subject: [PATCH 588/979] feat(common): `RelationalLinkedChunk` stores the `RoomId`. --- .../src/linked_chunk/relational.rs | 287 ++++++++++++++---- 1 file changed, 233 insertions(+), 54 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index d410b2fa8db..4418bf3e654 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -15,13 +15,14 @@ //! Implementation for a _relational linked chunk_, see //! [`RelationalLinkedChunk`]. -use ruma::RoomId; +use ruma::{OwnedRoomId, RoomId}; use crate::linked_chunk::{ChunkIdentifier, Position, Update}; /// A row of the [`RelationalLinkedChunk::chunks`]. #[derive(Debug, PartialEq)] struct ChunkRow { + room_id: OwnedRoomId, previous_chunk: Option, chunk: ChunkIdentifier, next_chunk: Option, @@ -30,6 +31,7 @@ struct ChunkRow { /// A row of the [`RelationalLinkedChunk::items`]. #[derive(Debug, PartialEq)] struct ItemRow { + room_id: OwnedRoomId, position: Position, item: Either, } @@ -78,7 +80,7 @@ impl RelationalLinkedChunk { /// Apply [`Update`]s. That's the only way to write data inside this /// relational linked chunk. - pub fn apply_updates(&mut self, _room_id: &RoomId, updates: &[Update]) + pub fn apply_updates(&mut self, room_id: &RoomId, updates: &[Update]) where Item: Clone, Gap: Clone, @@ -86,27 +88,32 @@ impl RelationalLinkedChunk { for update in updates { match update { Update::NewItemsChunk { previous, new, next } => { - insert_chunk(&mut self.chunks, previous, new, next); + insert_chunk(&mut self.chunks, room_id, previous, new, next); } Update::NewGapChunk { previous, new, next, gap } => { - insert_chunk(&mut self.chunks, previous, new, next); + insert_chunk(&mut self.chunks, room_id, previous, new, next); self.items.push(ItemRow { + room_id: room_id.to_owned(), position: Position(*new, 0), item: Either::Gap(gap.clone()), }); } Update::RemoveChunk(chunk_identifier) => { - remove_chunk(&mut self.chunks, chunk_identifier); + remove_chunk(&mut self.chunks, room_id, chunk_identifier); let indices_to_remove = self .items .iter() .enumerate() - .filter_map(|(nth, ItemRow { position, .. })| { - (position.chunk_identifier() == *chunk_identifier).then_some(nth) - }) + .filter_map( + |(nth, ItemRow { room_id: room_id_candidate, position, .. })| { + (room_id == room_id_candidate + && position.chunk_identifier() == *chunk_identifier) + .then_some(nth) + }, + ) .collect::>(); for index_to_remove in indices_to_remove.into_iter().rev() { @@ -118,7 +125,11 @@ impl RelationalLinkedChunk { let mut at = *at; for item in items { - self.items.push(ItemRow { position: at, item: Either::Item(item.clone()) }); + self.items.push(ItemRow { + room_id: room_id.to_owned(), + position: at, + item: Either::Item(item.clone()), + }); at.increment_index(); } } @@ -126,7 +137,14 @@ impl RelationalLinkedChunk { Update::RemoveItem { at } => { let mut entry_to_remove = None; - for (nth, ItemRow { position, .. }) in self.items.iter_mut().enumerate() { + for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in + self.items.iter_mut().enumerate() + { + // Filter by room ID. + if room_id != room_id_candidate { + continue; + } + // Find the item to remove. if position == at { debug_assert!(entry_to_remove.is_none(), "Found the same entry twice"); @@ -150,11 +168,14 @@ impl RelationalLinkedChunk { .items .iter() .enumerate() - .filter_map(|(nth, ItemRow { position, .. })| { - (position.chunk_identifier() == at.chunk_identifier() - && position.index() >= at.index()) - .then_some(nth) - }) + .filter_map( + |(nth, ItemRow { room_id: room_id_candidate, position, .. })| { + (room_id == room_id_candidate + && position.chunk_identifier() == at.chunk_identifier() + && position.index() >= at.index()) + .then_some(nth) + }, + ) .collect::>(); for index_to_remove in indices_to_remove.into_iter().rev() { @@ -168,6 +189,7 @@ impl RelationalLinkedChunk { fn insert_chunk( chunks: &mut Vec, + room_id: &RoomId, previous: &Option, new: &ChunkIdentifier, next: &Option, @@ -176,7 +198,9 @@ impl RelationalLinkedChunk { if let Some(previous) = previous { let entry_for_previous_chunk = chunks .iter_mut() - .find(|ChunkRow { chunk, .. }| chunk == previous) + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + room_id == room_id_candidate && chunk == previous + }) .expect("Previous chunk should be present"); // Insert the chunk. @@ -187,7 +211,9 @@ impl RelationalLinkedChunk { if let Some(next) = next { let entry_for_next_chunk = chunks .iter_mut() - .find(|ChunkRow { chunk, .. }| chunk == next) + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + room_id == room_id_candidate && chunk == next + }) .expect("Next chunk should be present"); // Insert the chunk. @@ -195,24 +221,37 @@ impl RelationalLinkedChunk { } // Insert the chunk. - chunks.push(ChunkRow { previous_chunk: *previous, chunk: *new, next_chunk: *next }); + chunks.push(ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: *previous, + chunk: *new, + next_chunk: *next, + }); } - fn remove_chunk(chunks: &mut Vec, chunk_to_remove: &ChunkIdentifier) { + fn remove_chunk( + chunks: &mut Vec, + room_id: &RoomId, + chunk_to_remove: &ChunkIdentifier, + ) { let entry_nth_to_remove = chunks .iter() .enumerate() - .find_map(|(nth, ChunkRow { chunk, .. })| (chunk == chunk_to_remove).then_some(nth)) + .find_map(|(nth, ChunkRow { room_id: room_id_candidate, chunk, .. })| { + (room_id == room_id_candidate && chunk == chunk_to_remove).then_some(nth) + }) .expect("Remove an unknown chunk"); - let ChunkRow { previous_chunk: previous, next_chunk: next, .. } = + let ChunkRow { room_id, previous_chunk: previous, next_chunk: next, .. } = chunks.remove(entry_nth_to_remove); // Find the previous chunk, and update its next chunk. if let Some(previous) = previous { let entry_for_previous_chunk = chunks .iter_mut() - .find(|ChunkRow { chunk, .. }| *chunk == previous) + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + &room_id == room_id_candidate && *chunk == previous + }) .expect("Previous chunk should be present"); // Insert the chunk. @@ -223,7 +262,9 @@ impl RelationalLinkedChunk { if let Some(next) = next { let entry_for_next_chunk = chunks .iter_mut() - .find(|ChunkRow { chunk, .. }| *chunk == next) + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + &room_id == room_id_candidate && *chunk == next + }) .expect("Next chunk should be present"); // Insert the chunk. @@ -268,10 +309,30 @@ mod tests { assert_eq!( relational_linked_chunk.chunks, &[ - ChunkRow { previous_chunk: Some(CId(3)), chunk: CId(0), next_chunk: Some(CId(1)) }, - ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: None }, - ChunkRow { previous_chunk: None, chunk: CId(2), next_chunk: Some(CId(3)) }, - ChunkRow { previous_chunk: Some(CId(2)), chunk: CId(3), next_chunk: Some(CId(0)) }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(3)), + chunk: CId(0), + next_chunk: Some(CId(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(0)), + chunk: CId(1), + next_chunk: None + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId(2), + next_chunk: Some(CId(3)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(2)), + chunk: CId(3), + next_chunk: Some(CId(0)) + }, ], ); // Items have not been modified. @@ -299,15 +360,34 @@ mod tests { assert_eq!( relational_linked_chunk.chunks, &[ - ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(1)) }, - ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: Some(CId(2)) }, - ChunkRow { previous_chunk: Some(CId(1)), chunk: CId(2), next_chunk: None }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId(0), + next_chunk: Some(CId(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(0)), + chunk: CId(1), + next_chunk: Some(CId(2)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(1)), + chunk: CId(2), + next_chunk: None + }, ], ); // Items contains the gap. assert_eq!( relational_linked_chunk.items, - &[ItemRow { position: Position(CId(1), 0), item: Either::Gap(()) }], + &[ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(1), 0), + item: Either::Gap(()) + }], ); } @@ -334,8 +414,18 @@ mod tests { assert_eq!( relational_linked_chunk.chunks, &[ - ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(2)) }, - ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(2), next_chunk: None }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId(0), + next_chunk: Some(CId(2)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(0)), + chunk: CId(2), + next_chunk: None + }, ], ); // Items no longer contains the gap. @@ -367,22 +457,64 @@ mod tests { assert_eq!( relational_linked_chunk.chunks, &[ - ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(1)) }, - ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: None }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId(0), + next_chunk: Some(CId(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(0)), + chunk: CId(1), + next_chunk: None + }, ], ); // Items contains the pushed items. assert_eq!( relational_linked_chunk.items, &[ - ItemRow { position: Position(CId(0), 0), item: Either::Item('a') }, - ItemRow { position: Position(CId(0), 1), item: Either::Item('b') }, - ItemRow { position: Position(CId(0), 2), item: Either::Item('c') }, - ItemRow { position: Position(CId(1), 0), item: Either::Item('x') }, - ItemRow { position: Position(CId(1), 1), item: Either::Item('y') }, - ItemRow { position: Position(CId(1), 2), item: Either::Item('z') }, - ItemRow { position: Position(CId(0), 3), item: Either::Item('d') }, - ItemRow { position: Position(CId(0), 4), item: Either::Item('e') }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 0), + item: Either::Item('a') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 1), + item: Either::Item('b') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 2), + item: Either::Item('c') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(1), 0), + item: Either::Item('x') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(1), 1), + item: Either::Item('y') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(1), 2), + item: Either::Item('z') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 3), + item: Either::Item('d') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 4), + item: Either::Item('e') + }, ], ); } @@ -409,15 +541,32 @@ mod tests { // Chunks are correctly links. assert_eq!( relational_linked_chunk.chunks, - &[ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: None }], + &[ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId(0), + next_chunk: None + }], ); // Items contains the pushed items. assert_eq!( relational_linked_chunk.items, &[ - ItemRow { position: Position(CId(0), 0), item: Either::Item('b') }, - ItemRow { position: Position(CId(0), 1), item: Either::Item('c') }, - ItemRow { position: Position(CId(0), 2), item: Either::Item('e') }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 0), + item: Either::Item('b') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 1), + item: Either::Item('c') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 2), + item: Either::Item('e') + }, ], ); } @@ -447,19 +596,49 @@ mod tests { assert_eq!( relational_linked_chunk.chunks, &[ - ChunkRow { previous_chunk: None, chunk: CId(0), next_chunk: Some(CId(1)) }, - ChunkRow { previous_chunk: Some(CId(0)), chunk: CId(1), next_chunk: None }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId(0), + next_chunk: Some(CId(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId(0)), + chunk: CId(1), + next_chunk: None + }, ], ); // Items contains the pushed items. assert_eq!( relational_linked_chunk.items, &[ - ItemRow { position: Position(CId(0), 0), item: Either::Item('a') }, - ItemRow { position: Position(CId(0), 1), item: Either::Item('b') }, - ItemRow { position: Position(CId(1), 0), item: Either::Item('x') }, - ItemRow { position: Position(CId(1), 1), item: Either::Item('y') }, - ItemRow { position: Position(CId(1), 2), item: Either::Item('z') }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 0), + item: Either::Item('a') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(0), 1), + item: Either::Item('b') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(1), 0), + item: Either::Item('x') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(1), 1), + item: Either::Item('y') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position(CId(1), 2), + item: Either::Item('z') + }, ], ); } From db9ee9d87ba8b51c4bc121fe55a4518c696b2a88 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 15:43:09 +0100 Subject: [PATCH 589/979] refactor: Add constructors for `Position` and `ChunkIdentifier`. This patch adds constructors for `Position` and `ChunkIdentifier` so that we keep their inner values private. --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 21 ++- .../src/linked_chunk/relational.rs | 169 ++++++++++-------- 2 files changed, 114 insertions(+), 76 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 479474c83e1..0aae2ecc942 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -934,7 +934,19 @@ impl ChunkIdentifierGenerator { /// Learn more with [`ChunkIdentifierGenerator`]. #[derive(Copy, Clone, Debug, PartialEq)] #[repr(transparent)] -pub struct ChunkIdentifier(pub(super) u64); +pub struct ChunkIdentifier(u64); + +impl ChunkIdentifier { + /// Create a new [`ChunkIdentifier`]. + pub(super) fn new(identifier: u64) -> Self { + Self(identifier) + } + + /// Get the underlying identifier. + fn index(&self) -> u64 { + self.0 + } +} impl PartialEq for ChunkIdentifier { fn eq(&self, other: &u64) -> bool { @@ -946,9 +958,14 @@ impl PartialEq for ChunkIdentifier { /// /// It's a pair of a chunk position and an item index. #[derive(Copy, Clone, Debug, PartialEq)] -pub struct Position(pub(super) ChunkIdentifier, pub(super) usize); +pub struct Position(ChunkIdentifier, usize); impl Position { + /// Create a new [`Position`]. + pub(super) fn new(chunk_identifier: ChunkIdentifier, index: usize) -> Self { + Self(chunk_identifier, index) + } + /// Get the chunk identifier of the item. pub fn chunk_identifier(&self) -> ChunkIdentifier { self.0 diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index 4418bf3e654..f7c2f9c7e24 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -56,7 +56,8 @@ enum Either { /// This type is also designed to receive [`Update`]. Applying `Update`s /// directly on a [`LinkedChunk`] is not ideal and particularly not trivial as /// the `Update`s do _not_ match the internal data layout of the `LinkedChunk`, -/// they are been designed for storages, like a relational database for example. +/// they have been designed for storages, like a relational database for +/// example. /// /// This type is not as performant as [`LinkedChunk`] (in terms of memory /// layout, CPU caches etc.). It is only designed to be used in memory stores, @@ -95,7 +96,7 @@ impl RelationalLinkedChunk { insert_chunk(&mut self.chunks, room_id, previous, new, next); self.items.push(ItemRow { room_id: room_id.to_owned(), - position: Position(*new, 0), + position: Position::new(*new, 0), item: Either::Gap(gap.clone()), }); } @@ -295,13 +296,17 @@ mod tests { room_id, &[ // 0 - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // 1 after 0 - Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None }, // 2 before 0 - Update::NewItemsChunk { previous: None, new: CId(2), next: Some(CId(0)) }, + Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) }, // 3 between 2 and 0 - Update::NewItemsChunk { previous: Some(CId(2)), new: CId(3), next: Some(CId(0)) }, + Update::NewItemsChunk { + previous: Some(CId::new(2)), + new: CId::new(3), + next: Some(CId::new(0)), + }, ], ); @@ -311,27 +316,27 @@ mod tests { &[ ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(3)), - chunk: CId(0), - next_chunk: Some(CId(1)) + previous_chunk: Some(CId::new(3)), + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) }, ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(0)), - chunk: CId(1), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), next_chunk: None }, ChunkRow { room_id: room_id.to_owned(), previous_chunk: None, - chunk: CId(2), - next_chunk: Some(CId(3)) + chunk: CId::new(2), + next_chunk: Some(CId::new(3)) }, ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(2)), - chunk: CId(3), - next_chunk: Some(CId(0)) + previous_chunk: Some(CId::new(2)), + chunk: CId::new(3), + next_chunk: Some(CId::new(0)) }, ], ); @@ -348,11 +353,16 @@ mod tests { room_id, &[ // 0 - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // 1 after 0 - Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, + Update::NewGapChunk { + previous: Some(CId::new(0)), + new: CId::new(1), + next: None, + gap: (), + }, // 2 after 1 - Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, + Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None }, ], ); @@ -363,19 +373,19 @@ mod tests { ChunkRow { room_id: room_id.to_owned(), previous_chunk: None, - chunk: CId(0), - next_chunk: Some(CId(1)) + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) }, ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(0)), - chunk: CId(1), - next_chunk: Some(CId(2)) + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), + next_chunk: Some(CId::new(2)) }, ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(1)), - chunk: CId(2), + previous_chunk: Some(CId::new(1)), + chunk: CId::new(2), next_chunk: None }, ], @@ -385,7 +395,7 @@ mod tests { relational_linked_chunk.items, &[ItemRow { room_id: room_id.to_owned(), - position: Position(CId(1), 0), + position: Position::new(CId::new(1), 0), item: Either::Gap(()) }], ); @@ -400,13 +410,18 @@ mod tests { room_id, &[ // 0 - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // 1 after 0 - Update::NewGapChunk { previous: Some(CId(0)), new: CId(1), next: None, gap: () }, + Update::NewGapChunk { + previous: Some(CId::new(0)), + new: CId::new(1), + next: None, + gap: (), + }, // 2 after 1 - Update::NewItemsChunk { previous: Some(CId(1)), new: CId(2), next: None }, + Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None }, // remove 1 - Update::RemoveChunk(CId(1)), + Update::RemoveChunk(CId::new(1)), ], ); @@ -417,13 +432,13 @@ mod tests { ChunkRow { room_id: room_id.to_owned(), previous_chunk: None, - chunk: CId(0), - next_chunk: Some(CId(2)) + chunk: CId::new(0), + next_chunk: Some(CId::new(2)) }, ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(0)), - chunk: CId(2), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(2), next_chunk: None }, ], @@ -441,15 +456,15 @@ mod tests { room_id, &[ // new chunk (this is not mandatory for this test, but let's try to be realistic) - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // new items on 0 - Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c'] }, + Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] }, // new chunk (to test new items are pushed in the correct chunk) - Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None }, // new items on 1 - Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, + Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] }, // new items on 0 again - Update::PushItems { at: Position(CId(0), 3), items: vec!['d', 'e'] }, + Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] }, ], ); @@ -460,13 +475,13 @@ mod tests { ChunkRow { room_id: room_id.to_owned(), previous_chunk: None, - chunk: CId(0), - next_chunk: Some(CId(1)) + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) }, ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(0)), - chunk: CId(1), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), next_chunk: None }, ], @@ -477,42 +492,42 @@ mod tests { &[ ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 0), + position: Position::new(CId::new(0), 0), item: Either::Item('a') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 1), + position: Position::new(CId::new(0), 1), item: Either::Item('b') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 2), + position: Position::new(CId::new(0), 2), item: Either::Item('c') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(1), 0), + position: Position::new(CId::new(1), 0), item: Either::Item('x') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(1), 1), + position: Position::new(CId::new(1), 1), item: Either::Item('y') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(1), 2), + position: Position::new(CId::new(1), 2), item: Either::Item('z') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 3), + position: Position::new(CId::new(0), 3), item: Either::Item('d') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 4), + position: Position::new(CId::new(0), 4), item: Either::Item('e') }, ], @@ -528,13 +543,16 @@ mod tests { room_id, &[ // new chunk (this is not mandatory for this test, but let's try to be realistic) - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // new items on 0 - Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec!['a', 'b', 'c', 'd', 'e'], + }, // remove an item: 'a' - Update::RemoveItem { at: Position(CId(0), 0) }, + Update::RemoveItem { at: Position::new(CId::new(0), 0) }, // remove an item: 'd' - Update::RemoveItem { at: Position(CId(0), 2) }, + Update::RemoveItem { at: Position::new(CId::new(0), 2) }, ], ); @@ -544,7 +562,7 @@ mod tests { &[ChunkRow { room_id: room_id.to_owned(), previous_chunk: None, - chunk: CId(0), + chunk: CId::new(0), next_chunk: None }], ); @@ -554,17 +572,17 @@ mod tests { &[ ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 0), + position: Position::new(CId::new(0), 0), item: Either::Item('b') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 1), + position: Position::new(CId::new(0), 1), item: Either::Item('c') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 2), + position: Position::new(CId::new(0), 2), item: Either::Item('e') }, ], @@ -580,15 +598,18 @@ mod tests { room_id, &[ // new chunk - Update::NewItemsChunk { previous: None, new: CId(0), next: None }, + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // new chunk - Update::NewItemsChunk { previous: Some(CId(0)), new: CId(1), next: None }, + Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None }, // new items on 0 - Update::PushItems { at: Position(CId(0), 0), items: vec!['a', 'b', 'c', 'd', 'e'] }, + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec!['a', 'b', 'c', 'd', 'e'], + }, // new items on 1 - Update::PushItems { at: Position(CId(1), 0), items: vec!['x', 'y', 'z'] }, + Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] }, // detach last items on 0 - Update::DetachLastItems { at: Position(CId(0), 2) }, + Update::DetachLastItems { at: Position::new(CId::new(0), 2) }, ], ); @@ -599,13 +620,13 @@ mod tests { ChunkRow { room_id: room_id.to_owned(), previous_chunk: None, - chunk: CId(0), - next_chunk: Some(CId(1)) + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) }, ChunkRow { room_id: room_id.to_owned(), - previous_chunk: Some(CId(0)), - chunk: CId(1), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), next_chunk: None }, ], @@ -616,27 +637,27 @@ mod tests { &[ ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 0), + position: Position::new(CId::new(0), 0), item: Either::Item('a') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(0), 1), + position: Position::new(CId::new(0), 1), item: Either::Item('b') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(1), 0), + position: Position::new(CId::new(1), 0), item: Either::Item('x') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(1), 1), + position: Position::new(CId::new(1), 1), item: Either::Item('y') }, ItemRow { room_id: room_id.to_owned(), - position: Position(CId(1), 2), + position: Position::new(CId::new(1), 2), item: Either::Item('z') }, ], From faa8aa2b9cf6afcfb4bb439d64d60ff5c982c979 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 15:53:28 +0100 Subject: [PATCH 590/979] fix(base): Move all fields of `MemoryStore` inside a `StdRwLock<_>`. This patch creates a new `MemoryStoreInner` and moves all fields from `MemoryStore` into this new type. All locks are removed, but a new lock is added around `MemoryStoreInner`. That way we have a single lock. --- .../src/event_cache/store/memory_store.rs | 58 ++++++++++++------- crates/matrix-sdk-common/src/store_locks.rs | 7 +-- .../src/store/memorystore.rs | 2 +- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index c7cf9f7501f..ba9c3d24b80 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -34,9 +34,14 @@ use crate::{ #[allow(clippy::type_complexity)] #[derive(Debug)] pub struct MemoryStore { - media: StdRwLock)>>, - leases: StdRwLock>, - events: StdRwLock>, + inner: StdRwLock, +} + +#[derive(Debug)] +struct MemoryStoreInner { + media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec)>, + leases: HashMap, + events: RelationalLinkedChunk, } // SAFETY: `new_unchecked` is safe because 20 is not zero. @@ -45,9 +50,11 @@ const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) impl Default for MemoryStore { fn default() -> Self { Self { - media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)), - leases: Default::default(), - events: StdRwLock::new(RelationalLinkedChunk::new()), + inner: StdRwLock::new(MemoryStoreInner { + media: RingBuffer::new(NUMBER_OF_MEDIAS), + leases: Default::default(), + events: RelationalLinkedChunk::new(), + }), } } } @@ -70,7 +77,9 @@ impl EventCacheStore for MemoryStore { key: &str, holder: &str, ) -> Result { - Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) + let mut inner = self.inner.write().unwrap(); + + Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder)) } async fn handle_linked_chunk_updates( @@ -78,9 +87,9 @@ impl EventCacheStore for MemoryStore { room_id: &RoomId, updates: &[Update], ) -> Result<(), Self::Error> { - self.events.write().unwrap().apply_updates(room_id, updates); + let mut inner = self.inner.write().unwrap(); - Ok(()) + Ok(inner.events.apply_updates(updates)) } async fn add_media_content( @@ -90,8 +99,10 @@ impl EventCacheStore for MemoryStore { ) -> Result<()> { // Avoid duplication. Let's try to remove it first. self.remove_media_content(request).await?; + // Now, let's add it. - self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data)); + let mut inner = self.inner.write().unwrap(); + inner.media.push((request.uri().to_owned(), request.unique_key(), data)); Ok(()) } @@ -103,8 +114,10 @@ impl EventCacheStore for MemoryStore { ) -> Result<(), Self::Error> { let expected_key = from.unique_key(); - let mut medias = self.media.write().unwrap(); - if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) { + let mut inner = self.inner.write().unwrap(); + + if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key) + { *mxc = to.uri().to_owned(); *key = to.unique_key(); } @@ -115,8 +128,9 @@ impl EventCacheStore for MemoryStore { async fn get_media_content(&self, request: &MediaRequestParameters) -> Result>> { let expected_key = request.unique_key(); - let media = self.media.read().unwrap(); - Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| { + let inner = self.inner.write().unwrap(); + + Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| { (media_key == &expected_key).then(|| media_content.to_owned()) })) } @@ -124,23 +138,27 @@ impl EventCacheStore for MemoryStore { async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> { let expected_key = request.unique_key(); - let mut media = self.media.write().unwrap(); - let Some(index) = media + let mut inner = self.inner.write().unwrap(); + + let Some(index) = inner + .media .iter() .position(|(_media_uri, media_key, _media_content)| media_key == &expected_key) else { return Ok(()); }; - media.remove(index); + inner.media.remove(index); Ok(()) } async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - let mut media = self.media.write().unwrap(); + let mut inner = self.inner.write().unwrap(); + let expected_key = uri.to_owned(); - let positions = media + let positions = inner + .media .iter() .enumerate() .filter_map(|(position, (media_uri, _media_key, _media_content))| { @@ -150,7 +168,7 @@ impl EventCacheStore for MemoryStore { // Iterate in reverse-order so that positions stay valid after first removals. for position in positions.into_iter().rev() { - media.remove(position); + inner.media.remove(position); } Ok(()) diff --git a/crates/matrix-sdk-common/src/store_locks.rs b/crates/matrix-sdk-common/src/store_locks.rs index e31acc5ca0b..08af735ed22 100644 --- a/crates/matrix-sdk-common/src/store_locks.rs +++ b/crates/matrix-sdk-common/src/store_locks.rs @@ -361,7 +361,7 @@ mod tests { impl TestStore { fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool { - try_take_leased_lock(&self.leases, lease_duration_ms, key, holder) + try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder) } } @@ -502,12 +502,11 @@ mod tests { pub mod memory_store_helper { use std::{ collections::{hash_map::Entry, HashMap}, - sync::RwLock, time::{Duration, Instant}, }; pub fn try_take_leased_lock( - leases: &RwLock>, + leases: &mut HashMap, lease_duration_ms: u32, key: &str, holder: &str, @@ -515,7 +514,7 @@ pub mod memory_store_helper { let now = Instant::now(); let expiration = now + Duration::from_millis(lease_duration_ms.into()); - match leases.write().unwrap().entry(key.to_owned()) { + match leases.entry(key.to_owned()) { // There is an existing holder. Entry::Occupied(mut entry) => { let (current_holder, current_expiration) = entry.get_mut(); diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 6b177ea2641..8d5da350300 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -632,7 +632,7 @@ impl CryptoStore for MemoryStore { key: &str, holder: &str, ) -> Result { - Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) + Ok(try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)) } } From 24b968ad3998c483e8afaa18c6e739c9721111b0 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 16:37:47 +0100 Subject: [PATCH 591/979] refactor: `EventCacheStore::handle_linked_chunk_updates` takes a `Vec`. This patch updates `EventCacheStore::handle_linked_chunk_updates` to take a `Vec>` instead of `&[Update]`. In fact, `linked_chunk::ObservableUpdates::take()` already returns a `Vec>`; we can simply forward this `Vec` up to here without any further clones. --- .../src/event_cache/store/memory_store.rs | 7 ++- .../src/event_cache/store/traits.rs | 4 +- .../src/linked_chunk/relational.rs | 58 +++++++++---------- .../src/event_cache_store.rs | 2 +- 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index ba9c3d24b80..92109029d3f 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -85,11 +85,12 @@ impl EventCacheStore for MemoryStore { async fn handle_linked_chunk_updates( &self, room_id: &RoomId, - updates: &[Update], + updates: Vec>, ) -> Result<(), Self::Error> { let mut inner = self.inner.write().unwrap(); + inner.events.apply_updates(room_id, updates); - Ok(inner.events.apply_updates(updates)) + Ok(()) } async fn add_media_content( @@ -128,7 +129,7 @@ impl EventCacheStore for MemoryStore { async fn get_media_content(&self, request: &MediaRequestParameters) -> Result>> { let expected_key = request.unique_key(); - let inner = self.inner.write().unwrap(); + let inner = self.inner.read().unwrap(); Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| { (media_key == &expected_key).then(|| media_content.to_owned()) diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 2584b8881ed..3c35862b3c5 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -46,7 +46,7 @@ pub trait EventCacheStore: AsyncTraitDeps { async fn handle_linked_chunk_updates( &self, room_id: &RoomId, - updates: &[Update], + updates: Vec>, ) -> Result<(), Self::Error>; /// Add a media file's content in the media store. @@ -146,7 +146,7 @@ impl EventCacheStore for EraseEventCacheStoreError { async fn handle_linked_chunk_updates( &self, room_id: &RoomId, - updates: &[Update], + updates: Vec>, ) -> Result<(), Self::Error> { self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index f7c2f9c7e24..56979ced5be 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -81,11 +81,7 @@ impl RelationalLinkedChunk { /// Apply [`Update`]s. That's the only way to write data inside this /// relational linked chunk. - pub fn apply_updates(&mut self, room_id: &RoomId, updates: &[Update]) - where - Item: Clone, - Gap: Clone, - { + pub fn apply_updates(&mut self, room_id: &RoomId, updates: Vec>) { for update in updates { match update { Update::NewItemsChunk { previous, new, next } => { @@ -96,8 +92,8 @@ impl RelationalLinkedChunk { insert_chunk(&mut self.chunks, room_id, previous, new, next); self.items.push(ItemRow { room_id: room_id.to_owned(), - position: Position::new(*new, 0), - item: Either::Gap(gap.clone()), + position: Position::new(new, 0), + item: Either::Gap(gap), }); } @@ -111,7 +107,7 @@ impl RelationalLinkedChunk { .filter_map( |(nth, ItemRow { room_id: room_id_candidate, position, .. })| { (room_id == room_id_candidate - && position.chunk_identifier() == *chunk_identifier) + && position.chunk_identifier() == chunk_identifier) .then_some(nth) }, ) @@ -122,14 +118,12 @@ impl RelationalLinkedChunk { } } - Update::PushItems { at, items } => { - let mut at = *at; - + Update::PushItems { mut at, items } => { for item in items { self.items.push(ItemRow { room_id: room_id.to_owned(), position: at, - item: Either::Item(item.clone()), + item: Either::Item(item), }); at.increment_index(); } @@ -147,7 +141,7 @@ impl RelationalLinkedChunk { } // Find the item to remove. - if position == at { + if *position == at { debug_assert!(entry_to_remove.is_none(), "Found the same entry twice"); entry_to_remove = Some(nth); @@ -191,21 +185,21 @@ impl RelationalLinkedChunk { fn insert_chunk( chunks: &mut Vec, room_id: &RoomId, - previous: &Option, - new: &ChunkIdentifier, - next: &Option, + previous: Option, + new: ChunkIdentifier, + next: Option, ) { // Find the previous chunk, and update its next chunk. if let Some(previous) = previous { let entry_for_previous_chunk = chunks .iter_mut() .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { - room_id == room_id_candidate && chunk == previous + room_id == room_id_candidate && *chunk == previous }) .expect("Previous chunk should be present"); // Insert the chunk. - entry_for_previous_chunk.next_chunk = Some(*new); + entry_for_previous_chunk.next_chunk = Some(new); } // Find the next chunk, and update its previous chunk. @@ -213,33 +207,33 @@ impl RelationalLinkedChunk { let entry_for_next_chunk = chunks .iter_mut() .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { - room_id == room_id_candidate && chunk == next + room_id == room_id_candidate && *chunk == next }) .expect("Next chunk should be present"); // Insert the chunk. - entry_for_next_chunk.previous_chunk = Some(*new); + entry_for_next_chunk.previous_chunk = Some(new); } // Insert the chunk. chunks.push(ChunkRow { room_id: room_id.to_owned(), - previous_chunk: *previous, - chunk: *new, - next_chunk: *next, + previous_chunk: previous, + chunk: new, + next_chunk: next, }); } fn remove_chunk( chunks: &mut Vec, room_id: &RoomId, - chunk_to_remove: &ChunkIdentifier, + chunk_to_remove: ChunkIdentifier, ) { let entry_nth_to_remove = chunks .iter() .enumerate() .find_map(|(nth, ChunkRow { room_id: room_id_candidate, chunk, .. })| { - (room_id == room_id_candidate && chunk == chunk_to_remove).then_some(nth) + (room_id == room_id_candidate && *chunk == chunk_to_remove).then_some(nth) }) .expect("Remove an unknown chunk"); @@ -294,7 +288,7 @@ mod tests { relational_linked_chunk.apply_updates( room_id, - &[ + vec![ // 0 Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // 1 after 0 @@ -351,7 +345,7 @@ mod tests { relational_linked_chunk.apply_updates( room_id, - &[ + vec![ // 0 Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // 1 after 0 @@ -408,7 +402,7 @@ mod tests { relational_linked_chunk.apply_updates( room_id, - &[ + vec![ // 0 Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // 1 after 0 @@ -454,7 +448,7 @@ mod tests { relational_linked_chunk.apply_updates( room_id, - &[ + vec![ // new chunk (this is not mandatory for this test, but let's try to be realistic) Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // new items on 0 @@ -541,7 +535,7 @@ mod tests { relational_linked_chunk.apply_updates( room_id, - &[ + vec![ // new chunk (this is not mandatory for this test, but let's try to be realistic) Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // new items on 0 @@ -596,7 +590,7 @@ mod tests { relational_linked_chunk.apply_updates( room_id, - &[ + vec![ // new chunk Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, // new chunk @@ -670,7 +664,7 @@ mod tests { let mut relational_linked_chunk = RelationalLinkedChunk::::new(); relational_linked_chunk - .apply_updates(room_id, &[Update::StartReattachItems, Update::EndReattachItems]); + .apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]); // Nothing happened. assert!(relational_linked_chunk.chunks.is_empty()); diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index f6b62e76216..5ee8d92324e 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -186,7 +186,7 @@ impl EventCacheStore for SqliteEventCacheStore { async fn handle_linked_chunk_updates( &self, _room_id: &RoomId, - _updates: &[Update], + _updates: Vec>, ) -> Result<(), Self::Error> { todo!() } From b979b2ea1e069a442bf03e87cddac0b597ec53cd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 16:40:58 +0100 Subject: [PATCH 592/979] doc(common): Fix typos. --- .../src/linked_chunk/relational.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index 56979ced5be..7f4983ca270 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -198,7 +198,7 @@ impl RelationalLinkedChunk { }) .expect("Previous chunk should be present"); - // Insert the chunk. + // Link the chunk. entry_for_previous_chunk.next_chunk = Some(new); } @@ -211,7 +211,7 @@ impl RelationalLinkedChunk { }) .expect("Next chunk should be present"); - // Insert the chunk. + // Link the chunk. entry_for_next_chunk.previous_chunk = Some(new); } @@ -360,7 +360,7 @@ mod tests { ], ); - // Chunks are correctly links. + // Chunks are correctly linked. assert_eq!( relational_linked_chunk.chunks, &[ @@ -419,7 +419,7 @@ mod tests { ], ); - // Chunks are correctly links. + // Chunks are correctly linked. assert_eq!( relational_linked_chunk.chunks, &[ @@ -462,7 +462,7 @@ mod tests { ], ); - // Chunks are correctly links. + // Chunks are correctly linked. assert_eq!( relational_linked_chunk.chunks, &[ @@ -550,7 +550,7 @@ mod tests { ], ); - // Chunks are correctly links. + // Chunks are correctly linked. assert_eq!( relational_linked_chunk.chunks, &[ChunkRow { @@ -607,7 +607,7 @@ mod tests { ], ); - // Chunks are correctly links. + // Chunks are correctly linked. assert_eq!( relational_linked_chunk.chunks, &[ From 2abbf58825af0a277e29394ff037f918fb04a464 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 15:24:43 +0100 Subject: [PATCH 593/979] feat(common): Implement `LinkedChunk::clear`. This patch implements `LinkedChunk::clear`. The code from `impl Drop for LinkedChunk` has been moved inside `Ends::clear`, and replaced by a simple `self.links.clear()`. In addition, `LinkedChunk::clear` indeed calls `self.links.clear()` but also resets all fields. This patch adds the `Clear` variant to `Update`. This patch updates `AsVector` to emit a `VectorDiff::Clear` on `Update::Clear`. Finally, this patch adds the necessary tests. --- .../src/linked_chunk/as_vector.rs | 20 ++- .../matrix-sdk-common/src/linked_chunk/mod.rs | 124 ++++++++++++++---- .../src/linked_chunk/updates.rs | 4 + 3 files changed, 122 insertions(+), 26 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs index f392f8a233b..4063385c961 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs @@ -405,6 +405,11 @@ impl UpdateToVectorDiff { // Exiting the _detaching_ mode. detaching = false; } + + Update::Clear => { + // Let's straightforwardly emit a `VectorDiff::Clear`. + diffs.push(VectorDiff::Clear); + } } } @@ -473,6 +478,7 @@ mod tests { VectorDiff::Remove { index } => { accumulator.remove(index); } + VectorDiff::Clear => accumulator.clear(), diff => unimplemented!("{diff:?}"), } } @@ -686,14 +692,20 @@ mod tests { &[VectorDiff::Insert { index: 14, value: 'z' }], ); - drop(linked_chunk); - assert!(as_vector.take().is_empty()); - - // Finally, ensure the “reconstitued” vector is the one expected. + // Ensure the “reconstitued” vector is the one expected. assert_eq!( accumulator, vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h'] ); + + // Let's try to clear the linked chunk now. + linked_chunk.clear(); + + apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Clear]); + assert!(accumulator.is_empty()); + + drop(linked_chunk); + assert!(as_vector.take().is_empty()); } #[test] diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 0aae2ecc942..63f1d44550c 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -191,6 +191,36 @@ impl Ends { chunk = chunk.previous_mut()?; } } + + /// Drop all chunks, and re-create the first one. + fn clear(&mut self) { + // Loop over all chunks, from the last to the first chunk, and drop them. + { + // Take the latest chunk. + let mut current_chunk_ptr = self.last.or(Some(self.first)); + + // As long as we have another chunk… + while let Some(chunk_ptr) = current_chunk_ptr { + // Fetch the previous chunk pointer. + let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous; + + // Re-box the chunk, and let Rust does its job. + let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) }; + + // Update the `current_chunk_ptr`. + current_chunk_ptr = previous_ptr; + } + + // At this step, all chunks have been dropped, including + // `self.first`. + } + + // Recreate the first chunk. + self.first = Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER); + + // Reset the last chunk. + self.last = None; + } } /// The [`LinkedChunk`] structure. @@ -259,6 +289,24 @@ impl LinkedChunk { } } + /// Clear all the chunks. + pub fn clear(&mut self) { + // Clear `self.links`. + self.links.clear(); + + // Clear `self.length`. + self.length = 0; + + // Clear `self.chunk_identifier_generator`. + self.chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch(); + + // “Clear” `self.updates`. + if let Some(updates) = self.updates.as_mut() { + // TODO: Optimisation: Do we want to clear all pending `Update`s in `updates`? + updates.push(Update::Clear); + } + } + /// Get the number of items in this linked chunk. #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { @@ -847,27 +895,11 @@ impl LinkedChunk { impl Drop for LinkedChunk { fn drop(&mut self) { - // Take the latest chunk. - let mut current_chunk_ptr = self.links.last.or(Some(self.links.first)); - - // As long as we have another chunk… - while let Some(chunk_ptr) = current_chunk_ptr { - // Disconnect the chunk by updating `previous_chunk.next` pointer. - let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous; - - if let Some(mut previous_ptr) = previous_ptr { - unsafe { previous_ptr.as_mut() }.next = None; - } - - // Re-box the chunk, and let Rust does its job. - let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) }; - - // Update the `current_chunk_ptr`. - current_chunk_ptr = previous_ptr; - } - - // At this step, all chunks have been dropped, including - // `self.first`. + // Only clear the links. Calling `Self::clear` would be an error as we don't + // want to emit an `Update::Clear` when `self` is dropped. Instead, we only care + // about freeing memory correctly. Rust can take care of everything except the + // pointers in `self.links`, hence the specific call to `self.links.clear()`. + self.links.clear(); } } @@ -1391,7 +1423,10 @@ impl EmptyChunk { #[cfg(test)] mod tests { - use std::ops::Not; + use std::{ + ops::Not, + sync::{atomic::Ordering, Arc}, + }; use assert_matches::assert_matches; @@ -2635,4 +2670,49 @@ mod tests { assert!(chunks.next().unwrap().is_last_chunk()); assert!(chunks.next().is_none()); } + + // Test `LinkedChunk::clear`. This test creates a `LinkedChunk` with `new` to + // avoid creating too much confusion with `Update`s. The next test + // `test_clear_emit_an_update_clear` uses `new_with_update_history` and only + // test `Update::Clear`. + #[test] + fn test_clear() { + let mut linked_chunk = LinkedChunk::<3, Arc, Arc<()>>::new(); + + let item = Arc::new('a'); + let gap = Arc::new(()); + + linked_chunk.push_items_back([ + item.clone(), + item.clone(), + item.clone(), + item.clone(), + item.clone(), + ]); + linked_chunk.push_gap_back(gap.clone()); + linked_chunk.push_items_back([item.clone()]); + + assert_eq!(Arc::strong_count(&item), 7); + assert_eq!(Arc::strong_count(&gap), 2); + assert_eq!(linked_chunk.len(), 6); + assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 3); + + // Now, we can clear the linked chunk and see what happens. + linked_chunk.clear(); + + assert_eq!(Arc::strong_count(&item), 1); + assert_eq!(Arc::strong_count(&gap), 1); + assert_eq!(linked_chunk.len(), 0); + assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 0); + } + + #[test] + fn test_clear_emit_an_update_clear() { + use super::Update::*; + + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + linked_chunk.clear(); + + assert_eq!(linked_chunk.updates().unwrap().take(), &[Clear]); + } } diff --git a/crates/matrix-sdk-common/src/linked_chunk/updates.rs b/crates/matrix-sdk-common/src/linked_chunk/updates.rs index 97d21231ed1..a39e3d29906 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/updates.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/updates.rs @@ -99,6 +99,10 @@ pub enum Update { /// Reattaching items (see [`Self::StartReattachItems`]) is finished. EndReattachItems, + + /// All chunks have been cleared, i.e. all items and all gaps have been + /// dropped. + Clear, } /// A collection of [`Update`]s that can be observed. From c61f70727f8d8895eb77b79388603638c3ac7e68 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 17:24:38 +0100 Subject: [PATCH 594/979] fix: `RelationalLinkedChunk` handles `Update::Clear`. What the title says. --- .../src/linked_chunk/relational.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index 7f4983ca270..445184cf1f2 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -179,6 +179,11 @@ impl RelationalLinkedChunk { } Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ } + + Update::Clear => { + self.chunks.clear(); + self.items.clear(); + } } } @@ -670,4 +675,57 @@ mod tests { assert!(relational_linked_chunk.chunks.is_empty()); assert!(relational_linked_chunk.items.is_empty()); } + + #[test] + fn test_clear() { + let room_id = room_id!("!r0:matrix.org"); + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] }, + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: None, + }], + ); + // Items contains the pushed items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 0), + item: Either::Item('a') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 1), + item: Either::Item('b') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 2), + item: Either::Item('c') + }, + ], + ); + + // Now, time for a clean up. + relational_linked_chunk.apply_updates(room_id, vec![Update::Clear]); + assert!(relational_linked_chunk.chunks.is_empty()); + assert!(relational_linked_chunk.items.is_empty()); + } } From 1fbe6815c31ebfd6fde8bab5dd848bc0fc11ea17 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 25 Nov 2024 17:13:09 +0100 Subject: [PATCH 595/979] task(event cache): log whenever we receive an ignore user list change --- crates/matrix-sdk/src/event_cache/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 035b6b0f631..2d2aff291f0 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -48,7 +48,7 @@ use tokio::sync::{ broadcast::{error::RecvError, Receiver}, Mutex, RwLock, }; -use tracing::{error, info_span, instrument, trace, warn, Instrument as _, Span}; +use tracing::{error, info, info_span, instrument, trace, warn, Instrument as _, Span}; use self::paginator::PaginatorError; use crate::{client::WeakClient, Client}; @@ -212,6 +212,7 @@ impl EventCache { async move { while ignore_user_list_stream.next().await.is_some() { + info!("received an ignore user list change"); inner.clear_all_rooms().await; } } From ca397dca0f6402f3ba5c9c627dfe7a5e5ec0b259 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 26 Nov 2024 11:58:23 +0200 Subject: [PATCH 596/979] feat(ffi): wrap Ruma MediaSources and run validations before passing them over FFI Ruma doesn't currently validate mxuri's and as such `MediaSource`s passed over FFI can contain invalid/empty URLs. This change introduces a wrapper type around Ruma's and failable transformations so that appropiate actions can be taken beforehand e.g. returning a `TimelineItemContent::FailedToParseMessageLike` or nil-ing out the thumbnail info. --- bindings/matrix-sdk-ffi/src/api.udl | 7 - bindings/matrix-sdk-ffi/src/client.rs | 14 +- bindings/matrix-sdk-ffi/src/event.rs | 2 +- bindings/matrix-sdk-ffi/src/lib.rs | 6 +- bindings/matrix-sdk-ffi/src/room.rs | 2 +- bindings/matrix-sdk-ffi/src/ruma.rs | 155 +++++++++++++----- .../matrix-sdk-ffi/src/timeline/content.rs | 55 +++++-- 7 files changed, 163 insertions(+), 78 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 1ed73c945f8..33053361dbf 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -13,10 +13,3 @@ interface RoomMessageEventContentWithoutRelation { interface ClientError { Generic(string msg); }; - -interface MediaSource { - [Name=from_json, Throws=ClientError] - constructor(string json); - string to_json(); - string url(); -}; diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 0a50f9716fc..444ee1cd84c 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -32,9 +32,7 @@ use matrix_sdk::{ user_directory::search_users, }, events::{ - room::{ - avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource, - }, + room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent}, AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent, }, serde::Raw, @@ -81,7 +79,7 @@ use crate::{ notification_settings::NotificationSettings, room_directory_search::RoomDirectorySearch, room_preview::RoomPreview, - ruma::AuthData, + ruma::{AuthData, MediaSource}, sync_service::{SyncService, SyncServiceBuilder}, task_handle::TaskHandle, utils::AsyncRuntimeDropped, @@ -455,7 +453,7 @@ impl Client { .inner .media() .get_media_file( - &MediaRequestParameters { source, format: MediaFormat::File }, + &MediaRequestParameters { source: source.media_source, format: MediaFormat::File }, filename, &mime_type, use_cache, @@ -728,7 +726,7 @@ impl Client { &self, media_source: Arc, ) -> Result, ClientError> { - let source = (*media_source).clone(); + let source = (*media_source).clone().media_source; debug!(?source, "requesting media file"); Ok(self @@ -744,9 +742,9 @@ impl Client { width: u64, height: u64, ) -> Result, ClientError> { - let source = (*media_source).clone(); + let source = (*media_source).clone().media_source; - debug!(source = ?media_source, width, height, "requesting media thumbnail"); + debug!(?source, width, height, "requesting media thumbnail"); Ok(self .inner .media() diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index 8771693ab61..1fbc423d54f 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -202,7 +202,7 @@ impl TryFrom for MessageLikeEventContent { _ => None, }); MessageLikeEventContent::RoomMessage { - message_type: original_content.msgtype.into(), + message_type: original_content.msgtype.try_into()?, in_reply_to_event_id, } } diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 9ed31580edd..a648947c2b6 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -33,13 +33,11 @@ mod utils; mod widget; use async_compat::TOKIO1 as RUNTIME; -use matrix_sdk::ruma::events::room::{ - message::RoomMessageEventContentWithoutRelation, MediaSource, -}; +use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation; use self::{ error::ClientError, - ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt}, + ruma::{Mentions, RoomMessageEventContentWithoutRelationExt}, task_handle::TaskHandle, }; diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index ed67a1d4fa4..dcaf6f2efc4 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -973,7 +973,7 @@ impl TryFrom for RumaAvatarImageInfo { fn try_from(value: ImageInfo) -> Result { let thumbnail_url = if let Some(media_source) = value.thumbnail_source { - match media_source.as_ref() { + match &media_source.as_ref().media_source { MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()), MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField), } diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 195fae191e9..7d70f6bd103 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -42,7 +42,8 @@ use ruma::{ VideoInfo as RumaVideoInfo, VideoMessageEventContent as RumaVideoMessageEventContent, }, - ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo, + ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource, + ThumbnailInfo as RumaThumbnailInfo, }, }, matrix_uri::MatrixId as RumaMatrixId, @@ -156,7 +157,7 @@ impl From<&RumaMatrixId> for MatrixId { #[matrix_sdk_ffi_macros::export] pub fn media_source_from_url(url: String) -> Arc { - Arc::new(MediaSource::Plain(url.into())) + Arc::new(MediaSource { media_source: RumaMediaSource::Plain(url.into()) }) } #[matrix_sdk_ffi_macros::export] @@ -200,21 +201,61 @@ pub fn message_event_content_from_html_as_emote( ))) } -#[extension_trait] -pub impl MediaSourceExt for MediaSource { - fn from_json(json: String) -> Result { - let res = serde_json::from_str(&json)?; - Ok(res) +#[derive(Clone, uniffi::Object)] +pub struct MediaSource { + pub(crate) media_source: RumaMediaSource, +} + +#[matrix_sdk_ffi_macros::export] +impl MediaSource { + pub fn url(&self) -> String { + self.media_source.url() + } +} + +impl TryFrom for MediaSource { + type Error = ClientError; + + fn try_from(value: RumaMediaSource) -> Result { + value.verify()?; + Ok(Self { media_source: value }) + } +} + +impl TryFrom<&RumaMediaSource> for MediaSource { + type Error = ClientError; + + fn try_from(value: &RumaMediaSource) -> Result { + value.verify()?; + Ok(Self { media_source: value.clone() }) + } +} + +impl From for RumaMediaSource { + fn from(value: MediaSource) -> Self { + value.media_source } +} + +#[extension_trait] +pub impl MediaSourceExt for RumaMediaSource { + fn verify(&self) -> Result<(), ClientError> { + match self { + RumaMediaSource::Plain(url) => { + url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?; + } + RumaMediaSource::Encrypted(file) => { + file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?; + } + } - fn to_json(&self) -> String { - serde_json::to_string(self).expect("Media source should always be serializable ") + Ok(()) } fn url(&self) -> String { match self { - MediaSource::Plain(url) => url.to_string(), - MediaSource::Encrypted(file) => file.url.to_string(), + RumaMediaSource::Plain(url) => url.to_string(), + RumaMediaSource::Encrypted(file) => file.url.to_string(), } } } @@ -280,7 +321,7 @@ fn get_body_and_filename(filename: String, caption: Option) -> (String, } impl TryFrom for RumaMessageType { - type Error = serde_json::Error; + type Error = ClientError; fn try_from(value: MessageType) -> Result { Ok(match value { @@ -292,7 +333,7 @@ impl TryFrom for RumaMessageType { MessageType::Image { content } => { let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaImageMessageEventContent::new(body, (*content.source).clone()) + RumaImageMessageEventContent::new(body, (*content.source).clone().into()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted_caption.map(Into::into); event_content.filename = filename; @@ -301,7 +342,7 @@ impl TryFrom for RumaMessageType { MessageType::Audio { content } => { let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaAudioMessageEventContent::new(body, (*content.source).clone()) + RumaAudioMessageEventContent::new(body, (*content.source).clone().into()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted_caption.map(Into::into); event_content.filename = filename; @@ -310,7 +351,7 @@ impl TryFrom for RumaMessageType { MessageType::Video { content } => { let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaVideoMessageEventContent::new(body, (*content.source).clone()) + RumaVideoMessageEventContent::new(body, (*content.source).clone().into()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted_caption.map(Into::into); event_content.filename = filename; @@ -319,7 +360,7 @@ impl TryFrom for RumaMessageType { MessageType::File { content } => { let (body, filename) = get_body_and_filename(content.filename, content.caption); let mut event_content = - RumaFileMessageEventContent::new(body, (*content.source).clone()) + RumaFileMessageEventContent::new(body, (*content.source).clone().into()) .info(content.info.map(Into::into).map(Box::new)); event_content.formatted = content.formatted_caption.map(Into::into); event_content.filename = filename; @@ -345,9 +386,11 @@ impl TryFrom for RumaMessageType { } } -impl From for MessageType { - fn from(value: RumaMessageType) -> Self { - match value { +impl TryFrom for MessageType { + type Error = ClientError; + + fn try_from(value: RumaMessageType) -> Result { + Ok(match value { RumaMessageType::Emote(c) => MessageType::Emote { content: EmoteMessageContent { body: c.body.clone(), @@ -359,16 +402,17 @@ impl From for MessageType { filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.clone()), - info: c.info.as_deref().map(Into::into), + source: Arc::new(c.source.try_into()?), + info: c.info.as_deref().map(TryInto::try_into).transpose()?, }, }, + RumaMessageType::Audio(c) => MessageType::Audio { content: AudioMessageContent { filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.clone()), + source: Arc::new(c.source.try_into()?), info: c.info.as_deref().map(Into::into), audio: c.audio.map(Into::into), voice: c.voice.map(Into::into), @@ -379,8 +423,8 @@ impl From for MessageType { filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.clone()), - info: c.info.as_deref().map(Into::into), + source: Arc::new(c.source.try_into()?), + info: c.info.as_deref().map(TryInto::try_into).transpose()?, }, }, RumaMessageType::File(c) => MessageType::File { @@ -388,8 +432,8 @@ impl From for MessageType { filename: c.filename().to_owned(), caption: c.caption().map(ToString::to_string), formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.clone()), - info: c.info.as_deref().map(Into::into), + source: Arc::new(c.source.try_into()?), + info: c.info.as_deref().map(TryInto::try_into).transpose()?, }, }, RumaMessageType::Notice(c) => MessageType::Notice { @@ -425,7 +469,7 @@ impl From for MessageType { msgtype: value.msgtype().to_owned(), body: value.body().to_owned(), }, - } + }) } } @@ -520,7 +564,7 @@ impl From for RumaImageInfo { mimetype: value.mimetype, size: value.size.map(u64_to_uint), thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new), - thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()), + thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()), blurhash: value.blurhash, }) } @@ -625,7 +669,7 @@ impl From for RumaVideoInfo { mimetype: value.mimetype, size: value.size.map(u64_to_uint), thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new), - thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()), + thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()), blurhash: value.blurhash, }) } @@ -668,7 +712,7 @@ impl From for RumaFileInfo { mimetype: value.mimetype, size: value.size.map(u64_to_uint), thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new), - thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()), + thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()), }) } } @@ -790,8 +834,10 @@ pub enum MessageFormat { Unknown { format: String }, } -impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo { - fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self { +impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo { + type Error = ClientError; + + fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result { let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo { height: info.height.map(Into::into), width: info.width.map(Into::into), @@ -799,15 +845,20 @@ impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo { size: info.size.map(Into::into), }); - Self { + Ok(Self { height: info.height.map(Into::into), width: info.width.map(Into::into), mimetype: info.mimetype.clone(), size: info.size.map(Into::into), thumbnail_info, - thumbnail_source: info.thumbnail_source.clone().map(Arc::new), + thumbnail_source: info + .thumbnail_source + .as_ref() + .map(TryInto::try_into) + .transpose()? + .map(Arc::new), blurhash: info.blurhash.clone(), - } + }) } } @@ -821,8 +872,10 @@ impl From<&RumaAudioInfo> for AudioInfo { } } -impl From<&RumaVideoInfo> for VideoInfo { - fn from(info: &RumaVideoInfo) -> Self { +impl TryFrom<&RumaVideoInfo> for VideoInfo { + type Error = ClientError; + + fn try_from(info: &RumaVideoInfo) -> Result { let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo { height: info.height.map(Into::into), width: info.width.map(Into::into), @@ -830,21 +883,28 @@ impl From<&RumaVideoInfo> for VideoInfo { size: info.size.map(Into::into), }); - Self { + Ok(Self { duration: info.duration, height: info.height.map(Into::into), width: info.width.map(Into::into), mimetype: info.mimetype.clone(), size: info.size.map(Into::into), thumbnail_info, - thumbnail_source: info.thumbnail_source.clone().map(Arc::new), + thumbnail_source: info + .thumbnail_source + .as_ref() + .map(TryInto::try_into) + .transpose()? + .map(Arc::new), blurhash: info.blurhash.clone(), - } + }) } } -impl From<&RumaFileInfo> for FileInfo { - fn from(info: &RumaFileInfo) -> Self { +impl TryFrom<&RumaFileInfo> for FileInfo { + type Error = ClientError; + + fn try_from(info: &RumaFileInfo) -> Result { let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo { height: info.height.map(Into::into), width: info.width.map(Into::into), @@ -852,12 +912,17 @@ impl From<&RumaFileInfo> for FileInfo { size: info.size.map(Into::into), }); - Self { + Ok(Self { mimetype: info.mimetype.clone(), size: info.size.map(Into::into), thumbnail_info, - thumbnail_source: info.thumbnail_source.clone().map(Arc::new), - } + thumbnail_source: info + .thumbnail_source + .as_ref() + .map(TryInto::try_into) + .transpose()? + .map(Arc::new), + }) } } diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index 0b01ef5005b..3c182c65e82 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -16,26 +16,55 @@ use std::{collections::HashMap, sync::Arc}; use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes}; use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails}; -use ruma::events::{room::MediaSource, FullStateEventContent}; +use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent}; use super::ProfileDetails; -use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind}; +use crate::{ + error::ClientError, + ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind}, +}; impl From for TimelineItemContent { fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self { use matrix_sdk_ui::timeline::TimelineItemContent as Content; match value { - Content::Message(message) => TimelineItemContent::Message { content: message.into() }, + Content::Message(message) => { + let msgtype = message.msgtype().msgtype().to_owned(); + + match TryInto::::try_into(message) { + Ok(message) => TimelineItemContent::Message { content: message }, + Err(error) => TimelineItemContent::FailedToParseMessageLike { + event_type: msgtype, + error: error.to_string(), + }, + } + } Content::RedactedMessage => TimelineItemContent::RedactedMessage, Content::Sticker(sticker) => { let content = sticker.content(); - TimelineItemContent::Sticker { - body: content.body.clone(), - info: (&content.info).into(), - source: Arc::new(MediaSource::from(content.source.clone())), + + let media_source = RumaMediaSource::from(content.source.clone()); + + if let Err(error) = media_source.verify() { + return TimelineItemContent::FailedToParseMessageLike { + event_type: sticker.content().event_type().to_string(), + error: error.to_string(), + }; + } + + match TryInto::::try_into(&content.info) { + Ok(info) => TimelineItemContent::Sticker { + body: content.body.clone(), + info, + source: Arc::new(MediaSource { media_source }), + }, + Err(error) => TimelineItemContent::FailedToParseMessageLike { + event_type: sticker.content().event_type().to_string(), + error: error.to_string(), + }, } } @@ -117,16 +146,18 @@ pub struct MessageContent { pub mentions: Option, } -impl From for MessageContent { - fn from(value: matrix_sdk_ui::timeline::Message) -> Self { - Self { - msg_type: value.msgtype().clone().into(), +impl TryFrom for MessageContent { + type Error = ClientError; + + fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result { + Ok(Self { + msg_type: value.msgtype().clone().try_into()?, body: value.body().to_owned(), in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())), is_edited: value.is_edited(), thread_root: value.thread_root().map(|id| id.to_string()), mentions: value.mentions().cloned().map(|m| m.into()), - } + }) } } From 728d646ce2821253c01f5d873c587b0133d229f1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 26 Nov 2024 10:38:56 +0100 Subject: [PATCH 597/979] fix(common): `AsVector` clears its internal state on `Update::Clear`. This patch fixes a bug in `AsVector`: when an `Update::Clear` value is received, `AsVector`'s internal state must be cleared too, i.e. the `UpdateToVectorDiff::chunks` field should be reset to an initial value! This patch adds a test to ensure this works as expected. --- .../src/linked_chunk/as_vector.rs | 64 ++++++++++++++++++- .../matrix-sdk-common/src/linked_chunk/mod.rs | 17 ++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs index 4063385c961..5e3fe663056 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs @@ -302,6 +302,12 @@ impl UpdateToVectorDiff { self.chunks.insert(next_chunk_index, (*new, 0)); } + // First chunk! + (None, None) if self.chunks.is_empty() => { + self.chunks.push_back((*new, 0)); + } + + // Impossible state. (None, None) => { unreachable!( "Inserting new chunk with no previous nor next chunk identifiers \ @@ -407,6 +413,9 @@ impl UpdateToVectorDiff { } Update::Clear => { + // Clean `self.chunks`. + self.chunks.clear(); + // Let's straightforwardly emit a `VectorDiff::Clear`. diffs.push(VectorDiff::Clear); } @@ -455,10 +464,11 @@ impl UpdateToVectorDiff { mod tests { use std::fmt::Debug; + use assert_matches::assert_matches; use imbl::{vector, Vector}; use super::{ - super::{EmptyChunk, LinkedChunk}, + super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk}, VectorDiff, }; @@ -708,6 +718,58 @@ mod tests { assert!(as_vector.take().is_empty()); } + #[test] + fn test_as_vector_with_update_clear() { + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + let mut as_vector = linked_chunk.as_vector().unwrap(); + + { + // 1 initial chunk in the `UpdateToVectorDiff` mapper. + let chunks = &as_vector.mapper.chunks; + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER); + assert_eq!(chunks[0].1, 0); + } + + assert!(as_vector.take().is_empty()); + + linked_chunk.push_items_back(['a', 'b', 'c', 'd']); + + { + let diffs = as_vector.take(); + assert_eq!(diffs.len(), 2); + assert_matches!(&diffs[0], VectorDiff::Append { .. }); + assert_matches!(&diffs[1], VectorDiff::Append { .. }); + + // 2 chunks in the `UpdateToVectorDiff` mapper. + assert_eq!(as_vector.mapper.chunks.len(), 2); + } + + linked_chunk.clear(); + + { + let diffs = as_vector.take(); + assert_eq!(diffs.len(), 1); + assert_matches!(&diffs[0], VectorDiff::Clear); + + // 1 chunk in the `UpdateToVectorDiff` mapper. + let chunks = &as_vector.mapper.chunks; + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER); + assert_eq!(chunks[0].1, 0); + } + + // And we can push again. + linked_chunk.push_items_back(['a', 'b', 'c', 'd']); + + { + let diffs = as_vector.take(); + assert_eq!(diffs.len(), 2); + assert_matches!(&diffs[0], VectorDiff::Append { .. }); + assert_matches!(&diffs[1], VectorDiff::Append { .. }); + } + } + #[test] fn updates_are_drained_when_constructing_as_vector() { let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history(); diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 63f1d44550c..db1a3b5486c 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -304,6 +304,11 @@ impl LinkedChunk { if let Some(updates) = self.updates.as_mut() { // TODO: Optimisation: Do we want to clear all pending `Update`s in `updates`? updates.push(Update::Clear); + updates.push(Update::NewItemsChunk { + previous: None, + new: ChunkIdentifierGenerator::FIRST_IDENTIFIER, + next: None, + }) } } @@ -2713,6 +2718,16 @@ mod tests { let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); linked_chunk.clear(); - assert_eq!(linked_chunk.updates().unwrap().take(), &[Clear]); + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + Clear, + NewItemsChunk { + previous: None, + new: ChunkIdentifierGenerator::FIRST_IDENTIFIER, + next: None + } + ] + ); } } From cc8bc0553745c96111fdfd4b84c0a932bcc8dc38 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 25 Nov 2024 15:36:53 +0100 Subject: [PATCH 598/979] refactor: `RoomEvents::reset` really clear the linked chunk. This patch updates `RoomEvents::reset` to not drop the `LinkedChunk` to clear it. --- .../src/linked_chunk/as_vector.rs | 4 +- .../matrix-sdk/src/event_cache/room/events.rs | 87 ++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs index 5e3fe663056..b607aca056e 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs @@ -729,9 +729,9 @@ mod tests { assert_eq!(chunks.len(), 1); assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER); assert_eq!(chunks[0].1, 0); - } - assert!(as_vector.take().is_empty()); + assert!(as_vector.take().is_empty()); + } linked_chunk.push_items_back(['a', 'b', 'c', 'd']); diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 50813c3f0e1..987bb151788 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -14,7 +14,9 @@ use std::cmp::Ordering; +use eyeball_im::VectorDiff; pub use matrix_sdk_base::event_cache::{Event, Gap}; +use matrix_sdk_base::linked_chunk::AsVector; use matrix_sdk_common::linked_chunk::{ Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position, }; @@ -31,6 +33,11 @@ pub struct RoomEvents { /// The real in-memory storage for all the events. chunks: LinkedChunk, + /// Type mapping [`Update`]s from [`Self::chunks`] to [`VectorDiff`]s. + /// + /// [`Update`]: matrix_sdk_base::linked_chunk::Update + chunks_updates_as_vectordiffs: AsVector, + /// The events deduplicator instance to help finding duplicates. deduplicator: Deduplicator, } @@ -44,12 +51,22 @@ impl Default for RoomEvents { impl RoomEvents { /// Build a new [`RoomEvents`] struct with zero events. pub fn new() -> Self { - Self { chunks: LinkedChunk::new(), deduplicator: Deduplicator::new() } + let mut chunks = LinkedChunk::new_with_update_history(); + let chunks_updates_as_vectordiffs = chunks + .as_vector() + // SAFETY: The `LinkedChunk` has been built with `new_with_update_history`, so + // `as_vector` must return `Some(…)`. + .expect("`LinkedChunk` must have been constructor with `new_with_update_history`"); + + Self { chunks, chunks_updates_as_vectordiffs, deduplicator: Deduplicator::new() } } /// Clear all events. + /// + /// All events, all gaps, everything is dropped, move into the void, into + /// the ether, forever. pub fn reset(&mut self) { - self.chunks = LinkedChunk::new(); + self.chunks.clear(); } /// Push events after all events or gaps. @@ -158,6 +175,18 @@ impl RoomEvents { self.chunks.items() } + /// Get all updates from the room events as [`VectorDiff`]. + /// + /// Be careful that each `VectorDiff` is returned only once! + /// + /// See [`AsVector`] to learn more. + /// + /// [`Update`]: matrix_sdk_base::linked_chunk::Update + #[allow(unused)] // gonna be useful very soon! but we need it now for test purposes + pub fn updates_as_vector_diffs(&mut self) -> Vec> { + self.chunks_updates_as_vectordiffs.take() + } + /// Deduplicate `events` considering all events in `Self::chunks`. /// /// The returned tuple contains (i) the unique events, and (ii) the @@ -315,6 +344,7 @@ impl RoomEvents { #[cfg(test)] mod tests { + use assert_matches::assert_matches; use assert_matches2::assert_let; use matrix_sdk_test::event_factory::EventFactory; use ruma::{user_id, EventId, OwnedEventId}; @@ -934,4 +964,57 @@ mod tests { // Ensure no chunk has been removed. assert_eq!(room_events.chunks().count(), 3); } + + #[test] + fn test_reset() { + let (event_id_0, event_0) = new_event("$ev0"); + let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev2"); + let (event_id_3, event_3) = new_event("$ev3"); + + // Push some events. + let mut room_events = RoomEvents::new(); + room_events.push_events([event_0, event_1]); + room_events.push_gap(Gap { prev_token: "raclette".to_owned() }); + room_events.push_events([event_2]); + + // Read the updates as `VectorDiff`. + let diffs = room_events.updates_as_vector_diffs(); + + assert_eq!(diffs.len(), 2); + + assert_matches!( + &diffs[0], + VectorDiff::Append { values } => { + assert_eq!(values.len(), 2); + assert_eq!(values[0].event_id(), Some(event_id_0)); + assert_eq!(values[1].event_id(), Some(event_id_1)); + } + ); + assert_matches!( + &diffs[1], + VectorDiff::Append { values } => { + assert_eq!(values.len(), 1); + assert_eq!(values[0].event_id(), Some(event_id_2)); + } + ); + + // Now we can reset and see what happens. + room_events.reset(); + room_events.push_events([event_3]); + + // Read the updates as `VectorDiff`. + let diffs = room_events.updates_as_vector_diffs(); + + assert_eq!(diffs.len(), 2); + + assert_matches!(&diffs[0], VectorDiff::Clear); + assert_matches!( + &diffs[1], + VectorDiff::Append { values } => { + assert_eq!(values.len(), 1); + assert_eq!(values[0].event_id(), Some(event_id_3)); + } + ); + } } From ecf44348cf6a872b843fb7d7af1a88f724c58c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 26 Nov 2024 12:35:03 +0100 Subject: [PATCH 599/979] fix(client): Do not use the encrypted original file's content type as the encrypted thumbnail's content type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/encryption/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index a22ebee2c00..2a264639bc8 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -460,8 +460,7 @@ impl Client { thumbnail: Option, send_progress: SharedObservable, ) -> Result<(MediaSource, Option<(MediaSource, Box)>)> { - let upload_thumbnail = - self.upload_encrypted_thumbnail(thumbnail, content_type, send_progress.clone()); + let upload_thumbnail = self.upload_encrypted_thumbnail(thumbnail, send_progress.clone()); let upload_attachment = async { let mut cursor = Cursor::new(data); @@ -480,7 +479,6 @@ impl Client { async fn upload_encrypted_thumbnail( &self, thumbnail: Option, - content_type: &mime::Mime, send_progress: SharedObservable, ) -> Result)>> { let Some(thumbnail) = thumbnail else { @@ -490,7 +488,7 @@ impl Client { let mut cursor = Cursor::new(thumbnail.data); let file = self - .upload_encrypted_file(content_type, &mut cursor) + .upload_encrypted_file(&thumbnail.content_type, &mut cursor) .with_send_progress_observable(send_progress) .await?; From d0257d1cb2272746336c6b42126d19a3b06e3e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 26 Nov 2024 12:36:57 +0100 Subject: [PATCH 600/979] refactor(media): Add method to split Thumbnail into parts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/attachment.rs | 14 +++++++++++++ crates/matrix-sdk/src/encryption/mod.rs | 13 ++++-------- crates/matrix-sdk/src/media.rs | 14 ++++--------- crates/matrix-sdk/src/send_queue/upload.rs | 24 ++++++++-------------- 4 files changed, 31 insertions(+), 34 deletions(-) diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 0fde6997466..986efcd8788 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -180,6 +180,20 @@ pub struct Thumbnail { pub info: Option, } +impl Thumbnail { + /// Convert this `Thumbnail` into a `(data, content_type, info)` tuple. + pub fn into_parts(self) -> (Vec, mime::Mime, Box) { + let thumbnail_info = assign!( + self.info + .as_ref() + .map(|info| ThumbnailInfo::from(info.clone())) + .unwrap_or_default(), + { mimetype: Some(self.content_type.to_string()) } + ); + (self.data, self.content_type, Box::new(thumbnail_info)) + } +} + /// Configuration for sending an attachment. #[derive(Debug, Default)] pub struct AttachmentConfig { diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 2a264639bc8..4d8d313aac8 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -485,20 +485,15 @@ impl Client { return Ok(None); }; - let mut cursor = Cursor::new(thumbnail.data); + let (data, content_type, thumbnail_info) = thumbnail.into_parts(); + let mut cursor = Cursor::new(data); let file = self - .upload_encrypted_file(&thumbnail.content_type, &mut cursor) + .upload_encrypted_file(&content_type, &mut cursor) .with_send_progress_observable(send_progress) .await?; - #[rustfmt::skip] - let thumbnail_info = - assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { - mimetype: Some(thumbnail.content_type.as_ref().to_owned()) - }); - - Ok(Some((MediaSource::Encrypted(Box::new(file)), Box::new(thumbnail_info)))) + Ok(Some((MediaSource::Encrypted(Box::new(file)), thumbnail_info))) } /// Claim one-time keys creating new Olm sessions. diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 9f686f1e2d8..108915984a4 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -669,20 +669,14 @@ impl Media { return Ok(None); }; + let (data, content_type, thumbnail_info) = thumbnail.into_parts(); + let response = self - .upload(&thumbnail.content_type, thumbnail.data, None) + .upload(&content_type, data, None) .with_send_progress_observable(send_progress) .await?; let url = response.content_uri; - let thumbnail_info = assign!( - thumbnail.info - .as_ref() - .map(|info| ThumbnailInfo::from(info.clone())) - .unwrap_or_default(), - { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } - ); - - Ok(Some((MediaSource::Plain(url), Box::new(thumbnail_info)))) + Ok(Some((MediaSource::Plain(url), thumbnail_info))) } } diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index ea4dea09734..5ef27f99ef6 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -24,11 +24,10 @@ use matrix_sdk_base::{ }; use mime::Mime; use ruma::{ - assign, events::{ room::{ message::{FormattedBody, MessageType, RoomMessageEventContent}, - MediaSource, ThumbnailInfo, + MediaSource, }, AnyMessageLikeEventContent, }, @@ -183,13 +182,16 @@ impl RoomSendQueue { // Process the thumbnail, if it's been provided. if let Some(thumbnail) = config.thumbnail.take() { + // Create the information required for filling the thumbnail section of the + // media event. + let (data, content_type, thumbnail_info) = thumbnail.into_parts(); + // Normalize information to retrieve the thumbnail in the cache store. - let info = thumbnail.info.as_ref(); - let height = info.and_then(|info| info.height).unwrap_or_else(|| { + let height = thumbnail_info.height.unwrap_or_else(|| { trace!("thumbnail height is unknown, using 0 for the cache entry"); uint!(0) }); - let width = info.and_then(|info| info.width).unwrap_or_else(|| { + let width = thumbnail_info.width.unwrap_or_else(|| { trace!("thumbnail width is unknown, using 0 for the cache entry"); uint!(0) }); @@ -201,25 +203,17 @@ impl RoomSendQueue { let thumbnail_media_request = make_local_thumbnail_media_request(&txn, height, width); cache_store - .add_media_content(&thumbnail_media_request, thumbnail.data.clone()) + .add_media_content(&thumbnail_media_request, data) .await .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; - // Create the information required for filling the thumbnail section of the - // media event. - let thumbnail_info = Box::new( - assign!(thumbnail.info.map(ThumbnailInfo::from).unwrap_or_default(), { - mimetype: Some(thumbnail.content_type.as_ref().to_owned()) - }), - ); - ( Some(txn.clone()), Some((thumbnail_media_request.source.clone(), thumbnail_info)), Some(( FinishUploadThumbnailInfo { txn, width, height }, thumbnail_media_request, - thumbnail.content_type, + content_type, )), ) } else { From d4d5f45edc6abc8f813a6edd5276e703127981a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 26 Nov 2024 13:00:50 +0100 Subject: [PATCH 601/979] feat(media)!: Make all fields of Thumbnail required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It seems sensible to assume that if a client is able to generate a thumbnail, it should be able to get all this information for it too. A thumbnail with no information is not really useful, as we don't know when it could be used instead of the original image. Removes `BaseThumbnailInfo`. Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/src/ruma.rs | 19 +- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 25 ++- crates/matrix-sdk/src/attachment.rs | 42 ++--- crates/matrix-sdk/src/room/mod.rs | 11 +- crates/matrix-sdk/src/send_queue/upload.rs | 53 ++---- .../tests/integration/room/attachment/mod.rs | 13 +- .../tests/integration/send_queue.rs | 172 +----------------- 7 files changed, 62 insertions(+), 273 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 7d70f6bd103..7d1c402215a 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -15,9 +15,7 @@ use std::{collections::BTreeSet, sync::Arc, time::Duration}; use extension_trait::extension_trait; -use matrix_sdk::attachment::{ - BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, -}; +use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo}; use ruma::{ assign, events::{ @@ -747,21 +745,6 @@ impl From for RumaThumbnailInfo { } } -impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo { - type Error = MediaInfoError; - - fn try_from(value: &ThumbnailInfo) -> Result { - let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?) - .map_err(|_| MediaInfoError::InvalidField)?; - let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?) - .map_err(|_| MediaInfoError::InvalidField)?; - let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?) - .map_err(|_| MediaInfoError::InvalidField)?; - - Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) }) - } -} - #[derive(Clone, uniffi::Record)] pub struct NoticeMessageContent { pub body: String, diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index af449c112a0..bf0d3ea1bdc 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -24,7 +24,7 @@ use matrix_sdk::crypto::CollectStrategy; use matrix_sdk::{ attachment::{ AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, - BaseThumbnailInfo, BaseVideoInfo, Thumbnail, + BaseVideoInfo, Thumbnail, }, deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode}, room::edit::EditedContent as SdkEditedContent, @@ -53,7 +53,7 @@ use ruma::{ }, AnyMessageLikeEventContent, }, - EventId, + EventId, UInt, }; use tokio::{ sync::Mutex, @@ -144,19 +144,26 @@ fn build_thumbnail_info( let thumbnail_data = fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?; - let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info) - .map_err(|_| RoomError::InvalidAttachmentData)?; + let height = thumbnail_info + .height + .and_then(|u| UInt::try_from(u).ok()) + .ok_or(RoomError::InvalidAttachmentData)?; + let width = thumbnail_info + .width + .and_then(|u| UInt::try_from(u).ok()) + .ok_or(RoomError::InvalidAttachmentData)?; + let size = thumbnail_info + .width + .and_then(|u| UInt::try_from(u).ok()) + .ok_or(RoomError::InvalidAttachmentData)?; let mime_str = thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; let mime_type = mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let thumbnail = Thumbnail { - data: thumbnail_data, - content_type: mime_type, - info: Some(base_thumbnail_info), - }; + let thumbnail = + Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size }; Ok(AttachmentConfig::with_thumbnail(thumbnail)) } diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 986efcd8788..ee35cce5893 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -148,27 +148,6 @@ impl From for FileInfo { } } -#[derive(Debug, Default, Clone)] -/// Base metadata about a thumbnail. -pub struct BaseThumbnailInfo { - /// The height of the thumbnail in pixels. - pub height: Option, - /// The width of the thumbnail in pixels. - pub width: Option, - /// The file size of the thumbnail in bytes. - pub size: Option, -} - -impl From for ThumbnailInfo { - fn from(info: BaseThumbnailInfo) -> Self { - assign!(ThumbnailInfo::new(), { - height: info.height, - width: info.width, - size: info.size, - }) - } -} - /// A thumbnail to upload and send for an attachment. #[derive(Debug)] pub struct Thumbnail { @@ -176,20 +155,23 @@ pub struct Thumbnail { pub data: Vec, /// The type of the thumbnail, this will be used as the content-type header. pub content_type: mime::Mime, - /// The metadata of the thumbnail. - pub info: Option, + /// The height of the thumbnail in pixels. + pub height: UInt, + /// The width of the thumbnail in pixels. + pub width: UInt, + /// The file size of the thumbnail in bytes. + pub size: UInt, } impl Thumbnail { /// Convert this `Thumbnail` into a `(data, content_type, info)` tuple. pub fn into_parts(self) -> (Vec, mime::Mime, Box) { - let thumbnail_info = assign!( - self.info - .as_ref() - .map(|info| ThumbnailInfo::from(info.clone())) - .unwrap_or_default(), - { mimetype: Some(self.content_type.to_string()) } - ); + let thumbnail_info = assign!(ThumbnailInfo::new(), { + height: Some(self.height), + width: Some(self.width), + size: Some(self.size), + mimetype: Some(self.content_type.to_string()) + }); (self.data, self.content_type, Box::new(thumbnail_info)) } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 1c3dc8f028b..0111977366b 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1949,14 +1949,9 @@ impl Room { // If necessary, store caching data for the thumbnail ahead of time. let thumbnail_cache_info = if store_in_cache { - // Use a small closure returning Option to avoid an unnecessary complicated - // chain of map/and_then. - let get_info = || { - let thumbnail = thumbnail.as_ref()?; - let info = thumbnail.info.as_ref()?; - Some((thumbnail.data.clone(), info.height?, info.width?)) - }; - get_info() + thumbnail + .as_ref() + .map(|thumbnail| (thumbnail.data.clone(), thumbnail.height, thumbnail.width)) } else { None }; diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 5ef27f99ef6..269f6ca3858 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -31,7 +31,7 @@ use ruma::{ }, AnyMessageLikeEventContent, }, - uint, OwnedMxcUri, OwnedTransactionId, TransactionId, UInt, + OwnedMxcUri, OwnedTransactionId, TransactionId, UInt, }; use tracing::{debug, error, instrument, trace, warn, Span}; @@ -182,23 +182,17 @@ impl RoomSendQueue { // Process the thumbnail, if it's been provided. if let Some(thumbnail) = config.thumbnail.take() { - // Create the information required for filling the thumbnail section of the - // media event. - let (data, content_type, thumbnail_info) = thumbnail.into_parts(); - // Normalize information to retrieve the thumbnail in the cache store. - let height = thumbnail_info.height.unwrap_or_else(|| { - trace!("thumbnail height is unknown, using 0 for the cache entry"); - uint!(0) - }); - let width = thumbnail_info.width.unwrap_or_else(|| { - trace!("thumbnail width is unknown, using 0 for the cache entry"); - uint!(0) - }); + let height = thumbnail.height; + let width = thumbnail.width; let txn = TransactionId::new(); trace!(upload_thumbnail_txn = %txn, thumbnail_size = ?(height, width), "attachment has a thumbnail"); + // Create the information required for filling the thumbnail section of the + // media event. + let (data, content_type, thumbnail_info) = thumbnail.into_parts(); + // Cache thumbnail in the cache store. let thumbnail_media_request = make_local_thumbnail_media_request(&txn, height, width); @@ -320,27 +314,18 @@ impl QueueStorage { let from_req = make_local_thumbnail_media_request(&info.txn, info.height, info.width); - if info.height == uint!(0) || info.width == uint!(0) { - trace!(from = ?from_req.source, "removing thumbnail with unknown dimension from cache store"); - - cache_store - .remove_media_content(&from_req) - .await - .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; - } else { - trace!(from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); - - // Reuse the same format for the cached thumbnail with the final MXC ID. - let new_format = from_req.format.clone(); - - cache_store - .replace_media_key( - &from_req, - &MediaRequestParameters { source: new_source, format: new_format }, - ) - .await - .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; - } + trace!(from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); + + // Reuse the same format for the cached thumbnail with the final MXC ID. + let new_format = from_req.format.clone(); + + cache_store + .replace_media_key( + &from_req, + &MediaRequestParameters { source: new_source, format: new_format }, + ) + .await + .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; } } diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index f81a6f09c67..8301ef60b77 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -1,10 +1,7 @@ use std::time::Duration; use matrix_sdk::{ - attachment::{ - AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, - Thumbnail, - }, + attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseVideoInfo, Thumbnail}, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, test_utils::mocks::MatrixMockServer, }; @@ -210,11 +207,9 @@ async fn test_room_attachment_send_info_thumbnail() { let config = AttachmentConfig::with_thumbnail(Thumbnail { data: b"Thumbnail".to_vec(), content_type: mime::IMAGE_JPEG, - info: Some(BaseThumbnailInfo { - height: Some(uint!(360)), - width: Some(uint!(480)), - size: Some(uint!(3600)), - }), + height: uint!(360), + width: uint!(480), + size: uint!(3600), }) .info(AttachmentInfo::Image(BaseImageInfo { height: Some(uint!(600)), diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index a0c2c9e412b..8024a26fb04 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -3,7 +3,7 @@ use std::{ops::Not as _, sync::Arc, time::Duration}; use as_variant::as_variant; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ - attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, Thumbnail}, + attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, Thumbnail}, config::StoreConfig, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, send_queue::{ @@ -74,11 +74,9 @@ async fn queue_attachment_with_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'st let thumbnail = Thumbnail { data: b"thumbnail".to_vec(), content_type: content_type.clone(), - info: Some(BaseThumbnailInfo { - height: Some(uint!(13)), - width: Some(uint!(37)), - size: Some(uint!(42)), - }), + height: uint!(13), + width: uint!(37), + size: uint!(42), }; let config = @@ -1789,11 +1787,9 @@ async fn test_media_uploads() { let thumbnail = Thumbnail { data: b"thumbnail".to_vec(), content_type: content_type.clone(), - info: Some(BaseThumbnailInfo { - height: Some(uint!(13)), - width: Some(uint!(37)), - size: Some(uint!(42)), - }), + height: uint!(13), + width: uint!(37), + size: uint!(42), }; let attachment_info = AttachmentInfo::Image(BaseImageInfo { @@ -1981,160 +1977,6 @@ async fn test_media_uploads() { assert!(watch.is_empty()); } -#[async_test] -async fn test_media_uploads_no_caching_of_thumbnails_of_unknown_sizes() { - let mock = MatrixMockServer::new().await; - - // Mark the room as joined. - let room_id = room_id!("!a:b.c"); - let client = mock.client_builder().build().await; - let room = mock.sync_joined_room(&client, room_id).await; - - let q = room.send_queue(); - - let (local_echoes, mut watch) = q.subscribe().await.unwrap(); - assert!(local_echoes.is_empty()); - - // ---------------------- - // Create the media to send, with a thumbnail that has unknown dimensions. - let filename = "surprise.jpeg.exe"; - let content_type = mime::IMAGE_JPEG; - let data = b"hello world".to_vec(); - - let thumbnail = Thumbnail { - data: b"thumbnail".to_vec(), - content_type: content_type.clone(), - info: Some(BaseThumbnailInfo::default()), - }; - - let attachment_info = AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(14)), - width: Some(uint!(38)), - size: Some(uint!(43)), - blurhash: None, - }); - - let config = AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info); - - // ---------------------- - // Prepare endpoints. - mock.mock_room_state_encryption().plain().mount().await; - mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; - - mock.mock_upload().ok(mxc_uri!("mxc://sdk.rs/thumbnail")).mock_once().mount().await; - mock.mock_upload().ok(mxc_uri!("mxc://sdk.rs/media")).mock_once().mount().await; - - // ---------------------- - // Send the media. - assert!(watch.is_empty()); - q.send_attachment(filename, content_type, data, config) - .await - .expect("queuing the attachment works"); - - // ---------------------- - // Observe the local echo - let (txn, _send_handle, content) = assert_update!(watch => local echo event); - - // Sanity-check metadata. - assert_let!(MessageType::Image(img_content) = content.msgtype); - assert_eq!(img_content.filename(), filename); - - let info = img_content.info.unwrap(); - assert_eq!(info.height, Some(uint!(14))); - assert_eq!(info.width, Some(uint!(38))); - assert_eq!(info.size, Some(uint!(43))); - assert_eq!(info.mimetype.as_deref(), Some("image/jpeg")); - - // Check the data source: it should reference the send queue local storage. - let local_source = img_content.source; - assert_let!(MediaSource::Plain(mxc) = &local_source); - assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); - - // The media is immediately available from the cache. - let file_media = client - .media() - .get_media_content( - &MediaRequestParameters { source: local_source, format: MediaFormat::File }, - true, - ) - .await - .expect("media should be found"); - assert_eq!(file_media, b"hello world"); - - // ---------------------- - // Thumbnail. - - // Check metadata. - let tinfo = info.thumbnail_info.unwrap(); - assert_eq!(tinfo.height, None); - assert_eq!(tinfo.width, None); - assert_eq!(tinfo.size, None); - assert_eq!(tinfo.mimetype.as_deref(), Some("image/jpeg")); - - // Check the thumbnail source: it should reference the send queue local storage. - let local_thumbnail_source = info.thumbnail_source.unwrap(); - assert_let!(MediaSource::Plain(mxc) = &local_thumbnail_source); - assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); - - let thumbnail_media = client - .media() - .get_media_content( - &MediaRequestParameters { - source: local_thumbnail_source, - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(0), uint!(0))), - }, - true, - ) - .await - .expect("media should be found"); - assert_eq!(thumbnail_media, b"thumbnail"); - - // ---------------------- - // Let the upload progress. - - assert_update!(watch => uploaded { related_to = txn, mxc = mxc_uri!("mxc://sdk.rs/thumbnail") }); - assert_update!(watch => uploaded { related_to = txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); - - let edit_msg = assert_update!(watch => edit local echo { txn = txn }); - assert_let!(MessageType::Image(new_content) = edit_msg.msgtype); - assert_let!(MediaSource::Plain(new_uri) = &new_content.source); - assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/media")); - - let file_media = client - .media() - .get_media_content( - &MediaRequestParameters { source: new_content.source, format: MediaFormat::File }, - true, - ) - .await - .expect("media should be found with its final MXC uri in the cache"); - assert_eq!(file_media, b"hello world"); - - let new_thumbnail_source = new_content.info.unwrap().thumbnail_source.unwrap(); - assert_let!(MediaSource::Plain(new_uri) = &new_thumbnail_source); - assert_eq!(new_uri, mxc_uri!("mxc://sdk.rs/thumbnail")); - - // Retrieving the thumbnail should NOT work, since it doesn't make sense to - // cache it with a size of 0. - client - .media() - .get_media_content( - &MediaRequestParameters { - source: new_thumbnail_source, - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(uint!(0), uint!(0))), - }, - true, - ) - .await - .unwrap_err(); - - // The event is sent, at some point. - assert_update!(watch => sent { event_id = event_id!("$1") }); - - // That's all, folks! - assert!(watch.is_empty()); -} - #[async_test] async fn test_media_upload_retry() { let mock = MatrixMockServer::new().await; From 75d7d07013a39c7a26f76cce22eb247bd1cec07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 26 Nov 2024 15:21:35 +0100 Subject: [PATCH 602/979] chore(ffi): Fix thumbnail size info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index bf0d3ea1bdc..c9075a928c1 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -153,7 +153,7 @@ fn build_thumbnail_info( .and_then(|u| UInt::try_from(u).ok()) .ok_or(RoomError::InvalidAttachmentData)?; let size = thumbnail_info - .width + .size .and_then(|u| UInt::try_from(u).ok()) .ok_or(RoomError::InvalidAttachmentData)?; From db84936dcd6bb4600c5fcdf83188d65ccf374480 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 26 Nov 2024 14:49:41 +0100 Subject: [PATCH 603/979] fix(room): make `Room::history_visibility()` return an Option And introduce `Room::history_visibility_or_default()` to return a better sensible default, according to the spec. --- bindings/matrix-sdk-ffi/src/room_preview.rs | 2 +- crates/matrix-sdk-base/src/client.rs | 15 ++++++---- crates/matrix-sdk-base/src/rooms/normal.rs | 29 +++++++++++++++---- crates/matrix-sdk/src/room/mod.rs | 2 +- crates/matrix-sdk/src/room_preview.rs | 10 ++++--- .../src/tests/sliding_sync/room.rs | 2 +- 6 files changed, 42 insertions(+), 18 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 8bfe945519e..49d040c52e1 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -92,7 +92,7 @@ pub struct RoomPreviewInfo { /// The room type (space, custom) or nothing, if it's a regular room. pub room_type: RoomType, /// Is the history world-readable for this room? - pub is_history_world_readable: bool, + pub is_history_world_readable: Option, /// The membership state for the current user, if known. pub membership: Option, /// The join rule for this room (private, public, knock, etc.). diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 1fcaee38b26..34ef269366c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1459,10 +1459,14 @@ impl BaseClient { pub async fn share_room_key(&self, room_id: &RoomId) -> Result>> { match self.olm_machine().await.as_ref() { Some(o) => { - let (history_visibility, settings) = self - .get_room(room_id) - .map(|r| (r.history_visibility(), r.encryption_settings())) - .unwrap_or((HistoryVisibility::Joined, None)); + let Some(room) = self.get_room(room_id) else { + return Err(Error::InsufficientData); + }; + + let history_visibility = room.history_visibility_or_default(); + let Some(room_encryption_event) = room.encryption_settings() else { + return Err(Error::EncryptionNotEnabled); + }; // Don't share the group session with members that are invited // if the history visibility is set to `Joined` @@ -1474,9 +1478,8 @@ impl BaseClient { let members = self.store.get_user_ids(room_id, filter).await?; - let settings = settings.ok_or(Error::EncryptionNotEnabled)?; let settings = EncryptionSettings::new( - settings, + room_encryption_event, history_visibility, self.room_key_recipient_strategy.clone(), ); diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index d80da192610..d05ce29d1ad 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -487,8 +487,14 @@ impl Room { } /// Get the history visibility policy of this room. - pub fn history_visibility(&self) -> HistoryVisibility { - self.inner.read().history_visibility().clone() + pub fn history_visibility(&self) -> Option { + self.inner.read().history_visibility().cloned() + } + + /// Get the history visibility policy of this room, or a sensible default if + /// the event is missing. + pub fn history_visibility_or_default(&self) -> HistoryVisibility { + self.inner.read().history_visibility_or_default().clone() } /// Is the room considered to be public. @@ -1522,11 +1528,24 @@ impl RoomInfo { /// Returns the history visibility for this room. /// - /// Defaults to `WorldReadable`, if missing. - pub fn history_visibility(&self) -> &HistoryVisibility { + /// Returns None if the event was never seen during sync. + pub fn history_visibility(&self) -> Option<&HistoryVisibility> { + match &self.base_info.history_visibility { + Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility), + _ => None, + } + } + + /// Returns the history visibility for this room, or a sensible default. + /// + /// Returns `Shared`, the default specified by the [spec], when the event is + /// missing. + /// + /// [spec]: https://spec.matrix.org/latest/client-server-api/#server-behaviour-7 + pub fn history_visibility_or_default(&self) -> &HistoryVisibility { match &self.base_info.history_visibility { Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility, - _ => &HistoryVisibility::WorldReadable, + _ => &HistoryVisibility::Shared, } } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 0111977366b..285411de6e5 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -613,7 +613,7 @@ impl Room { fn are_events_visible(&self) -> bool { if let RoomState::Invited = self.inner.state() { return matches!( - self.inner.history_visibility(), + self.inner.history_visibility_or_default(), HistoryVisibility::WorldReadable | HistoryVisibility::Invited ); } diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 198d335ef66..58c9c68a501 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -68,7 +68,7 @@ pub struct RoomPreview { /// Is the room world-readable (i.e. is its history_visibility set to /// world_readable)? - pub is_world_readable: bool, + pub is_world_readable: Option, /// Has the current user been invited/joined/left this room? /// @@ -115,7 +115,9 @@ impl RoomPreview { SpaceRoomJoinRule::Private } }, - is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable, + is_world_readable: room_info + .history_visibility() + .map(|vis| *vis == HistoryVisibility::WorldReadable), num_joined_members, num_active_members, state, @@ -266,7 +268,7 @@ impl RoomPreview { num_active_members, room_type: response.room_type, join_rule: response.join_rule, - is_world_readable: response.world_readable, + is_world_readable: Some(response.world_readable), state, is_direct, heroes: cached_room.map(|r| r.heroes()), @@ -361,7 +363,7 @@ async fn search_for_room_preview_in_room_directory( panic!("Unexpected PublicRoomJoinRule {:?}", room_description.join_rule) } }, - is_world_readable: room_description.is_world_readable, + is_world_readable: Some(room_description.is_world_readable), state: None, is_direct: None, heroes: None, diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index aa85dcb0360..04d4a9774e1 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -1204,7 +1204,7 @@ fn assert_room_preview(preview: &RoomPreview, room_alias: &str) { assert_eq!(preview.num_joined_members, 1); assert!(preview.room_type.is_none()); assert_eq!(preview.join_rule, SpaceRoomJoinRule::Invite); - assert!(preview.is_world_readable); + assert!(preview.is_world_readable.unwrap()); } async fn get_room_preview_with_room_state( From 8dc7c1f8766b55fc06de09fe6a8c0458b78dec28 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 26 Nov 2024 14:52:42 +0100 Subject: [PATCH 604/979] fix(ui): have the room list service require the create and history visibility events These two are required to properly compute the room preview of a joined room: - m.room.create ends up filling the `room_type` (space or not) - m.room.history_visibility ends up filling the `is_world_readable` field. --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 5 ++++- crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 0ac74d6c79a..441c79944bb 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -89,12 +89,15 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[ (StateEventType::RoomPowerLevels, ""), (StateEventType::CallMember, "*"), (StateEventType::RoomJoinRules, ""), + // Those two events are required to properly compute room previews. + (StateEventType::RoomCreate, ""), + (StateEventType::RoomHistoryVisibility, ""), ]; /// The default `required_state` constant value for sliding sync room /// subscriptions that must be added to `DEFAULT_REQUIRED_STATE`. const DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE: &[(StateEventType, &str)] = - &[(StateEventType::RoomCreate, ""), (StateEventType::RoomPinnedEvents, "")]; + &[(StateEventType::RoomPinnedEvents, "")]; /// The default `timeline_limit` value when used with room subscriptions. const DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT: u32 = 20; 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 6dd85150f76..40c448aab83 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -358,6 +358,8 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.power_levels", ""], ["org.matrix.msc3401.call.member", "*"], ["m.room.join_rules", ""], + ["m.room.create", ""], + ["m.room.history_visibility", ""], ], "include_heroes": true, "filters": { @@ -2224,6 +2226,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["org.matrix.msc3401.call.member", "*"], ["m.room.join_rules", ""], ["m.room.create", ""], + ["m.room.history_visibility", ""], ["m.room.pinned_events", ""], ], "timeline_limit": 20, @@ -2263,6 +2266,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["org.matrix.msc3401.call.member", "*"], ["m.room.join_rules", ""], ["m.room.create", ""], + ["m.room.history_visibility", ""], ["m.room.pinned_events", ""], ], "timeline_limit": 20, From 2c45316bcb17d9f963ce773d926ae4b80912642f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 26 Nov 2024 18:18:38 +0100 Subject: [PATCH 605/979] fixup! fix(room): make `Room::history_visibility()` return an Option --- .../matrix-sdk-integration-testing/src/tests/e2ee.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs index 1c72ae8c17a..abc24e110d0 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee.rs @@ -672,11 +672,14 @@ async fn test_failed_members_response() -> Result<()> { bob.sync_once().await?; - // Cause a failure of a sync_members request by asking for members before - // joining. Since this is a private DM room, it will fail with a 401, as - // we're not authorized to look at state history. + // Although we haven't joined the room yet, logic in `sync_members` looks at the + // room's visibility first; since it may be unknown for this room, from the + // point of view of Bob, it'll be assumed to be the default, aka shared. As + // a result, `sync_members()` doesn't even spawn a network request, and + // silently ignores the request. + let result = bob.get_room(alice_room.room_id()).unwrap().sync_members().await; - assert!(result.is_err()); + assert!(result.is_ok()); bob.get_room(alice_room.room_id()).unwrap().join().await?; From 3e7d7e8a3178d6fedf5e97cf4110020111a36251 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 26 Nov 2024 16:49:17 +0100 Subject: [PATCH 606/979] chore(ui): Rename `TimelineEnd` to `TimelineNewItemPosition`. This patch renames `TimelineEnd` into `TimelineNewItemPosition` for 2 reasons: 1. In the following patches, we will introduce a new variant to insert at a specific index, so the suffix `End` would no longer make sense. 2. It's exactly like `TimelineItemPosition` except that it's used only and strictly only to add **new** items, which is why we can't use `TimelineItemPosition` because it contains the `UpdateDecrypted` variant. This renaming reflects it's only about **new** items. This patch takes the opportunity to move the `RemoteEventOrigin` inside `TimelineNewItemPosition` to simplify method signatures. They always go together. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 8 ++-- .../src/timeline/controller/mod.rs | 34 ++++++------- .../src/timeline/controller/state.rs | 48 ++++++++++--------- .../matrix-sdk-ui/src/timeline/pagination.rs | 4 +- .../matrix-sdk-ui/src/timeline/tests/basic.rs | 23 +++++---- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 12 +++-- .../src/timeline/tests/reactions.rs | 7 ++- .../src/timeline/tests/redaction.rs | 7 ++- 8 files changed, 77 insertions(+), 66 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 2314a1afd3a..4de77f57862 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -31,7 +31,7 @@ use super::{ Error, Timeline, TimelineDropHandle, TimelineFocus, }; use crate::{ - timeline::{controller::TimelineEnd, event_item::RemoteEventOrigin}, + timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin}, unable_to_decrypt_hook::UtdHookManager, }; @@ -273,9 +273,9 @@ impl TimelineBuilder { inner.add_events_at( events, - TimelineEnd::Back, - match origin { - EventsOrigin::Sync => RemoteEventOrigin::Sync, + TimelineNewItemPosition::End { origin: match origin { + EventsOrigin::Sync => RemoteEventOrigin::Sync, + } } ).await; } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 6b8605ba7e2..05de8cb3c89 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -52,8 +52,8 @@ use tracing::{ }; pub(super) use self::state::{ - EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineEnd, TimelineMetadata, - TimelineState, TimelineStateTransaction, + EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, + TimelineNewItemPosition, TimelineState, TimelineStateTransaction, }; use super::{ event_handler::TimelineEventKind, @@ -404,8 +404,11 @@ impl TimelineController

{ .map_err(PaginationError::Paginator)?, }; - self.add_events_at(pagination.events, TimelineEnd::Front, RemoteEventOrigin::Pagination) - .await; + self.add_events_at( + pagination.events, + TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }, + ) + .await; Ok(pagination.hit_end_of_timeline) } @@ -428,8 +431,11 @@ impl TimelineController

{ .map_err(PaginationError::Paginator)?, }; - self.add_events_at(pagination.events, TimelineEnd::Back, RemoteEventOrigin::Pagination) - .await; + self.add_events_at( + pagination.events, + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Pagination }, + ) + .await; Ok(pagination.hit_end_of_timeline) } @@ -629,23 +635,14 @@ impl TimelineController

{ pub(super) async fn add_events_at( &self, events: Vec>, - position: TimelineEnd, - origin: RemoteEventOrigin, + position: TimelineNewItemPosition, ) -> HandleManyEventsResult { if events.is_empty() { return Default::default(); } let mut state = self.state.write().await; - state - .add_remote_events_at( - events, - position, - origin, - &self.room_data_provider, - &self.settings, - ) - .await + state.add_remote_events_at(events, position, &self.room_data_provider, &self.settings).await } pub(super) async fn clear(&self) { @@ -683,8 +680,7 @@ impl TimelineController

{ state .replace_with_remote_events( events, - TimelineEnd::Back, - origin, + TimelineNewItemPosition::End { origin }, &self.room_data_provider, &self.settings, ) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 3668622dacd..4204481b9f4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -64,16 +64,27 @@ use crate::{ unable_to_decrypt_hook::UtdHookManager, }; -/// Which end of the timeline should an event be added to? -/// -/// This is a simplification of `TimelineItemPosition` which doesn't contain the -/// `Update` variant, when adding a bunch of events at the same time. +/// This is a simplification of [`TimelineItemPosition`] which doesn't contain +/// the [`TimelineItemPosition::UpdateDecrypted`] variant, because it is used +/// only for **new** items. #[derive(Debug)] -pub(crate) enum TimelineEnd { - /// Event should be prepended to the front of the timeline. - Front, - /// Event should be appended to the back of the timeline. - Back, +pub(crate) enum TimelineNewItemPosition { + /// One or more items are prepended to the timeline (i.e. they're the + /// oldest). + Start { origin: RemoteEventOrigin }, + + /// One or more items are appended to the timeline (i.e. they're the most + /// recent). + End { origin: RemoteEventOrigin }, +} + +impl From for TimelineItemPosition { + fn from(value: TimelineNewItemPosition) -> Self { + match value { + TimelineNewItemPosition::Start { origin } => Self::Start { origin }, + TimelineNewItemPosition::End { origin } => Self::End { origin }, + } + } } #[derive(Debug)] @@ -119,8 +130,7 @@ impl TimelineState { pub(super) async fn add_remote_events_at( &mut self, events: Vec>, - position: TimelineEnd, - origin: RemoteEventOrigin, + position: TimelineNewItemPosition, room_data_provider: &P, settings: &TimelineSettings, ) -> HandleManyEventsResult { @@ -130,7 +140,7 @@ impl TimelineState { let mut txn = self.transaction(); let handle_many_res = - txn.add_remote_events_at(events, position, origin, room_data_provider, settings).await; + txn.add_remote_events_at(events, position, room_data_provider, settings).await; txn.commit(); handle_many_res @@ -285,15 +295,13 @@ impl TimelineState { pub(super) async fn replace_with_remote_events( &mut self, events: Vec, - position: TimelineEnd, - origin: RemoteEventOrigin, + position: TimelineNewItemPosition, room_data_provider: &P, settings: &TimelineSettings, ) -> HandleManyEventsResult { let mut txn = self.transaction(); txn.clear(); - let result = - txn.add_remote_events_at(events, position, origin, room_data_provider, settings).await; + let result = txn.add_remote_events_at(events, position, room_data_provider, settings).await; txn.commit(); result } @@ -347,17 +355,13 @@ impl TimelineStateTransaction<'_> { pub(super) async fn add_remote_events_at( &mut self, events: Vec>, - position: TimelineEnd, - origin: RemoteEventOrigin, + position: TimelineNewItemPosition, room_data_provider: &P, settings: &TimelineSettings, ) -> HandleManyEventsResult { let mut total = HandleManyEventsResult::default(); - let position = match position { - TimelineEnd::Front => TimelineItemPosition::Start { origin }, - TimelineEnd::Back => TimelineItemPosition::End { origin }, - }; + let position = position.into(); let mut day_divider_adjuster = DayDividerAdjuster::default(); diff --git a/crates/matrix-sdk-ui/src/timeline/pagination.rs b/crates/matrix-sdk-ui/src/timeline/pagination.rs index d46c90f99c1..afdbb48c1ca 100644 --- a/crates/matrix-sdk-ui/src/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/src/timeline/pagination.rs @@ -26,7 +26,7 @@ use matrix_sdk::event_cache::{ use tracing::{instrument, trace, warn}; use super::Error; -use crate::timeline::{controller::TimelineEnd, event_item::RemoteEventOrigin}; +use crate::timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin}; impl super::Timeline { /// Add more events to the start of the timeline. @@ -81,7 +81,7 @@ impl super::Timeline { // `matrix_sdk::event_cache::RoomEventCacheUpdate` from // `matrix_sdk::event_cache::RoomPagination::run_backwards`. self.controller - .add_events_at(events, TimelineEnd::Front, RemoteEventOrigin::Pagination) + .add_events_at(events, TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }) .await; if num_events == 0 && !reached_start { diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index 3d86d2c4383..bcc0d7cd296 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -35,7 +35,7 @@ use stream_assert::assert_next_matches; use super::TestTimeline; use crate::timeline::{ - controller::{TimelineEnd, TimelineSettings}, + controller::{TimelineNewItemPosition, TimelineSettings}, event_item::{AnyOtherFullStateEventContent, RemoteEventOrigin}, tests::{ReadReceiptMap, TestRoomDataProvider}, MembershipChange, TimelineDetails, TimelineItemContent, TimelineItemKind, VirtualTimelineItem, @@ -51,8 +51,7 @@ async fn test_initial_events() { .controller .add_events_at( vec![f.text_msg("A").sender(*ALICE), f.text_msg("B").sender(*BOB)], - TimelineEnd::Back, - RemoteEventOrigin::Sync, + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; @@ -91,7 +90,10 @@ async fn test_replace_with_initial_events_and_read_marker() { let f = &timeline.factory; let ev = f.text_msg("hey").sender(*ALICE).into_sync(); - timeline.controller.add_events_at(vec![ev], TimelineEnd::Back, RemoteEventOrigin::Sync).await; + timeline + .controller + .add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }) + .await; let items = timeline.controller.items().await; assert_eq!(items.len(), 2); @@ -317,8 +319,7 @@ async fn test_dedup_initial() { // … and a new event also came in event_c, ], - TimelineEnd::Back, - RemoteEventOrigin::Sync, + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; @@ -354,7 +355,10 @@ async fn test_internal_id_prefix() { timeline .controller - .add_events_at(vec![ev_a, ev_b, ev_c], TimelineEnd::Back, RemoteEventOrigin::Sync) + .add_events_at( + vec![ev_a, ev_b, ev_c], + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, + ) .await; let timeline_items = timeline.controller.items().await; @@ -516,7 +520,10 @@ async fn test_replace_with_initial_events_when_batched() { let f = &timeline.factory; let ev = f.text_msg("hey").sender(*ALICE).into_sync(); - timeline.controller.add_events_at(vec![ev], TimelineEnd::Back, RemoteEventOrigin::Sync).await; + timeline + .controller + .add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }) + .await; let (items, mut stream) = timeline.controller.subscribe_batched().await; assert_eq!(items.len(), 2); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index e212a82d6be..943b641b099 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -57,7 +57,7 @@ use ruma::{ use tokio::sync::RwLock; use super::{ - controller::{TimelineEnd, TimelineSettings}, + controller::{TimelineNewItemPosition, TimelineSettings}, event_handler::TimelineEventKind, event_item::RemoteEventOrigin, traits::RoomDataProvider, @@ -237,7 +237,10 @@ impl TestTimeline { async fn handle_live_event(&self, event: impl Into) { let event = event.into(); self.controller - .add_events_at(vec![event], TimelineEnd::Back, RemoteEventOrigin::Sync) + .add_events_at( + vec![event], + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, + ) .await; } @@ -256,7 +259,10 @@ impl TestTimeline { async fn handle_back_paginated_event(&self, event: Raw) { let timeline_event = TimelineEvent::new(event.cast()); self.controller - .add_events_at(vec![timeline_event], TimelineEnd::Front, RemoteEventOrigin::Pagination) + .add_events_at( + vec![timeline_event], + TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }, + ) .await; } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs index d1e92e4ca49..c146abee637 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs @@ -28,8 +28,8 @@ use stream_assert::assert_next_matches; use tokio::time::timeout; use crate::timeline::{ - controller::TimelineEnd, event_item::RemoteEventOrigin, tests::TestTimeline, ReactionStatus, - TimelineEventItemId, TimelineItem, + controller::TimelineNewItemPosition, event_item::RemoteEventOrigin, tests::TestTimeline, + ReactionStatus, TimelineEventItemId, TimelineItem, }; const REACTION_KEY: &str = "👍"; @@ -204,8 +204,7 @@ async fn test_initial_reaction_timestamp_is_stored() { // Event comes next. f.text_msg("A").event_id(&message_event_id).into_sync(), ], - TimelineEnd::Back, - RemoteEventOrigin::Sync, + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs index e592660f074..73f3eff0eff 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs @@ -29,8 +29,8 @@ use stream_assert::assert_next_matches; use super::TestTimeline; use crate::timeline::{ - controller::TimelineEnd, event_item::RemoteEventOrigin, AnyOtherFullStateEventContent, - TimelineDetails, TimelineItemContent, + controller::TimelineNewItemPosition, event_item::RemoteEventOrigin, + AnyOtherFullStateEventContent, TimelineDetails, TimelineItemContent, }; #[async_test] @@ -146,8 +146,7 @@ async fn test_reaction_redaction_timeline_filter() { .event_builder .make_sync_redacted_message_event(*ALICE, RedactedReactionEventContent::new()), )], - TimelineEnd::Back, - RemoteEventOrigin::Sync, + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; // Timeline items are actually empty. From 1098095846271d494aceeecdc51590a93dd119bd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 27 Nov 2024 09:16:37 +0100 Subject: [PATCH 607/979] refactor(linked chunk): replace `LinkedChunk::len()` with a simpler implementation It's unused so it's mostly cosmetic, and it's trivial to reimplement using `linked_chunk.items().count()`; let's do that instead of keeping the perfect exact count synchronized with the chunks, which pollutes the code in a few places. --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 110 +++++++----------- .../matrix-sdk/src/event_cache/room/events.rs | 2 +- 2 files changed, 43 insertions(+), 69 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index db1a3b5486c..c9147a45e43 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -233,9 +233,6 @@ pub struct LinkedChunk { /// The links to the chunks, i.e. the first and the last chunk. links: Ends, - /// The number of items hold by this linked chunk. - length: usize, - /// The generator of chunk identifiers. chunk_identifier_generator: ChunkIdentifierGenerator, @@ -263,7 +260,6 @@ impl LinkedChunk { first: Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER), last: None, }, - length: 0, chunk_identifier_generator: ChunkIdentifierGenerator::new_from_scratch(), updates: None, marker: PhantomData, @@ -282,7 +278,6 @@ impl LinkedChunk { first: Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER), last: None, }, - length: 0, chunk_identifier_generator: ChunkIdentifierGenerator::new_from_scratch(), updates: Some(ObservableUpdates::new()), marker: PhantomData, @@ -294,9 +289,6 @@ impl LinkedChunk { // Clear `self.links`. self.links.clear(); - // Clear `self.length`. - self.length = 0; - // Clear `self.chunk_identifier_generator`. self.chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch(); @@ -312,12 +304,6 @@ impl LinkedChunk { } } - /// Get the number of items in this linked chunk. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.length - } - /// Push items at the end of the [`LinkedChunk`], i.e. on the last /// chunk. /// @@ -331,7 +317,6 @@ impl LinkedChunk { I::IntoIter: ExactSizeIterator, { let items = items.into_iter(); - let number_of_items = items.len(); let last_chunk = self.links.latest_chunk_mut(); @@ -348,8 +333,6 @@ impl LinkedChunk { // OK. self.links.last = Some(last_chunk.as_ptr()); } - - self.length += number_of_items; } /// Push a gap at the end of the [`LinkedChunk`], i.e. after the last @@ -387,7 +370,7 @@ impl LinkedChunk { .chunk_mut(chunk_identifier) .ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?; - let (chunk, number_of_items) = match &mut chunk.content { + let chunk = match &mut chunk.content { ChunkContent::Gap(..) => { return Err(Error::ChunkIsAGap { identifier: chunk_identifier }) } @@ -401,50 +384,46 @@ impl LinkedChunk { // Prepare the items to be pushed. let items = items.into_iter(); - let number_of_items = items.len(); - - ( - // Push at the end of the current items. - if item_index == current_items_length { - chunk - // Push the new items. - .push_items(items, &self.chunk_identifier_generator, &mut self.updates) + + // Push at the end of the current items. + if item_index == current_items_length { + chunk + // Push the new items. + .push_items(items, &self.chunk_identifier_generator, &mut self.updates) + } + // Insert inside the current items. + else { + if let Some(updates) = self.updates.as_mut() { + updates.push(Update::DetachLastItems { + at: Position(chunk_identifier, item_index), + }); } - // Insert inside the current items. - else { - if let Some(updates) = self.updates.as_mut() { - updates.push(Update::DetachLastItems { - at: Position(chunk_identifier, item_index), - }); - } - // Split the items. - let detached_items = current_items.split_off(item_index); + // Split the items. + let detached_items = current_items.split_off(item_index); - let chunk = chunk - // Push the new items. - .push_items(items, &self.chunk_identifier_generator, &mut self.updates); + let chunk = chunk + // Push the new items. + .push_items(items, &self.chunk_identifier_generator, &mut self.updates); - if let Some(updates) = self.updates.as_mut() { - updates.push(Update::StartReattachItems); - } + if let Some(updates) = self.updates.as_mut() { + updates.push(Update::StartReattachItems); + } - let chunk = chunk - // Finally, push the items that have been detached. - .push_items( - detached_items.into_iter(), - &self.chunk_identifier_generator, - &mut self.updates, - ); + let chunk = chunk + // Finally, push the items that have been detached. + .push_items( + detached_items.into_iter(), + &self.chunk_identifier_generator, + &mut self.updates, + ); - if let Some(updates) = self.updates.as_mut() { - updates.push(Update::EndReattachItems); - } + if let Some(updates) = self.updates.as_mut() { + updates.push(Update::EndReattachItems); + } - chunk - }, - number_of_items, - ) + chunk + } } }; @@ -456,8 +435,6 @@ impl LinkedChunk { self.links.last = Some(chunk.as_ptr()); } - self.length += number_of_items; - Ok(()) } @@ -525,8 +502,6 @@ impl LinkedChunk { } } - self.length -= 1; - // Stop borrowing `chunk`. } @@ -677,10 +652,9 @@ impl LinkedChunk { debug_assert!(chunk.is_first_chunk().not(), "A gap cannot be the first chunk"); - let (maybe_last_chunk_ptr, number_of_items) = match &mut chunk.content { + let maybe_last_chunk_ptr = match &mut chunk.content { ChunkContent::Gap(..) => { let items = items.into_iter(); - let number_of_items = items.len(); let last_inserted_chunk = chunk // Insert a new items chunk… @@ -691,11 +665,9 @@ impl LinkedChunk { // … and insert the items. .push_items(items, &self.chunk_identifier_generator, &mut self.updates); - ( - last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr()), - number_of_items, - ) + last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr()) } + ChunkContent::Items(..) => { return Err(Error::ChunkIsItems { identifier: chunk_identifier }) } @@ -717,8 +689,6 @@ impl LinkedChunk { self.links.last = Some(last_chunk_ptr); } - self.length += number_of_items; - // Stop borrowing `chunk`. } @@ -896,6 +866,11 @@ impl LinkedChunk { Some(AsVector::new(updates, token, chunk_iterator)) } + + /// Returns the number of items of the linked chunk. + fn len(&self) -> usize { + self.items().count() + } } impl Drop for LinkedChunk { @@ -1387,7 +1362,6 @@ where .debug_struct("LinkedChunk") .field("first (deref)", unsafe { self.links.first.as_ref() }) .field("last", &self.links.last) - .field("length", &self.length) .finish_non_exhaustive() } } diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 987bb151788..7e5e004894e 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -383,7 +383,7 @@ mod tests { fn test_new_room_events_has_zero_events() { let room_events = RoomEvents::new(); - assert_eq!(room_events.chunks.len(), 0); + assert_eq!(room_events.events().count(), 0); } #[test] From 23ee8e25dd090f84417e0f871cea9f7751cc94dd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 21 Nov 2024 15:37:07 +0100 Subject: [PATCH 608/979] feat(linked chunk): add a way to reconstruct a linked chunk from its raw representation --- .../src/linked_chunk/builder.rs | 486 ++++++++++++++++++ .../matrix-sdk-common/src/linked_chunk/mod.rs | 12 +- 2 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 crates/matrix-sdk-common/src/linked_chunk/builder.rs diff --git a/crates/matrix-sdk-common/src/linked_chunk/builder.rs b/crates/matrix-sdk-common/src/linked_chunk/builder.rs new file mode 100644 index 00000000000..04c6b5bdc60 --- /dev/null +++ b/crates/matrix-sdk-common/src/linked_chunk/builder.rs @@ -0,0 +1,486 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + collections::{BTreeMap, HashSet}, + marker::PhantomData, +}; + +use tracing::error; + +use super::{ + Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk, + ObservableUpdates, +}; + +/// A temporary chunk representation in the [`LinkedChunkBuilder`]. +/// +/// Instead of using linking the chunks with pointers, this uses +/// [`ChunkIdentifier`] as the temporary links to the previous and next chunks, +/// which will get resolved later when re-building the full data structure. This +/// allows using chunks that references other chunks that aren't known yet. +struct TemporaryChunk { + id: ChunkIdentifier, + previous: Option, + next: Option, + content: ChunkContent, +} + +/// A data structure to rebuild a linked chunk from its raw representation. +/// +/// A linked chunk can be rebuilt incrementally from its internal +/// representation, with the chunks being added *in any order*, as long as they +/// form a single connected component eventually (viz., there's no +/// subgraphs/sublists isolated from the one final linked list). If they don't, +/// then the final call to [`LinkedChunkBuilder::build()`] will result in an +/// error). +#[allow(missing_debug_implementations)] +pub struct LinkedChunkBuilder { + /// Work-in-progress chunks. + chunks: BTreeMap>, + + /// Is the final `LinkedChunk` expected to include an update history, as if + /// it were created with [`LinkedChunk::new_with_update_history`]? + build_with_update_history: bool, +} + +impl Default for LinkedChunkBuilder { + fn default() -> Self { + Self::new() + } +} + +impl LinkedChunkBuilder { + /// Create an empty [`LinkedChunkBuilder`] with no update history. + pub fn new() -> Self { + Self { chunks: Default::default(), build_with_update_history: false } + } + + /// Stash a gap chunk with its content. + /// + /// This can be called even if the previous and next chunks have not been + /// added yet. Resolving these chunks will happen at the time of calling + /// [`LinkedChunkBuilder::build()`]. + pub fn push_gap( + &mut self, + previous: Option, + id: ChunkIdentifier, + next: Option, + content: Gap, + ) { + let chunk = TemporaryChunk { id, previous, next, content: ChunkContent::Gap(content) }; + self.chunks.insert(id, chunk); + } + + /// Stash an item chunk with its contents. + /// + /// This can be called even if the previous and next chunks have not been + /// added yet. Resolving these chunks will happen at the time of calling + /// [`LinkedChunkBuilder::build()`]. + pub fn push_items( + &mut self, + previous: Option, + id: ChunkIdentifier, + next: Option, + items: impl IntoIterator, + ) { + let chunk = TemporaryChunk { + id, + previous, + next, + content: ChunkContent::Items(items.into_iter().collect()), + }; + self.chunks.insert(id, chunk); + } + + /// Request that the resulting linked chunk will have an update history, as + /// if it were created with [`LinkedChunk::new_with_update_history`]. + pub fn with_update_history(&mut self) { + self.build_with_update_history = true; + } + + /// Run all error checks before reconstructing the full linked chunk. + /// + /// Must be called after checking `self.chunks` isn't empty. + /// + /// Returns the identifier of the first chunk. + fn check_consistency(&mut self) -> Result { + // Look for the first id. + let mut first_id = None; + + for (id, chunk) in self.chunks.iter() { + if chunk.previous.is_none() { + // This chunk is a good candidate to be the first chunk. + first_id = Some(*id); + break; + } + } + + // There's no first chunk, but we've checked that `self.chunks` isn't empty: + // it's a malformed list. + let Some(first_id) = first_id else { + return Err(LinkedChunkBuilderError::MissingFirstChunk); + }; + + // We're going to iterate from the first to the last chunk. + // Keep track of chunks we've already visited. + let mut visited = HashSet::new(); + + // Start from the first chunk. + let mut maybe_cur = Some(first_id); + + while let Some(cur) = maybe_cur { + // The chunk must be referenced in `self.chunks`. + let Some(chunk) = self.chunks.get(&cur) else { + return Err(LinkedChunkBuilderError::MissingChunk { id: cur }); + }; + + if let ChunkContent::Items(items) = &chunk.content { + if items.len() > CAP { + return Err(LinkedChunkBuilderError::ChunkTooLarge { id: cur }); + } + } + + // If it's not the first chunk, + if cur != first_id { + // It must have a previous link. + let Some(prev) = chunk.previous else { + return Err(LinkedChunkBuilderError::MultipleFirstChunks { + first_candidate: first_id, + second_candidate: cur, + }); + }; + + // And we must have visited its predecessor at this point, since we've + // iterated from the first chunk. + if !visited.contains(&prev) { + return Err(LinkedChunkBuilderError::MissingChunk { id: prev }); + } + } + + // Add the current chunk to the list of seen chunks. + if !visited.insert(cur) { + // If we didn't insert, then it was already visited: there's a cycle! + return Err(LinkedChunkBuilderError::Cycle { repeated: cur }); + } + + // Move on to the next chunk. If it's none, we'll quit the loop. + maybe_cur = chunk.next; + } + + // If there are more chunks than those we've visited: some of them were not + // linked to the "main" branch of the linked list, so we had multiple connected + // components. + if visited.len() != self.chunks.len() { + return Err(LinkedChunkBuilderError::MultipleConnectedComponents); + } + + Ok(first_id) + } + + pub fn build(mut self) -> Result>, LinkedChunkBuilderError> { + if self.chunks.is_empty() { + return Ok(None); + } + + // Run checks. + let first_id = self.check_consistency()?; + + // We're now going to iterate from the first to the last chunk. As we're doing + // this, we're also doing a few other things: + // + // - rebuilding the final `Chunk`s one by one, that will be linked using + // pointers, + // - counting items from the item chunks we'll encounter, + // - finding the max `ChunkIdentifier` (`max_chunk_id`). + + let mut num_items = 0; + let mut max_chunk_id = first_id.index(); + + // Small helper to graduate a temporary chunk into a final one. As we're doing + // this, we're also updating the maximum chunk id (that will be used to + // set up the id generator), and the number of items in this chunk. + + let mut graduate_chunk = |id: ChunkIdentifier| { + let temp = self.chunks.remove(&id)?; + + // Update the maximum chunk identifier, while we're around. + max_chunk_id = max_chunk_id.max(id.index()); + + // Graduate the current temporary chunk into a final chunk. + let chunk_ptr = match temp.content { + ChunkContent::Gap(gap) => Chunk::new_gap_leaked(id, gap), + + ChunkContent::Items(items) => { + // Update the total count of items. + num_items += items.len(); + Chunk::new_leaked(id, ChunkContent::Items(items)) + } + }; + + Some((temp.next, chunk_ptr)) + }; + + let Some((mut next_chunk_id, first_chunk_ptr)) = graduate_chunk(first_id) else { + // Can't really happen, but oh well. + return Err(LinkedChunkBuilderError::MissingFirstChunk); + }; + + let mut prev_chunk_ptr = first_chunk_ptr; + + while let Some(id) = next_chunk_id { + let Some((new_next, mut chunk_ptr)) = graduate_chunk(id) else { + // Can't really happen, but oh well. + return Err(LinkedChunkBuilderError::MissingChunk { id }); + }; + + let chunk = unsafe { chunk_ptr.as_mut() }; + + // Link the current chunk to its previous one. + let prev_chunk = unsafe { prev_chunk_ptr.as_mut() }; + prev_chunk.next = Some(chunk_ptr); + chunk.previous = Some(prev_chunk_ptr); + + // Prepare for the next iteration. + prev_chunk_ptr = chunk_ptr; + next_chunk_id = new_next; + } + + debug_assert!(self.chunks.is_empty()); + + // Maintain the convention that `Ends::last` may be unset. + let last_chunk_ptr = prev_chunk_ptr; + let last_chunk_ptr = + if first_chunk_ptr == last_chunk_ptr { None } else { Some(last_chunk_ptr) }; + let links = Ends { first: first_chunk_ptr, last: last_chunk_ptr }; + + let chunk_identifier_generator = + ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new( + max_chunk_id, + )); + + let updates = + if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None }; + + Ok(Some(LinkedChunk { + links, + length: num_items, + chunk_identifier_generator, + updates, + marker: PhantomData, + })) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum LinkedChunkBuilderError { + #[error("chunk with id {} is too large", id.index())] + ChunkTooLarge { id: ChunkIdentifier }, + + #[error("there's no first chunk")] + MissingFirstChunk, + + #[error("there are multiple first chunks")] + MultipleFirstChunks { first_candidate: ChunkIdentifier, second_candidate: ChunkIdentifier }, + + #[error("unable to resolve chunk with id {}", id.index())] + MissingChunk { id: ChunkIdentifier }, + + #[error("rebuilt chunks form a cycle: repeated identifier: {}", repeated.index())] + Cycle { repeated: ChunkIdentifier }, + + #[error("multiple connected components")] + MultipleConnectedComponents, +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::LinkedChunkBuilder; + use crate::linked_chunk::{ChunkIdentifier, LinkedChunkBuilderError}; + + #[test] + fn test_empty() { + let lcb = LinkedChunkBuilder::<3, char, char>::new(); + + // Building an empty linked chunk works, and returns `None`. + let lc = lcb.build().unwrap(); + assert!(lc.is_none()); + } + + #[test] + fn test_success() { + let mut lcb = LinkedChunkBuilder::<3, char, char>::new(); + + let cid0 = ChunkIdentifier::new(0); + let cid1 = ChunkIdentifier::new(1); + // Note: cid2 is missing on purpose, to confirm that it's fine to have holes in + // the chunk id space. + let cid3 = ChunkIdentifier::new(3); + + // Check that we can successfully create a linked chunk, independently of the + // order in which chunks are added. + // + // The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order. + + // Adding chunk cid0. + lcb.push_items(None, cid0, Some(cid1), vec!['a', 'b', 'c']); + // Adding chunk cid3. + lcb.push_items(Some(cid1), cid3, None, vec!['d', 'e']); + // Adding chunk cid1. + lcb.push_gap(Some(cid0), cid1, Some(cid3), 'g'); + + let mut lc = + lcb.build().expect("building works").expect("returns a non-empty linked chunk"); + + // Check the entire content first. + assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']); + + // Run checks on the first chunk. + let mut chunks = lc.chunks(); + let first_chunk = chunks.next().unwrap(); + { + assert!(first_chunk.previous().is_none()); + assert_eq!(first_chunk.identifier(), cid0); + } + + // Run checks on the second chunk. + let second_chunk = chunks.next().unwrap(); + { + assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier()); + assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier()); + assert_eq!(second_chunk.identifier(), cid1); + } + + // Run checks on the third chunk. + let third_chunk = chunks.next().unwrap(); + { + assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier()); + assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier()); + assert!(third_chunk.next().is_none()); + assert_eq!(third_chunk.identifier(), cid3); + } + + // There's no more chunk. + assert!(chunks.next().is_none()); + + // The linked chunk had 5 items. + assert_eq!(lc.len(), 5); + + // Now, if we add a new chunk, its identifier should be the previous one we used + // + 1. + lc.push_gap_back('h'); + + let last_chunk = lc.chunks().last().unwrap(); + assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1)); + } + + #[test] + fn test_chunk_too_large() { + let mut lcb = LinkedChunkBuilder::<3, char, char>::new(); + + let cid0 = ChunkIdentifier::new(0); + + // Adding a chunk with 4 items will fail, because the max capacity specified in + // the builder generics is 3. + lcb.push_items(None, cid0, None, vec!['a', 'b', 'c', 'd']); + + let res = lcb.build(); + assert_matches!(res, Err(LinkedChunkBuilderError::ChunkTooLarge { id }) => { + assert_eq!(id, cid0); + }); + } + + #[test] + fn test_missing_first_chunk() { + let mut lcb = LinkedChunkBuilder::<3, char, char>::new(); + + let cid0 = ChunkIdentifier::new(0); + let cid1 = ChunkIdentifier::new(1); + let cid2 = ChunkIdentifier::new(2); + + lcb.push_gap(Some(cid2), cid0, Some(cid1), 'g'); + lcb.push_items(Some(cid0), cid1, Some(cid2), ['a', 'b', 'c']); + lcb.push_items(Some(cid1), cid2, Some(cid0), ['d', 'e', 'f']); + + let res = lcb.build(); + assert_matches!(res, Err(LinkedChunkBuilderError::MissingFirstChunk)); + } + + #[test] + fn test_multiple_first_chunks() { + let mut lcb = LinkedChunkBuilder::<3, char, char>::new(); + + let cid0 = ChunkIdentifier::new(0); + let cid1 = ChunkIdentifier::new(1); + + lcb.push_gap(None, cid0, Some(cid1), 'g'); + // Second chunk lies and pretends to be the first too. + lcb.push_items(None, cid1, Some(cid0), ['a', 'b', 'c']); + + let res = lcb.build(); + assert_matches!(res, Err(LinkedChunkBuilderError::MultipleFirstChunks { first_candidate, second_candidate }) => { + assert_eq!(first_candidate, cid0); + assert_eq!(second_candidate, cid1); + }); + } + + #[test] + fn test_missing_chunk() { + let mut lcb = LinkedChunkBuilder::<3, char, char>::new(); + + let cid0 = ChunkIdentifier::new(0); + let cid1 = ChunkIdentifier::new(1); + lcb.push_gap(None, cid0, Some(cid1), 'g'); + + let res = lcb.build(); + assert_matches!(res, Err(LinkedChunkBuilderError::MissingChunk { id }) => { + assert_eq!(id, cid1); + }); + } + + #[test] + fn test_cycle() { + let mut lcb = LinkedChunkBuilder::<3, char, char>::new(); + + let cid0 = ChunkIdentifier::new(0); + let cid1 = ChunkIdentifier::new(1); + lcb.push_gap(None, cid0, Some(cid1), 'g'); + lcb.push_gap(Some(cid0), cid1, Some(cid0), 'g'); + + let res = lcb.build(); + assert_matches!(res, Err(LinkedChunkBuilderError::Cycle { repeated }) => { + assert_eq!(repeated, cid0); + }); + } + + #[test] + fn test_multiple_connected_components() { + let mut lcb = LinkedChunkBuilder::<3, char, char>::new(); + + let cid0 = ChunkIdentifier::new(0); + let cid1 = ChunkIdentifier::new(1); + let cid2 = ChunkIdentifier::new(2); + + // cid0 and cid1 are linked to each other. + lcb.push_gap(None, cid0, Some(cid1), 'g'); + lcb.push_items(Some(cid0), cid1, None, ['a', 'b', 'c']); + // cid2 stands on its own. + lcb.push_items(None, cid2, None, ['d', 'e', 'f']); + + let res = lcb.build(); + assert_matches!(res, Err(LinkedChunkBuilderError::MultipleConnectedComponents)); + } +} diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index c9147a45e43..edaa4ecfbd9 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -93,6 +93,7 @@ macro_rules! assert_items_eq { } mod as_vector; +mod builder; pub mod relational; mod updates; @@ -105,6 +106,7 @@ use std::{ }; pub use as_vector::*; +pub use builder::*; pub use updates::*; /// Errors of [`LinkedChunk`]. @@ -944,7 +946,7 @@ impl ChunkIdentifierGenerator { /// It is not the position of the chunk, just its unique identifier. /// /// Learn more with [`ChunkIdentifierGenerator`]. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(transparent)] pub struct ChunkIdentifier(u64); @@ -1093,6 +1095,14 @@ impl Chunk { Self { previous: None, next: None, identifier, content } } + /// Create a new chunk given some content, but box it and leak it. + fn new_leaked(identifier: ChunkIdentifier, content: ChunkContent) -> NonNull { + let chunk = Self::new(identifier, content); + let chunk_box = Box::new(chunk); + + NonNull::from(Box::leak(chunk_box)) + } + /// Create a new gap chunk, but box it and leak it. fn new_gap_leaked(identifier: ChunkIdentifier, content: Gap) -> NonNull { let chunk = Self::new_gap(identifier, content); From 21f8b7ed316505e179e0df77dd149f8e40805e26 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 27 Nov 2024 10:46:41 +0100 Subject: [PATCH 609/979] refactor(linked chunk): simplify further impl of the `LinkedChunkRebuilder` --- .../src/linked_chunk/builder.rs | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/builder.rs b/crates/matrix-sdk-common/src/linked_chunk/builder.rs index 04c6b5bdc60..20247d83533 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/builder.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/builder.rs @@ -112,20 +112,14 @@ impl LinkedChunkBuilder { /// Run all error checks before reconstructing the full linked chunk. /// - /// Must be called after checking `self.chunks` isn't empty. + /// Must be called after checking `self.chunks` isn't empty in + /// [`Self::build`]. /// /// Returns the identifier of the first chunk. fn check_consistency(&mut self) -> Result { // Look for the first id. - let mut first_id = None; - - for (id, chunk) in self.chunks.iter() { - if chunk.previous.is_none() { - // This chunk is a good candidate to be the first chunk. - first_id = Some(*id); - break; - } - } + let first_id = + self.chunks.iter().find_map(|(id, chunk)| chunk.previous.is_none().then_some(*id)); // There's no first chunk, but we've checked that `self.chunks` isn't empty: // it's a malformed list. @@ -205,7 +199,6 @@ impl LinkedChunkBuilder { // - counting items from the item chunks we'll encounter, // - finding the max `ChunkIdentifier` (`max_chunk_id`). - let mut num_items = 0; let mut max_chunk_id = first_id.index(); // Small helper to graduate a temporary chunk into a final one. As we're doing @@ -219,15 +212,7 @@ impl LinkedChunkBuilder { max_chunk_id = max_chunk_id.max(id.index()); // Graduate the current temporary chunk into a final chunk. - let chunk_ptr = match temp.content { - ChunkContent::Gap(gap) => Chunk::new_gap_leaked(id, gap), - - ChunkContent::Items(items) => { - // Update the total count of items. - num_items += items.len(); - Chunk::new_leaked(id, ChunkContent::Items(items)) - } - }; + let chunk_ptr = Chunk::new_leaked(id, temp.content); Some((temp.next, chunk_ptr)) }; @@ -273,13 +258,7 @@ impl LinkedChunkBuilder { let updates = if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None }; - Ok(Some(LinkedChunk { - links, - length: num_items, - chunk_identifier_generator, - updates, - marker: PhantomData, - })) + Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData })) } } From 1c554c491247ec6927e271004814675ae56dbc76 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 26 Nov 2024 17:08:17 +0100 Subject: [PATCH 610/979] chore(ui): Clarifies what `TimelineItemPosition::UpdateDecrypted` holds. This patch tries to clear confusion around `TimelineItemPosition::UpdateDecrypted(usize)`: it does contains a timeline item index. This patch changes to `TimelineItemPosition::UpdateDecrypted { timeline_item_index: usize }` --- .../src/timeline/controller/state.rs | 6 ++-- .../src/timeline/event_handler.rs | 28 +++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4204481b9f4..fd543556fa9 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -251,7 +251,7 @@ impl TimelineState { let handle_one_res = txn .handle_remote_event( event.into(), - TimelineItemPosition::UpdateDecrypted(idx), + TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx }, room_data_provider, settings, &mut day_divider_adjuster, @@ -451,7 +451,7 @@ impl TimelineStateTransaction<'_> { TimelineItemPosition::End { origin } | TimelineItemPosition::Start { origin } => origin, - TimelineItemPosition::UpdateDecrypted(idx) => self + TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self .items .get(idx) .and_then(|item| item.as_event()) @@ -707,7 +707,7 @@ impl TimelineStateTransaction<'_> { self.meta.all_events.push_back(event_meta.base_meta()); } - TimelineItemPosition::UpdateDecrypted(_) => { + TimelineItemPosition::UpdateDecrypted { .. } => { if let Some(event) = self.meta.all_events.iter_mut().find(|e| e.event_id == event_meta.event_id) { diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 941823b967a..aad559c0469 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -269,17 +269,26 @@ impl TimelineEventKind { pub(super) enum TimelineItemPosition { /// One or more items are prepended to the timeline (i.e. they're the /// oldest). - Start { origin: RemoteEventOrigin }, + Start { + /// The origin of the new item(s). + origin: RemoteEventOrigin, + }, /// One or more items are appended to the timeline (i.e. they're the most /// recent). - End { origin: RemoteEventOrigin }, + End { + /// The origin of the new item(s). + origin: RemoteEventOrigin, + }, /// A single item is updated, after it's been successfully decrypted. /// /// This happens when an item that was a UTD must be replaced with the /// decrypted event. - UpdateDecrypted(usize), + UpdateDecrypted { + /// The index of the **timeline item**. + timeline_item_index: usize, + }, } /// The outcome of handling a single event with @@ -481,8 +490,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { if !self.result.item_added { trace!("No new item added"); - if let Flow::Remote { position: TimelineItemPosition::UpdateDecrypted(idx), .. } = - self.ctx.flow + if let Flow::Remote { + position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx }, + .. + } = self.ctx.flow { // If add was not called, that means the UTD event is one that // wouldn't normally be visible. Remove it. @@ -576,7 +587,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: PendingEdit, ) { match position { - TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted(_) => { + TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted { .. } => { // Only insert the edit if there wasn't any other edit // before. // @@ -1012,7 +1023,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { | TimelineItemPosition::End { origin } => origin, // For updates, reuse the origin of the encrypted event. - TimelineItemPosition::UpdateDecrypted(idx) => self.items[idx] + TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self + .items[idx] .as_event() .and_then(|ev| Some(ev.as_remote()?.origin)) .unwrap_or_else(|| { @@ -1162,7 +1174,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Remote { event_id: decrypted_event_id, - position: TimelineItemPosition::UpdateDecrypted(idx), + position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx }, .. } => { trace!("Updating timeline item at position {idx}"); From bb598b61a5021b9ba53008cfe5c6eca57eabc02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 10:04:19 +0100 Subject: [PATCH 611/979] chore: Bump the nightly version we use for the CI --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/documentation.yml | 2 +- xtask/src/main.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 9f6f4e6e2a4..371c453cefa 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -17,7 +17,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2024-06-25 + toolchain: nightly-2024-11-26 components: rustfmt - name: Run Benchmarks diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bfd5b50db2..0dd0b90930b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -288,7 +288,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2024-06-25 + toolchain: nightly-2024-11-26 components: rustfmt - name: Cargo fmt @@ -323,7 +323,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2024-06-25 + toolchain: nightly-2024-11-26 components: clippy - name: Load cache diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 4855b00e43d..31e2cb3c6cd 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -36,7 +36,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2024-06-25 + toolchain: nightly-2024-11-26 - name: Install Node.js uses: actions/setup-node@v4 diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1c5a5edfa86..80d740f3f83 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -13,7 +13,7 @@ use release::ReleaseArgs; use swift::SwiftArgs; use xshell::cmd; -const NIGHTLY: &str = "nightly-2024-06-25"; +const NIGHTLY: &str = "nightly-2024-11-26"; type Result> = std::result::Result; From dcf6af405d41b6b249e9a736e1bd086fa832af65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 12:17:23 +0100 Subject: [PATCH 612/979] chore: Silence unexpected cfg warnings These are all coming from macro invocations of macros that are defined in other crates. It's likely a clippy issue. We should try to revert this the next time we bump the nightly version we're using. --- bindings/matrix-sdk-ffi/src/lib.rs | 2 ++ crates/matrix-sdk-base/src/lib.rs | 1 + crates/matrix-sdk-base/src/rooms/mod.rs | 3 ++- crates/matrix-sdk-crypto/src/lib.rs | 1 + crates/matrix-sdk-sqlite/src/lib.rs | 1 + crates/matrix-sdk-ui/src/lib.rs | 2 ++ crates/matrix-sdk-ui/tests/integration/main.rs | 2 ++ crates/matrix-sdk/src/lib.rs | 1 + crates/matrix-sdk/tests/integration/main.rs | 2 +- examples/custom_events/src/main.rs | 2 ++ testing/matrix-sdk-integration-testing/src/lib.rs | 1 + 11 files changed, 16 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index a648947c2b6..f4a937967ec 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -1,6 +1,8 @@ // TODO: target-os conditional would be good. #![allow(unused_qualifications, clippy::new_without_default)] +#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty + // lines after docs. mod authentication; mod chunk_iterator; diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 0c4f394fd7e..aacbc1b20bf 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -16,6 +16,7 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] +#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. #![warn(missing_docs, missing_debug_implementations)] pub use matrix_sdk_common::*; diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index cd884e9c898..9d63e6a8401 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -1,4 +1,5 @@ -#![allow(clippy::assign_op_pattern)] // triggered by bitflags! usage +#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage +#![allow(unexpected_cfgs)] // Triggered by the `EventContent` macro usage mod members; pub(crate) mod normal; diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 0f0b109e9af..0da301178d9 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -17,6 +17,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![warn(missing_docs, missing_debug_implementations)] #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] +#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. pub mod backups; mod ciphers; diff --git a/crates/matrix-sdk-sqlite/src/lib.rs b/crates/matrix-sdk-sqlite/src/lib.rs index ff62c3f7f0c..8da15149c87 100644 --- a/crates/matrix-sdk-sqlite/src/lib.rs +++ b/crates/matrix-sdk-sqlite/src/lib.rs @@ -15,6 +15,7 @@ not(any(feature = "state-store", feature = "crypto-store", feature = "event-cache")), allow(dead_code, unused_imports) )] +#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. #[cfg(feature = "crypto-store")] mod crypto_store; diff --git a/crates/matrix-sdk-ui/src/lib.rs b/crates/matrix-sdk-ui/src/lib.rs index b0f07b9c6b9..cfb06210c4d 100644 --- a/crates/matrix-sdk-ui/src/lib.rs +++ b/crates/matrix-sdk-ui/src/lib.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. + use ruma::html::HtmlSanitizerMode; mod events; diff --git a/crates/matrix-sdk-ui/tests/integration/main.rs b/crates/matrix-sdk-ui/tests/integration/main.rs index da8bc38c3ee..1a9d9ac82ba 100644 --- a/crates/matrix-sdk-ui/tests/integration/main.rs +++ b/crates/matrix-sdk-ui/tests/integration/main.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation. + use itertools::Itertools as _; use matrix_sdk::deserialized_responses::TimelineEvent; use ruma::{events::AnyStateEvent, serde::Raw, EventId, RoomId}; diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 0c7864d3abe..437f750d3bf 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -17,6 +17,7 @@ #![warn(missing_debug_implementations, missing_docs)] #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. pub use async_trait::async_trait; pub use bytes; diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 10205a5604f..7dc0170e622 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -1,6 +1,6 @@ // The http mocking library is not supported for wasm32 #![cfg(not(target_arch = "wasm32"))] - +#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation. use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Client}; use matrix_sdk_test::test_json; use serde::Serialize; diff --git a/examples/custom_events/src/main.rs b/examples/custom_events/src/main.rs index d88dc5ffa43..c2c54a76923 100644 --- a/examples/custom_events/src/main.rs +++ b/examples/custom_events/src/main.rs @@ -1,3 +1,5 @@ +#![allow(unexpected_cfgs)] // Triggered by the `EventContent` macro usage + /// /// This is an example showcasing how to build a very simple bot with custom /// events using the matrix-sdk. To try it, you need a rust build setup, then diff --git a/testing/matrix-sdk-integration-testing/src/lib.rs b/testing/matrix-sdk-integration-testing/src/lib.rs index eeeeb11239b..ade85ecacbf 100644 --- a/testing/matrix-sdk-integration-testing/src/lib.rs +++ b/testing/matrix-sdk-integration-testing/src/lib.rs @@ -1,4 +1,5 @@ #![cfg(test)] +#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation. matrix_sdk_test::init_tracing_for_tests!(); From 79c8d2c3455bb8dbdbd6b70420fdebe7f1529a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 12:17:38 +0100 Subject: [PATCH 613/979] chore: Don't build the docs for xtask Building the docs for xtask spews a bunch of unexpected cfg warnings. As these warnings come from a macro in a dependency and the docs for xtask don't exist nor will, let's just not build them with the rest of the docs. --- .github/workflows/documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 31e2cb3c6cd..73064089853 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -53,7 +53,7 @@ jobs: env: RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings" run: - cargo doc --no-deps --workspace --features docsrs + cargo doc --no-deps --workspace --features docsrs --exclude=xtask - name: Upload artifact if: github.event_name == 'push' && github.ref == 'refs/heads/main' From a1b7906a7d707d2078d383c5f9118050b0044089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 12:26:41 +0100 Subject: [PATCH 614/979] chore: Fix some clippy lints around lifetimes --- crates/matrix-sdk-base/src/debug.rs | 8 ++++---- crates/matrix-sdk-base/src/event_cache/store/mod.rs | 4 ++-- crates/matrix-sdk-base/src/sync.rs | 4 ++-- .../matrix-sdk-crypto/src/file_encryption/attachments.rs | 2 +- crates/matrix-sdk-crypto/src/machine/mod.rs | 4 ++-- crates/matrix-sdk-crypto/src/store/mod.rs | 2 +- .../matrix-sdk-crypto/src/types/cross_signing/common.rs | 2 +- crates/matrix-sdk-crypto/src/verification/event_enums.rs | 4 ++-- crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs | 2 +- crates/matrix-sdk-sqlite/src/utils.rs | 2 +- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 2 +- crates/matrix-sdk-ui/src/timeline/day_dividers.rs | 2 +- crates/matrix-sdk-ui/src/timeline/util.rs | 2 +- crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs | 4 ++-- crates/matrix-sdk/src/authentication/qrcode/login.rs | 2 +- crates/matrix-sdk/src/encryption/backups/futures.rs | 2 +- crates/matrix-sdk/src/encryption/recovery/mod.rs | 2 +- crates/matrix-sdk/src/room/edit.rs | 2 +- crates/matrix-sdk/src/sliding_sync/client.rs | 2 +- crates/matrix-sdk/src/test_utils/mocks.rs | 4 ++-- crates/matrix-sdk/src/widget/mod.rs | 2 +- 21 files changed, 30 insertions(+), 30 deletions(-) diff --git a/crates/matrix-sdk-base/src/debug.rs b/crates/matrix-sdk-base/src/debug.rs index 95035c1c4f7..b7a2f64888f 100644 --- a/crates/matrix-sdk-base/src/debug.rs +++ b/crates/matrix-sdk-base/src/debug.rs @@ -27,7 +27,7 @@ use ruma::{ pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw]); #[cfg(not(tarpaulin_include))] -impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> { +impl fmt::Debug for DebugListOfRawEventsNoId<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut list = f.debug_list(); list.entries(self.0.iter().map(DebugRawEventNoId)); @@ -41,7 +41,7 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> { pub struct DebugInvitedRoom<'a>(pub &'a InvitedRoom); #[cfg(not(tarpaulin_include))] -impl<'a> fmt::Debug for DebugInvitedRoom<'a> { +impl fmt::Debug for DebugInvitedRoom<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("InvitedRoom") .field("invite_state", &DebugListOfRawEvents(&self.0.invite_state.events)) @@ -55,7 +55,7 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> { pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom); #[cfg(not(tarpaulin_include))] -impl<'a> fmt::Debug for DebugKnockedRoom<'a> { +impl fmt::Debug for DebugKnockedRoom<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("KnockedRoom") .field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events)) @@ -66,7 +66,7 @@ impl<'a> fmt::Debug for DebugKnockedRoom<'a> { pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw]); #[cfg(not(tarpaulin_include))] -impl<'a, T> fmt::Debug for DebugListOfRawEvents<'a, T> { +impl fmt::Debug for DebugListOfRawEvents<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut list = f.debug_list(); list.entries(self.0.iter().map(DebugRawEvent)); diff --git a/crates/matrix-sdk-base/src/event_cache/store/mod.rs b/crates/matrix-sdk-base/src/event_cache/store/mod.rs index de84b89a5d8..d08218c5bd0 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/mod.rs @@ -100,13 +100,13 @@ pub struct EventCacheStoreLockGuard<'a> { } #[cfg(not(tarpaulin_include))] -impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> { +impl fmt::Debug for EventCacheStoreLockGuard<'_> { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive() } } -impl<'a> Deref for EventCacheStoreLockGuard<'a> { +impl Deref for EventCacheStoreLockGuard<'_> { type Target = DynEventCacheStore; fn deref(&self) -> &Self::Target { diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index 36ee7421ef7..92c87cf201e 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -248,7 +248,7 @@ impl Timeline { struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap); #[cfg(not(tarpaulin_include))] -impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> { +impl fmt::Debug for DebugInvitedRoomUpdates<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish() } @@ -257,7 +257,7 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> { struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap); #[cfg(not(tarpaulin_include))] -impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> { +impl fmt::Debug for DebugKnockedRoomUpdates<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish() } diff --git a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs index fe41aedffaf..f83216ce43f 100644 --- a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs +++ b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs @@ -56,7 +56,7 @@ impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor } } -impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> { +impl Read for AttachmentDecryptor<'_, R> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read_bytes = self.inner.read(buf)?; diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 660a6d13cf1..9d822275157 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -2311,8 +2311,8 @@ impl OlmMachine { /// incremented and updated it in the database. Otherwise, `false`. /// /// * The (possibly updated) generation counter. - pub async fn maintain_crypto_store_generation<'a>( - &'a self, + pub async fn maintain_crypto_store_generation( + &'_ self, generation: &Mutex>, ) -> StoreResult<(bool, u64)> { let mut gen_guard = generation.lock().await; diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 9b8d06e3139..61427b13895 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -231,7 +231,7 @@ pub(crate) struct SyncedKeyQueryManager<'a> { manager: &'a KeyQueryManager, } -impl<'a> SyncedKeyQueryManager<'a> { +impl SyncedKeyQueryManager<'_> { /// Add entries to the list of users being tracked for device changes /// /// Any users not already on the list are flagged as awaiting a key query. diff --git a/crates/matrix-sdk-crypto/src/types/cross_signing/common.rs b/crates/matrix-sdk-crypto/src/types/cross_signing/common.rs index 55aad7ca2a3..01b5f1887f4 100644 --- a/crates/matrix-sdk-crypto/src/types/cross_signing/common.rs +++ b/crates/matrix-sdk-crypto/src/types/cross_signing/common.rs @@ -138,7 +138,7 @@ pub(crate) enum CrossSigningSubKeys<'a> { UserSigning(&'a UserSigningPubkey), } -impl<'a> CrossSigningSubKeys<'a> { +impl CrossSigningSubKeys<'_> { /// Get the id of the user that owns this cross signing subkey. pub fn user_id(&self) -> &UserId { match self { diff --git a/crates/matrix-sdk-crypto/src/verification/event_enums.rs b/crates/matrix-sdk-crypto/src/verification/event_enums.rs index 103ce912582..caed048943b 100644 --- a/crates/matrix-sdk-crypto/src/verification/event_enums.rs +++ b/crates/matrix-sdk-crypto/src/verification/event_enums.rs @@ -398,7 +398,7 @@ pub enum StartContent<'a> { Room(&'a KeyVerificationStartEventContent), } -impl<'a> StartContent<'a> { +impl StartContent<'_> { #[allow(clippy::wrong_self_convention)] pub fn from_device(&self) -> &DeviceId { match self { @@ -458,7 +458,7 @@ impl<'a> From<&'a ToDeviceKeyVerificationDoneEventContent> for DoneContent<'a> { } } -impl<'a> DoneContent<'a> { +impl DoneContent<'_> { pub fn flow_id(&self) -> &str { match self { Self::ToDevice(c) => c.transaction_id.as_str(), diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index cf45fa2be2f..5f4c05532c6 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -195,7 +195,7 @@ struct PendingStoreChanges<'a> { operations: &'a mut Vec, } -impl<'a> PendingStoreChanges<'a> { +impl PendingStoreChanges<'_> { fn put(&mut self, key: JsValue, value: JsValue) { self.operations.push(PendingOperation::Put { key, value }); } diff --git a/crates/matrix-sdk-sqlite/src/utils.rs b/crates/matrix-sdk-sqlite/src/utils.rs index 1ec9339e7da..04f549f8f3d 100644 --- a/crates/matrix-sdk-sqlite/src/utils.rs +++ b/crates/matrix-sdk-sqlite/src/utils.rs @@ -198,7 +198,7 @@ pub(crate) trait SqliteTransactionExt { Query: Fn(&Transaction<'_>, Vec) -> Result> + Send + 'static; } -impl<'a> SqliteTransactionExt for Transaction<'a> { +impl SqliteTransactionExt for Transaction<'_> { fn chunk_large_query_over( &self, mut keys_to_chunk: Vec, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index fd543556fa9..aa5ea8414a5 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -1117,7 +1117,7 @@ pub(crate) struct FullEventMeta<'a> { pub timestamp: Option, } -impl<'a> FullEventMeta<'a> { +impl FullEventMeta<'_> { fn base_meta(&self) -> EventMeta { EventMeta { event_id: self.event_id.to_owned(), visible: self.visible } } diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index b83d7154220..cbd76e8cce7 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -517,7 +517,7 @@ struct DayDividerInvariantsReport<'a, 'o> { errors: Vec, } -impl<'a, 'o> Display for DayDividerInvariantsReport<'a, 'o> { +impl Display for DayDividerInvariantsReport<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Write all the items of a slice of timeline items. fn write_items( diff --git a/crates/matrix-sdk-ui/src/timeline/util.rs b/crates/matrix-sdk-ui/src/timeline/util.rs index 3dd1302fa43..9ad3be2b851 100644 --- a/crates/matrix-sdk-ui/src/timeline/util.rs +++ b/crates/matrix-sdk-ui/src/timeline/util.rs @@ -31,7 +31,7 @@ pub(super) struct EventTimelineItemWithId<'a> { pub internal_id: &'a TimelineUniqueId, } -impl<'a> EventTimelineItemWithId<'a> { +impl EventTimelineItemWithId<'_> { /// Create a clone of the underlying [`TimelineItem`] with the given kind. pub fn with_inner_kind(&self, kind: impl Into) -> Arc { TimelineItem::new(self.inner.with_kind(kind), self.internal_id.clone()) diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index 14f72aee824..6279a6d7ad7 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -290,11 +290,11 @@ impl UtdHookManager { /// /// Must be called with the lock held on [`UtdHookManager::reported_utds`], /// and takes a `MutexGuard` to enforce that. - async fn report_utd<'a>( + async fn report_utd( info: UnableToDecryptInfo, parent_hook: &Arc, client: &Client, - reported_utds_lock: &mut MutexGuard<'a, GrowableBloom>, + reported_utds_lock: &mut MutexGuard<'_, GrowableBloom>, ) { let event_id = info.event_id.clone(); parent_hook.on_utd(info); diff --git a/crates/matrix-sdk/src/authentication/qrcode/login.rs b/crates/matrix-sdk/src/authentication/qrcode/login.rs index f449fc30642..4422d6b0757 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/login.rs @@ -86,7 +86,7 @@ pub struct LoginWithQrCode<'a> { state: SharedObservable, } -impl<'a> LoginWithQrCode<'a> { +impl LoginWithQrCode<'_> { /// Subscribe to the progress of QR code login. /// /// It's usually necessary to subscribe to this to let the existing device diff --git a/crates/matrix-sdk/src/encryption/backups/futures.rs b/crates/matrix-sdk/src/encryption/backups/futures.rs index c0fd1d3495e..90a4f4859e3 100644 --- a/crates/matrix-sdk/src/encryption/backups/futures.rs +++ b/crates/matrix-sdk/src/encryption/backups/futures.rs @@ -60,7 +60,7 @@ pub struct WaitForSteadyState<'a> { pub(super) timeout: Option, } -impl<'a> WaitForSteadyState<'a> { +impl WaitForSteadyState<'_> { /// Subscribe to the progress of the backup upload step while waiting for it /// to settle down. pub fn subscribe_to_progress( diff --git a/crates/matrix-sdk/src/encryption/recovery/mod.rs b/crates/matrix-sdk/src/encryption/recovery/mod.rs index a8ee851575b..214e6cb42b6 100644 --- a/crates/matrix-sdk/src/encryption/recovery/mod.rs +++ b/crates/matrix-sdk/src/encryption/recovery/mod.rs @@ -341,7 +341,7 @@ impl Recovery { /// # anyhow::Ok(()) }; /// ``` #[instrument(skip_all)] - pub fn recover_and_reset<'a>(&'a self, old_key: &'a str) -> RecoverAndReset<'_> { + pub fn recover_and_reset<'a>(&'a self, old_key: &'a str) -> RecoverAndReset<'a> { RecoverAndReset::new(self, old_key) } diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index fcb3533559f..1f9381a334d 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -128,7 +128,7 @@ trait EventSource { ) -> impl Future> + SendOutsideWasm; } -impl<'a> EventSource for &'a Room { +impl EventSource for &Room { async fn get_event(&self, event_id: &EventId) -> Result { match self.event_cache().await { Ok((event_cache, _drop_handles)) => { diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index eee13306665..d232e698f20 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -253,7 +253,7 @@ impl Client { struct SlidingSyncPreviousEventsProvider<'a>(&'a BTreeMap); -impl<'a> PreviousEventsProvider for SlidingSyncPreviousEventsProvider<'a> { +impl PreviousEventsProvider for SlidingSyncPreviousEventsProvider<'_> { fn for_room( &self, room_id: &ruma::RoomId, diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 53cdbcee5dc..121d57b9956 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -554,7 +554,7 @@ pub struct MatrixMock<'a> { server: &'a MockServer, } -impl<'a> MatrixMock<'a> { +impl MatrixMock<'_> { /// Set an expectation on the number of times this [`MatrixMock`] should /// match in the current test case. /// @@ -850,7 +850,7 @@ pub struct SyncEndpoint { sync_response_builder: Arc>, } -impl<'a> MockEndpoint<'a, SyncEndpoint> { +impl MockEndpoint<'_, SyncEndpoint> { /// Temporarily mocks the sync with the given endpoint and runs a client /// sync with it. /// diff --git a/crates/matrix-sdk/src/widget/mod.rs b/crates/matrix-sdk/src/widget/mod.rs index c80d0c0db75..4f75a006b3a 100644 --- a/crates/matrix-sdk/src/widget/mod.rs +++ b/crates/matrix-sdk/src/widget/mod.rs @@ -305,7 +305,7 @@ impl<'de> Deserialize<'de> for StateKeySelector { { struct StateKeySelectorVisitor; - impl<'de> Visitor<'de> for StateKeySelectorVisitor { + impl Visitor<'_> for StateKeySelectorVisitor { type Value = StateKeySelector; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { From ad615b76121fe4d9ebe6ebe00eb8ea18e2f67eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 12:27:12 +0100 Subject: [PATCH 615/979] chore: Fix some clippy lint warnings around the usage of map_or --- crates/matrix-sdk-base/src/latest_event.rs | 11 ++++++----- crates/matrix-sdk-base/src/read_receipts.rs | 2 +- crates/matrix-sdk-crypto/src/identities/manager.rs | 2 +- crates/matrix-sdk-crypto/src/identities/user.rs | 4 ++-- .../src/store/crypto_store_wrapper.rs | 2 +- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 2 +- crates/matrix-sdk/src/widget/machine/mod.rs | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 4b2f1ff9b13..ccedb842edb 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -74,7 +74,7 @@ pub fn is_suitable_for_latest_event<'a>( // Check if this is a replacement for another message. If it is, ignore it if let Some(original_message) = message.as_original() { let is_replacement = - original_message.content.relates_to.as_ref().map_or(false, |relates_to| { + original_message.content.relates_to.as_ref().is_some_and(|relates_to| { if let Some(relation_type) = relates_to.rel_type() { relation_type == RelationType::Replacement } else { @@ -83,12 +83,13 @@ pub fn is_suitable_for_latest_event<'a>( }); if is_replacement { - return PossibleLatestEvent::NoUnsupportedMessageLikeType; + PossibleLatestEvent::NoUnsupportedMessageLikeType + } else { + PossibleLatestEvent::YesRoomMessage(message) } - return PossibleLatestEvent::YesRoomMessage(message); + } else { + PossibleLatestEvent::YesRoomMessage(message) } - - return PossibleLatestEvent::YesRoomMessage(message); } AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => { diff --git a/crates/matrix-sdk-base/src/read_receipts.rs b/crates/matrix-sdk-base/src/read_receipts.rs index f582bdd5ade..5b640658cb6 100644 --- a/crates/matrix-sdk-base/src/read_receipts.rs +++ b/crates/matrix-sdk-base/src/read_receipts.rs @@ -438,7 +438,7 @@ fn events_intersects<'a>( let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id())); new_events .iter() - .any(|ev| ev.event_id().map_or(false, |event_id| previous_events_ids.contains(&event_id))) + .any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id))) } /// Given a set of events coming from sync, for a room, update the diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index e4d24aeeea3..2a06208cfbe 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -548,7 +548,7 @@ impl IdentityManager { // First time seen, create the identity. The current MSK will be pinned. let identity = OtherUserIdentityData::new(master_key, self_signing)?; let is_verified = maybe_verified_own_identity - .map_or(false, |own_user_identity| own_user_identity.is_identity_signed(&identity)); + .is_some_and(|own_user_identity| own_user_identity.is_identity_signed(&identity)); if is_verified { identity.mark_as_previously_verified(); } diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 62de8fa7f15..fe68b33aee1 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -854,8 +854,8 @@ impl OtherUserIdentityData { // Check if the new master_key is signed by our own **verified** // user_signing_key. If the identity was verified we remember it. - let updated_is_verified = maybe_verified_own_user_signing_key - .map_or(false, |own_user_signing_key| { + let updated_is_verified = + maybe_verified_own_user_signing_key.is_some_and(|own_user_signing_key| { own_user_signing_key.verify_master_key(&master_key).is_ok() }); diff --git a/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs b/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs index a2d1f8fb4e4..609c5fe8262 100644 --- a/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs +++ b/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs @@ -102,7 +102,7 @@ impl CryptoStoreWrapper { .await? .as_ref() .and_then(|i| i.own()) - .map_or(false, |own| own.is_verified()); + .is_some_and(|own| own.is_verified()); let secrets = changes.secrets.to_owned(); let devices = changes.devices.to_owned(); diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index aa5ea8414a5..8ead599f6cc 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -1031,7 +1031,7 @@ impl TimelineMetadata { .skip(*i + 1) // …that's not virtual and not sent by us… .find(|(_, item)| { - item.as_event().map_or(false, |event| event.sender() != self.own_user_id) + item.as_event().is_some_and(|event| event.sender() != self.own_user_id) }) .map(|(i, _)| i); diff --git a/crates/matrix-sdk/src/widget/machine/mod.rs b/crates/matrix-sdk/src/widget/machine/mod.rs index 4649dc3efa0..ca000d37632 100644 --- a/crates/matrix-sdk/src/widget/machine/mod.rs +++ b/crates/matrix-sdk/src/widget/machine/mod.rs @@ -588,13 +588,13 @@ impl WidgetMachine { let update = NotifyCapabilitiesChanged { approved, requested }; let (_request, action) = machine.send_to_widget_request(update); - subscribe_required.then(|| Action::Subscribe).into_iter().chain(action).collect() + subscribe_required.then_some(Action::Subscribe).into_iter().chain(action).collect() }); action.map(|a| vec![a]).unwrap_or_default() }); - unsubscribe_required.then(|| Action::Unsubscribe).into_iter().chain(action).collect() + unsubscribe_required.then_some(Action::Unsubscribe).into_iter().chain(action).collect() } } From 514af54c4cb148c1e4eb0200c15ce7a184dc7d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 12:27:44 +0100 Subject: [PATCH 616/979] chore: Fix some clippy warnings about our docs --- crates/matrix-sdk-crypto/src/gossiping/machine.rs | 1 - crates/matrix-sdk-crypto/src/identities/user.rs | 6 +++++- crates/matrix-sdk-crypto/src/lib.rs | 1 - crates/matrix-sdk-crypto/src/requests.rs | 2 ++ crates/matrix-sdk-crypto/src/store/integration_tests.rs | 1 - crates/matrix-sdk-crypto/src/types/events/olm_v1.rs | 5 +++++ crates/matrix-sdk-indexeddb/src/safe_encode.rs | 4 ++-- 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 9fa0bd9fdaf..913a9a82fff 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -616,7 +616,6 @@ impl GossipMachine { /// i. /// - `Err(x)`: Should *refuse* to share the session. `x` is the reason for /// the refusal. - #[cfg(feature = "automatic-room-key-forwarding")] async fn should_share_key( &self, diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index fe68b33aee1..09111a68328 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -435,16 +435,20 @@ impl OtherUserIdentity { Ok(()) } - // Test helper + /// Test helper that marks that an identity has been previously verified and + /// persist the change in the store. #[cfg(test)] pub async fn mark_as_previously_verified(&self) -> Result<(), CryptoStoreError> { self.inner.mark_as_previously_verified(); + let to_save = UserIdentityData::Other(self.inner.clone()); let changes = Changes { identities: IdentityChanges { changed: vec![to_save], ..Default::default() }, ..Default::default() }; + self.verification_machine.store.inner().save_changes(changes).await?; + Ok(()) } diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 0da301178d9..d483e845b5d 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -1078,7 +1078,6 @@ pub enum RoomEventDecryptionResult { /// # Ok(()) /// # } /// ``` - /// /// TODO /// diff --git a/crates/matrix-sdk-crypto/src/requests.rs b/crates/matrix-sdk-crypto/src/requests.rs index 04dafb914b2..11e3dfc4470 100644 --- a/crates/matrix-sdk-crypto/src/requests.rs +++ b/crates/matrix-sdk-crypto/src/requests.rs @@ -222,6 +222,8 @@ pub enum OutgoingRequests { #[cfg(test)] impl OutgoingRequests { + /// Test helper to destructure the [`OutgoingRequests`] as a + /// [`ToDeviceRequest`]. pub fn to_device(&self) -> Option<&ToDeviceRequest> { as_variant!(self, Self::ToDeviceRequest) } diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 0f8af2d0733..c9292499447 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -28,7 +28,6 @@ /// cryptostore_integration_tests!(); /// } /// ``` - #[allow(unused_macros)] #[macro_export] macro_rules! cryptostore_integration_tests { diff --git a/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs b/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs index dc33fffdae7..c9a04910b3f 100644 --- a/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs +++ b/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs @@ -184,6 +184,11 @@ where impl DecryptedOlmV1Event { #[cfg(test)] + /// Test helper to create a new [`DecryptedOlmV1Event`] with the given + /// content. + /// + /// This should never be done in real code as we need to deserialize + /// decrypted events. pub fn new( sender: &UserId, recipient: &UserId, diff --git a/crates/matrix-sdk-indexeddb/src/safe_encode.rs b/crates/matrix-sdk-indexeddb/src/safe_encode.rs index 2ee19658f7b..4121b5aa430 100644 --- a/crates/matrix-sdk-indexeddb/src/safe_encode.rs +++ b/crates/matrix-sdk-indexeddb/src/safe_encode.rs @@ -1,3 +1,5 @@ +//! Helpers for wasm32/browser environments + #![allow(dead_code)] use base64::{ alphabet, @@ -15,8 +17,6 @@ use ruma::{ use wasm_bindgen::JsValue; use web_sys::IdbKeyRange; -/// Helpers for wasm32/browser environments - /// ASCII Group Separator, for elements in the keys pub const KEY_SEPARATOR: &str = "\u{001D}"; /// ASCII Record Separator is sure smaller than the Key Separator but smaller From 7783188769850866d92a1f5f68fb487e3dad8833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 13:07:22 +0100 Subject: [PATCH 617/979] chore: Box the OidcSession so the AuthSession enum isn't unnecessarily big --- bindings/matrix-sdk-ffi/src/client.rs | 2 +- crates/matrix-sdk/src/authentication/mod.rs | 4 ++-- crates/matrix-sdk/src/client/mod.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 444ee1cd84c..848d5544c16 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1633,7 +1633,7 @@ impl TryFrom for AuthSession { user: user_session, }; - Ok(AuthSession::Oidc(session)) + Ok(AuthSession::Oidc(session.into())) } else { // Create a regular Matrix Session. let session = matrix_sdk::matrix_auth::MatrixSession { diff --git a/crates/matrix-sdk/src/authentication/mod.rs b/crates/matrix-sdk/src/authentication/mod.rs index 81df8af3bed..408de86805e 100644 --- a/crates/matrix-sdk/src/authentication/mod.rs +++ b/crates/matrix-sdk/src/authentication/mod.rs @@ -107,7 +107,7 @@ pub enum AuthSession { /// A session using the OpenID Connect API. #[cfg(feature = "experimental-oidc")] - Oidc(oidc::OidcSession), + Oidc(Box), } impl AuthSession { @@ -157,7 +157,7 @@ impl From for AuthSession { #[cfg(feature = "experimental-oidc")] impl From for AuthSession { fn from(session: oidc::OidcSession) -> Self { - Self::Oidc(session) + Self::Oidc(session.into()) } } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 9c4b948b81b..dca0543e9f7 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -1248,7 +1248,7 @@ impl Client { match session { AuthSession::Matrix(s) => Box::pin(self.matrix_auth().restore_session(s)).await, #[cfg(feature = "experimental-oidc")] - AuthSession::Oidc(s) => Box::pin(self.oidc().restore_session(s)).await, + AuthSession::Oidc(s) => Box::pin(self.oidc().restore_session(*s)).await, } } From 9e20659d5d99b7c0e194e755b753d42df97f9042 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 27 Nov 2024 14:34:45 +0200 Subject: [PATCH 618/979] chore: bring back `MediaSource` JSON serialization methods --- bindings/matrix-sdk-ffi/src/ruma.rs | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 7d1c402215a..89f7cc920cf 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -153,11 +153,6 @@ impl From<&RumaMatrixId> for MatrixId { } } -#[matrix_sdk_ffi_macros::export] -pub fn media_source_from_url(url: String) -> Arc { - Arc::new(MediaSource { media_source: RumaMediaSource::Plain(url.into()) }) -} - #[matrix_sdk_ffi_macros::export] pub fn message_event_content_new( msgtype: MessageType, @@ -206,9 +201,32 @@ pub struct MediaSource { #[matrix_sdk_ffi_macros::export] impl MediaSource { + #[uniffi::constructor] + pub fn from_url(url: String) -> Result, ClientError> { + let media_source = RumaMediaSource::Plain(url.into()); + media_source.verify()?; + + Ok(Arc::new(MediaSource { media_source })) + } + pub fn url(&self) -> String { self.media_source.url() } + + // Used on Element X Android + #[uniffi::constructor] + pub fn from_json(json: String) -> Result, ClientError> { + let media_source: RumaMediaSource = serde_json::from_str(&json)?; + media_source.verify()?; + + Ok(Arc::new(MediaSource { media_source })) + } + + // Used on Element X Android + pub fn to_json(&self) -> String { + serde_json::to_string(&self.media_source) + .expect("Media source should always be serializable ") + } } impl TryFrom for MediaSource { @@ -236,7 +254,7 @@ impl From for RumaMediaSource { } #[extension_trait] -pub impl MediaSourceExt for RumaMediaSource { +pub(crate) impl MediaSourceExt for RumaMediaSource { fn verify(&self) -> Result<(), ClientError> { match self { RumaMediaSource::Plain(url) => { From 185423539ed8fb98ba49f65250fff719db21bfcf Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 27 Nov 2024 13:56:47 +0100 Subject: [PATCH 619/979] test(ui): Fix the `test_echo` test. This patch fixes the `test_echo` test. It was doing the following: * client sends an event to the server, * servers acknowledges with the ID `$wWgymRfo7ri1uQx0NXO40vLJ`, * client syncs and the server returns one event with ID `$7at8sd:localhost`, * the test expects those 2 events to be the same!, which is incorrect. The test was working because the transaction IDs are the same, but that's an abuse of the existing code (the code will change soon, another patch is coming). Whatever the code does: the connection must be based on the event ID, not the transaction ID. --- .../tests/integration/timeline/echo.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs index 69db503f269..1479c3648c3 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs @@ -67,13 +67,12 @@ async fn test_echo() { ); let (_, mut timeline_stream) = timeline.subscribe().await; + let event_id = event_id!("$ev"); + Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) .and(header("authorization", "Bearer 1234")) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({ "event_id": "$wWgymRfo7ri1uQx0NXO40vLJ" })), - ) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id }))) .mount(&server) .await; @@ -87,15 +86,15 @@ async fn test_echo() { assert_let!(Some(VectorDiff::PushBack { value: local_echo }) = timeline_stream.next().await); let item = local_echo.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "Hello, World!"); + assert!(item.event_id().is_none()); let txn_id = item.transaction_id().unwrap(); assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); assert!(day_divider.is_day_divider()); - assert_let!(TimelineItemContent::Message(msg) = item.content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "Hello, World!"); - // Wait for the sending to finish and assert everything was successful send_hdl.await.unwrap().unwrap(); @@ -104,13 +103,14 @@ async fn test_echo() { ); let item = sent_confirmation.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::Sent { .. })); + assert_eq!(item.event_id(), Some(event_id)); let f = EventFactory::new(); sync_builder.add_joined_room( JoinedRoomBuilder::new(room_id).add_timeline_event( f.text_msg("Hello, World!") .sender(user_id!("@example:localhost")) - .event_id(event_id!("$7at8sd:localhost")) + .event_id(event_id) .server_ts(152038280) .unsigned_transaction_id(txn_id), ), From 37f52e1c6ca632350f556f9fe341de0f2ec37b99 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 26 Nov 2024 15:52:41 +0100 Subject: [PATCH 620/979] fix(common): `LinkedChunk` emits an `Update::NewItemsChunk` when constructed. This patch updates `LinkedChunk::new_with_update_history` to emit an `Update::NewItemsChunk` because the first chunk is created and it must emit an update accordingly. --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 63 ++++++++++++++++++- .../src/linked_chunk/updates.rs | 49 ++++++++++++++- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index edaa4ecfbd9..c9e9c4e7744 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -274,14 +274,23 @@ impl LinkedChunk { /// [`ObservableUpdates::take`] method must be called to consume and /// clean the updates. See [`Self::updates`]. pub fn new_with_update_history() -> Self { + let first_chunk_identifier = ChunkIdentifierGenerator::FIRST_IDENTIFIER; + + let mut updates = ObservableUpdates::new(); + updates.push(Update::NewItemsChunk { + previous: None, + new: first_chunk_identifier, + next: None, + }); + Self { links: Ends { // INVARIANT: The first chunk must always be an Items, not a Gap. - first: Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER), + first: Chunk::new_items_leaked(first_chunk_identifier), last: None, }, chunk_identifier_generator: ChunkIdentifierGenerator::new_from_scratch(), - updates: Some(ObservableUpdates::new()), + updates: Some(updates), marker: PhantomData, } } @@ -1458,11 +1467,27 @@ mod tests { assert!(LinkedChunk::<3, char, ()>::new_with_update_history().updates().is_some()); } + #[test] + fn test_new_with_initial_update() { + use super::Update::*; + + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }] + ); + } + #[test] fn test_push_items() { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + linked_chunk.push_items_back(['a']); assert_items_eq!(linked_chunk, ['a']); @@ -1521,6 +1546,10 @@ mod tests { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + linked_chunk.push_items_back(['a']); assert_items_eq!(linked_chunk, ['a']); assert_eq!( @@ -1838,6 +1867,10 @@ mod tests { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f']); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f']); assert_eq!( @@ -2016,6 +2049,10 @@ mod tests { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h', 'i'] ['j', 'k']); assert_eq!(linked_chunk.len(), 11); @@ -2216,6 +2253,10 @@ mod tests { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h']); assert_eq!(linked_chunk.len(), 8); @@ -2320,6 +2361,10 @@ mod tests { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f']); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f']); assert_eq!( @@ -2495,6 +2540,10 @@ mod tests { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + linked_chunk.push_items_back(['a', 'b']); linked_chunk.push_gap_back(()); linked_chunk.push_items_back(['l', 'm']); @@ -2700,6 +2749,16 @@ mod tests { use super::Update::*; let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[NewItemsChunk { + previous: None, + new: ChunkIdentifierGenerator::FIRST_IDENTIFIER, + next: None + }] + ); + linked_chunk.clear(); assert_eq!( diff --git a/crates/matrix-sdk-common/src/linked_chunk/updates.rs b/crates/matrix-sdk-common/src/linked_chunk/updates.rs index a39e3d29906..8792f733921 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/updates.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/updates.rs @@ -384,7 +384,21 @@ mod tests { other_token }; - // There is no new update yet. + // There is an initial update. + { + let updates = linked_chunk.updates().unwrap(); + + assert_eq!( + updates.take(), + &[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }], + ); + assert_eq!( + updates.inner.write().unwrap().take_with_token(other_token), + &[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }], + ); + } + + // No new update. { let updates = linked_chunk.updates().unwrap(); @@ -617,7 +631,16 @@ mod tests { let updates_subscriber = linked_chunk.updates().unwrap().subscribe(); pin_mut!(updates_subscriber); - // No update, stream is pending. + // Initial update, stream is ready. + assert_matches!( + updates_subscriber.as_mut().poll_next(&mut context), + Poll::Ready(Some(items)) => { + assert_eq!( + items, + &[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }] + ); + } + ); assert_matches!(updates_subscriber.as_mut().poll_next(&mut context), Poll::Pending); assert_eq!(*counter_waker.number_of_wakeup.lock().unwrap(), 0); @@ -651,6 +674,7 @@ mod tests { assert_eq!( linked_chunk.updates().unwrap().take(), &[ + NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }, PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] }, PushItems { at: Position(ChunkIdentifier(0), 1), items: vec!['b'] }, PushItems { at: Position(ChunkIdentifier(0), 2), items: vec!['c'] }, @@ -701,9 +725,28 @@ mod tests { let updates_subscriber2 = linked_chunk.updates().unwrap().subscribe(); pin_mut!(updates_subscriber2); - // No update, streams are pending. + // Initial updates, streams are ready. + assert_matches!( + updates_subscriber1.as_mut().poll_next(&mut context1), + Poll::Ready(Some(items)) => { + assert_eq!( + items, + &[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }] + ); + } + ); assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending); assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 0); + + assert_matches!( + updates_subscriber2.as_mut().poll_next(&mut context2), + Poll::Ready(Some(items)) => { + assert_eq!( + items, + &[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }] + ); + } + ); assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending); assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 0); From 7a454888a3cec5ee9cde7ae185089cfa851a3158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 14:54:15 +0100 Subject: [PATCH 621/979] chore: Bump the deps and move some of them to the workspace --- .deny.toml | 1 + Cargo.lock | 612 ++++++++++++------ Cargo.toml | 58 +- crates/matrix-sdk-base/Cargo.toml | 10 +- crates/matrix-sdk-common/Cargo.toml | 12 +- crates/matrix-sdk-crypto/Cargo.toml | 26 +- crates/matrix-sdk-indexeddb/Cargo.toml | 16 +- crates/matrix-sdk-qrcode/Cargo.toml | 6 +- crates/matrix-sdk-sqlite/Cargo.toml | 10 +- crates/matrix-sdk-store-encryption/Cargo.toml | 10 +- crates/matrix-sdk-ui/Cargo.toml | 12 +- crates/matrix-sdk/Cargo.toml | 38 +- 12 files changed, 540 insertions(+), 271 deletions(-) diff --git a/.deny.toml b/.deny.toml index 5b60ee4be2f..ee7d34d1e28 100644 --- a/.deny.toml +++ b/.deny.toml @@ -24,6 +24,7 @@ allow = [ "ISC", "MIT", "MPL-2.0", + "Unicode-3.0", "Zlib", ] exceptions = [ diff --git a/Cargo.lock b/Cargo.lock index a71427865f2..52a3ed545fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "anymap2" @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "async-once-cell" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9338790e78aa95a416786ec8389546c4b6a1dfc3dc36071ed9518a9413a542eb" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" [[package]] name = "async-rx" @@ -341,9 +341,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -370,9 +370,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.7.5" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", @@ -396,7 +396,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -404,9 +404,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -417,7 +417,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -514,21 +514,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" @@ -552,9 +537,9 @@ checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" [[package]] name = "blake3" -version = "1.5.3" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ec96fe9a81b5e365f9db71fe00edc4fe4ca2cc7dcb7861f0603012a7caa210" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" dependencies = [ "arrayref", "arrayvec", @@ -627,9 +612,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "bytesize" @@ -666,7 +651,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -701,9 +686,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.6" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -918,9 +906,9 @@ source = "git+https://github.com/jplatte/const_panic?rev=9024a4cb3eac45c1d2d980f [[package]] name = "constant_time_eq" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "core-foundation" @@ -1219,9 +1207,9 @@ dependencies = [ [[package]] name = "deadpool-sqlite" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9cc6210316f8b7ced394e2a5d2833ce7097fb28afb5881299c61bc18e8e0e9" +checksum = "656f14fc1ab819c65f332045ea7cb38841bbe551f3b2bc7e3abefb559af4155c" dependencies = [ "deadpool 0.12.1", "deadpool-sync", @@ -1329,6 +1317,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.17" @@ -1571,7 +1570,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tower", + "tower 0.4.13", "tracing-subscriber", "url", ] @@ -1701,9 +1700,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fancy_constructor" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +checksum = "07b19d0e43eae2bfbafe4931b5e79c73fb1a849ca15cd41a761a7b8587f9a1a2" dependencies = [ "macroific", "proc-macro2", @@ -1713,9 +1712,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "ff" @@ -1993,9 +1992,9 @@ dependencies = [ [[package]] name = "growable-bloom-filter" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c669fa03050eb3445343f215d62fc1ab831e8098bc9a55f26e9724faff11075c" +checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1" dependencies = [ "serde", "serde_bytes", @@ -2259,7 +2258,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -2287,6 +2286,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2295,19 +2412,30 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -2598,9 +2726,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" [[package]] name = "libm" @@ -2620,9 +2748,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -2635,6 +2763,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -2745,9 +2879,9 @@ dependencies = [ [[package]] name = "mas-http" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4fa3f4f6cece26099dba086413276f440fcad27b8ea204baf46462a730aed7" +checksum = "6f0f43adb7c4c4dc44517b3167d0b25111273c088eaaf79bf326c5bfb6006e52" dependencies = [ "async-trait", "bytes", @@ -2764,8 +2898,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror", - "tower", + "thiserror 1.0.63", + "tower 0.4.13", "tower-http", "tracing", "tracing-opentelemetry", @@ -2773,9 +2907,9 @@ dependencies = [ [[package]] name = "mas-iana" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19952e638c55401f5ce3d9f3a7a8a4213bc64f8d009824bdc6a06d3d8ebb5ba5" +checksum = "0d41af7e8eb3584b648c563a1b97b8f60c7f3dcd7ea0ded525050418d90c5200" dependencies = [ "schemars", "serde", @@ -2783,9 +2917,9 @@ dependencies = [ [[package]] name = "mas-jose" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73e868cddb188020c87cebc64ef426b96af4577eeb752deae72c6bb61d0375b" +checksum = "bb6a1c99221601a1e9ef284efaa6db5985389c839c299d71d5a4c2933ba88eda" dependencies = [ "base64ct", "chrono", @@ -2807,16 +2941,16 @@ dependencies = [ "serde_with", "sha2", "signature", - "thiserror", + "thiserror 1.0.63", "tracing", "url", ] [[package]] name = "mas-oidc-client" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ca6eb2ed615b36f5a988856be91918dbdd46c3160fb0739ca277d03ad690b4" +checksum = "0b4aea512a69c0441c349668da2f5b0bdaeba45163b9616cb9373177de1834ec" dependencies = [ "base64ct", "bytes", @@ -2836,8 +2970,8 @@ dependencies = [ "serde_json", "serde_urlencoded", "serde_with", - "thiserror", - "tower", + "thiserror 1.0.63", + "tower 0.4.13", "tracing", "url", ] @@ -2864,7 +2998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e2551de3bba2cc65b52dc6b268df6114011fe118ac24870fbcf1b35537bd721" dependencies = [ "matrix-pickle-derive", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2938,12 +3072,12 @@ dependencies = [ "similar-asserts", "stream_assert", "tempfile", - "thiserror", + "thiserror 2.0.3", "tokio", "tokio-stream", "tokio-test", "tokio-util", - "tower", + "tower 0.5.1", "tracing", "tracing-subscriber", "uniffi", @@ -2984,7 +3118,7 @@ dependencies = [ "serde_json", "similar-asserts", "stream_assert", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", "unicode-normalization", @@ -3010,7 +3144,7 @@ dependencies = [ "ruma", "serde", "serde_json", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", "tracing-subscriber", @@ -3044,7 +3178,7 @@ dependencies = [ "hmac", "http", "indoc", - "itertools 0.12.1", + "itertools 0.13.0", "js_option", "matrix-sdk-common", "matrix-sdk-qrcode", @@ -3060,7 +3194,7 @@ dependencies = [ "similar-asserts", "stream_assert", "subtle", - "thiserror", + "thiserror 2.0.3", "time", "tokio", "tokio-stream", @@ -3093,7 +3227,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing-subscriber", "uniffi", @@ -3123,7 +3257,7 @@ dependencies = [ "ruma", "serde", "serde_json", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", "tracing-appender", @@ -3171,7 +3305,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "sha2", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", "tracing-subscriber", @@ -3221,7 +3355,7 @@ dependencies = [ "image", "qrcode", "ruma-common", - "thiserror", + "thiserror 2.0.3", "vodozemac", ] @@ -3233,7 +3367,7 @@ dependencies = [ "async-trait", "deadpool-sqlite", "glob", - "itertools 0.12.1", + "itertools 0.13.0", "matrix-sdk-base", "matrix-sdk-crypto", "matrix-sdk-store-encryption", @@ -3246,7 +3380,7 @@ dependencies = [ "serde_json", "similar-asserts", "tempfile", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", "vodozemac", @@ -3268,7 +3402,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror", + "thiserror 2.0.3", "zeroize", ] @@ -3323,7 +3457,7 @@ dependencies = [ "growable-bloom-filter", "imbl", "indexmap 2.6.0", - "itertools 0.12.1", + "itertools 0.13.0", "matrix-sdk", "matrix-sdk-base", "matrix-sdk-test", @@ -3335,7 +3469,7 @@ dependencies = [ "serde_json", "stream_assert", "tempfile", - "thiserror", + "thiserror 2.0.3", "tokio", "tokio-stream", "tracing", @@ -3367,9 +3501,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime2ext" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a85a5069ebd40e64b1985773cc81addbe9d90d7ecf60e7b5475a57ad584c70" +checksum = "515a63dc9666c865e848b043ab52fe9a5c713ae89cde4b5fbaae67cfd614b93a" [[package]] name = "mime_guess" @@ -3587,9 +3721,9 @@ dependencies = [ [[package]] name = "oauth2" -version = "5.0.0-alpha.4" +version = "5.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "098af5a5110b4deacf3200682963713b143ae9d28762b739bdb7b98429dfaf68" +checksum = "23d385da3c602d29036d2f70beed71c36604df7570be17fed4c5b839616785bf" dependencies = [ "base64 0.22.1", "chrono", @@ -3601,15 +3735,15 @@ dependencies = [ "serde_json", "serde_path_to_error", "sha2", - "thiserror", + "thiserror 1.0.63", "url", ] [[package]] name = "oauth2-types" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74573d5c35d172f72bc0f8ca82ed7978c060ce880780e4470e27d2efce10a06f" +checksum = "db9baa46cfc1969e04a7f8c31ee6718a8b4cb52ef31d7d91772f878052d872e1" dependencies = [ "chrono", "data-encoding", @@ -3621,7 +3755,7 @@ dependencies = [ "serde_json", "serde_with", "sha2", - "thiserror", + "thiserror 1.0.63", "url", ] @@ -3636,9 +3770,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" @@ -3654,9 +3788,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openidconnect" -version = "4.0.0-alpha.2" +version = "4.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4c74c00c2727896cebfcd04018dea51902881e711c69f76a446314ab5596e2" +checksum = "a93a50789d0b649986bfb104cdef97736ca072d579ec88496d5c6f9abed0ea85" dependencies = [ "base64 0.21.7", "chrono", @@ -3679,7 +3813,7 @@ dependencies = [ "serde_with", "sha2", "subtle", - "thiserror", + "thiserror 1.0.63", "url", ] @@ -3738,7 +3872,7 @@ dependencies = [ "js-sys", "once_cell", "pin-project-lite", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3761,7 +3895,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3981,9 +4115,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -4088,7 +4222,7 @@ dependencies = [ "smallvec", "symbolic-demangle", "tempfile", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -4144,9 +4278,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -4157,8 +4291,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ - "bit-set", - "bit-vec", "bitflags 2.6.0", "lazy_static", "num-traits", @@ -4166,8 +4298,6 @@ dependencies = [ "rand_chacha", "rand_xorshift", "regex-syntax 0.8.5", - "rusty-fork", - "tempfile", "unarray", ] @@ -4221,12 +4351,6 @@ dependencies = [ "image", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-xml" version = "0.26.0" @@ -4248,7 +4372,7 @@ dependencies = [ "quinn-udp", "rustc-hash 1.1.0", "rustls", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] @@ -4265,7 +4389,7 @@ dependencies = [ "rustc-hash 2.0.0", "rustls", "slab", - "thiserror", + "thiserror 1.0.63", "tinyvec", "tracing", ] @@ -4402,7 +4526,7 @@ checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -4632,7 +4756,7 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror", + "thiserror 1.0.63", "url", "web-time", ] @@ -4661,7 +4785,7 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror", + "thiserror 1.0.63", "time", "tracing", "url", @@ -4689,7 +4813,7 @@ dependencies = [ "ruma-macros", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tracing", "url", "web-time", @@ -4731,7 +4855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e7f9b534a65698d7db3c08d94bf91de0046fe6c7893a7b360502f65e7011ac4" dependencies = [ "js_int", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -4766,9 +4890,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.31.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ "bitflags 2.6.0", "fallible-iterator", @@ -4807,9 +4931,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", @@ -4865,18 +4989,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "rusty-fork" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - [[package]] name = "ryu" version = "1.0.18" @@ -5194,6 +5306,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -5246,9 +5364,9 @@ dependencies = [ [[package]] name = "similar-asserts" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" +checksum = "cfe85670573cd6f0fa97940f26e7e6601213c3b0555246c24234131f88c5709e" dependencies = [ "console", "similar", @@ -5429,9 +5547,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -5450,14 +5568,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" -version = "3.10.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", "windows-sys 0.52.0", ] @@ -5488,7 +5618,16 @@ version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.63", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] @@ -5502,6 +5641,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -5545,6 +5695,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -5572,9 +5732,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -5626,7 +5786,7 @@ checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.63", "tokio", ] @@ -5657,9 +5817,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -5727,6 +5887,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -5754,15 +5930,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -5781,7 +5957,7 @@ version = "0.2.3" source = "git+https://github.com/element-hq/tracing.git?rev=ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd#ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.63", "time", "tracing-subscriber", ] @@ -5911,12 +6087,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -6112,9 +6282,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -6134,6 +6304,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -6142,9 +6324,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", @@ -6183,9 +6365,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vodozemac" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7761890811f1dfe2ebef5e630add0a98597682ebf4e4477d98fb8d2e9172ac" +checksum = "dd4b56780b7827dd72c3c6398c3048752bebf8d1d84ec19b606b15dbc3c850b8" dependencies = [ "aes", "arrayvec", @@ -6206,20 +6388,11 @@ dependencies = [ "serde_json", "sha2", "subtle", - "thiserror", + "thiserror 1.0.63", "x25519-dalek", "zeroize", ] -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -6593,13 +6766,13 @@ dependencies = [ [[package]] name = "wiremock" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a59f8ae78a4737fb724f20106fb35ccb7cfe61ff335665d3042b3aa98e34717" +checksum = "7fff469918e7ca034884c7fd8f93fe27bacb7fcb599fd879df6c7b429a29b646" dependencies = [ "assert-json-diff", "async-trait", - "base64 0.21.7", + "base64 0.22.1", "deadpool 0.10.0", "futures", "http", @@ -6615,6 +6788,18 @@ dependencies = [ "url", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -6661,6 +6846,30 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63658493314859b4dfdf3fb8c1defd61587839def09582db50b8a4e93afca6bb" +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -6681,6 +6890,27 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -6700,3 +6930,25 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 63687ba8f71..56c356e22d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,34 +18,44 @@ default-members = ["benchmarks", "crates/*", "labs/*"] resolver = "2" [workspace.package] -rust-version = "1.76" +rust-version = "1.80" [workspace.dependencies] -anyhow = "1.0.68" +anyhow = "1.0.93" aquamarine = "0.6.0" -assert-json-diff = "2" +assert-json-diff = "2.0.2" assert_matches = "1.5.0" -assert_matches2 = "0.1.1" +assert_matches2 = "0.1.2" async-rx = "0.1.3" -async-stream = "0.3.3" -async-trait = "0.1.60" +async-stream = "0.3.5" +async-trait = "0.1.83" as_variant = "1.2.0" -base64 = "0.22.0" -byteorder = "1.4.3" +base64 = "0.22.1" +byteorder = "1.5.0" +chrono = "0.4.38" eyeball = { version = "0.8.8", features = ["tracing"] } eyeball-im = { version = "0.5.1", features = ["tracing"] } eyeball-im-util = "0.7.0" -futures-core = "0.3.28" +futures-core = "0.3.31" futures-executor = "0.3.21" -futures-util = "0.3.26" -growable-bloom-filter = "2.1.0" +futures-util = "0.3.31" +gloo-timers = "0.3.0" +growable-bloom-filter = "2.1.1" +hkdf = "0.12.4" +hmac = "0.12.1" http = "1.1.0" imbl = "3.0.0" -itertools = "0.12.0" -once_cell = "1.16.0" -pin-project-lite = "0.2.9" +indexmap = "2.6.0" +itertools = "0.13.0" +js-sys = "0.3.69" +mime = "0.3.17" +once_cell = "1.20.2" +pbkdf2 = { version = "0.12.2" } +pin-project-lite = "0.2.15" +proptest = { version = "1.5.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } +rmp-serde = "1.3.0" ruma = { version = "0.11.1", features = [ "client-api-c", "compat-upload-signatures", @@ -65,20 +75,26 @@ serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" sha2 = "0.10.8" -similar-asserts = "1.5.0" +similar-asserts = "1.6.0" stream_assert = "0.1.1" -thiserror = "1.0.38" -tokio = { version = "1.39.1", default-features = false, features = ["sync"] } +tempfile = "3.9.0" +thiserror = "2.0.3" +tokio = { version = "1.41.1", default-features = false, features = ["sync"] } tokio-stream = "0.1.14" tracing = { version = "0.1.40", default-features = false, features = ["std"] } tracing-core = "0.1.32" tracing-subscriber = "0.3.18" +unicode-normalization = "0.1.24" uniffi = { version = "0.28.0" } uniffi_bindgen = { version = "0.28.0" } -url = "2.5.0" -vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] } -wiremock = "0.6.0" -zeroize = "1.6.0" +url = "2.5.4" +uuid = "1.11.0" +vodozemac = { version = "0.8.1", features = ["insecure-pk-encryption"] } +wasm-bindgen = "0.2.84" +wasm-bindgen-test = "0.3.33" +web-sys = "0.3.69" +wiremock = "0.6.2" +zeroize = "1.8.1" matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false } matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" } diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index c312798fb85..656beca6ef8 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -49,8 +49,8 @@ as_variant = { workspace = true } assert_matches = { workspace = true, optional = true } assert_matches2 = { workspace = true, optional = true } async-trait = { workspace = true } -bitflags = { version = "2.4.0", features = ["serde"] } -decancer = "3.2.4" +bitflags = { version = "2.6.0", features = ["serde"] } +decancer = "3.2.8" eyeball = { workspace = true } eyeball-im = { workspace = true } futures-util = { workspace = true } @@ -61,9 +61,9 @@ matrix-sdk-crypto = { workspace = true, optional = true } matrix-sdk-store-encryption = { workspace = true } matrix-sdk-test = { workspace = true, optional = true } once_cell = { workspace = true } -regex = "1.11.0" +regex = "1.11.1" ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] } -unicode-normalization = "0.1.24" +unicode-normalization = { workspace = true } serde = { workspace = true, features = ["rc"] } serde_json = { workspace = true } tokio = { workspace = true } @@ -85,7 +85,7 @@ similar-asserts = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.33" +wasm-bindgen-test = { workspace = true } [lints] workspace = true diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 1d9b0ea5c3f..78837937e11 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -36,16 +36,16 @@ uniffi = { workspace = true, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-util = { workspace = true, features = ["channel"] } wasm-bindgen-futures = { version = "0.4.33", optional = true } -gloo-timers = { version = "0.3.0", features = ["futures"] } -web-sys = { version = "0.3.60", features = ["console"] } +gloo-timers = { workspace = true, features = ["futures"] } +web-sys = { workspace = true, features = ["console"] } tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } -wasm-bindgen = "0.2.84" +wasm-bindgen = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } -proptest = { version = "1.4.0", default-features = false, features = ["std"] } +proptest = { workspace = true } matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" } -wasm-bindgen-test = "0.3.33" +wasm-bindgen-test = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Enable the test macro. @@ -54,7 +54,7 @@ tokio = { workspace = true, features = ["rt", "macros"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] # Enable the JS feature for getrandom. getrandom = { version = "0.2.6", default-features = false, features = ["js"] } -js-sys = "0.3.64" +js-sys = { workspace = true } [lints] workspace = true diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index e97c18ca6c5..060692780fe 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -35,39 +35,39 @@ message-ids = [] testing = ["matrix-sdk-test"] [dependencies] -aes = "0.8.1" +aes = "0.8.4" aquamarine = { workspace = true } as_variant = { workspace = true } async-trait = { workspace = true } -bs58 = { version = "0.5.0" } +bs58 = { version = "0.5.1" } byteorder = { workspace = true } cfg-if = "1.0" -ctr = "0.9.1" +ctr = "0.9.2" eyeball = { workspace = true } futures-core = { workspace = true } futures-util = { workspace = true } -hkdf = "0.12.3" -hmac = "0.12.1" +hkdf = { workspace = true } +hmac = { workspace = true } itertools = { workspace = true } js_option = "0.1.1" matrix-sdk-qrcode = { workspace = true, optional = true } matrix-sdk-common = { workspace = true } matrix-sdk-test = { workspace = true, optional = true } # feature = testing only -pbkdf2 = { version = "0.12.2", default-features = false } +pbkdf2 = { workspace = true } rand = { workspace = true } -rmp-serde = "1.1.1" +rmp-serde = { workspace = true } ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc3814"] } serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true } sha2 = { workspace = true } -subtle = "2.5.0" -time = { version = "0.3.34", features = ["formatting"] } +subtle = "2.6.1" +time = { version = "0.3.36", features = ["formatting"] } tokio-stream = { workspace = true, features = ["sync"] } tokio = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, features = ["attributes"] } url = { workspace = true } -ulid = { version = "1.0.0" } +ulid = { version = "1.1.3" } uniffi = { workspace = true, optional = true } vodozemac = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } @@ -84,10 +84,10 @@ assert_matches = { workspace = true } assert_matches2 = { workspace = true } futures-executor = { workspace = true } http = { workspace = true } -indoc = "2.0.1" +indoc = "2.0.5" matrix-sdk-test = { workspace = true } -proptest = { version = "1.0.0", default-features = false, features = ["std"] } -similar-asserts = "1.5.0" +proptest = { workspace = true } +similar-asserts = { workspace = true } # required for async_test macro stream_assert = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 35610b05f10..46de1f42acf 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -26,20 +26,20 @@ base64 = { workspace = true } gloo-utils = { version = "0.2.0", features = ["serde"] } growable-bloom-filter = { workspace = true, optional = true } indexed_db_futures = "0.5.0" -js-sys = { version = "0.3.58" } +js-sys = { workspace = true } matrix-sdk-base = { workspace = true, features = ["js"], optional = true } matrix-sdk-crypto = { workspace = true, features = ["js"], optional = true } matrix-sdk-store-encryption = { workspace = true } ruma = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -serde-wasm-bindgen = "0.6.1" +serde-wasm-bindgen = "0.6.5" thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -wasm-bindgen = "0.2.83" -web-sys = { version = "0.3.57", features = ["IdbKeyRange"] } -hkdf = "0.12.4" +wasm-bindgen = { workspace = true } +web-sys = { workspace = true, features = ["IdbKeyRange"] } +hkdf = { workspace = true } zeroize = { workspace = true } sha2 = { workspace = true } @@ -56,9 +56,9 @@ matrix-sdk-crypto = { workspace = true, features = ["js", "testing"] } matrix-sdk-test = { workspace = true } rand = { workspace = true } tracing-subscriber = { workspace = true, features = ["registry", "tracing-log"] } -uuid = "1.3.0" -wasm-bindgen-test = "0.3.33" -web-sys = { version = "0.3.57", features = ["IdbKeyRange", "Window", "Performance"] } +uuid = { workspace = true } +wasm-bindgen-test = { workspace = true } +web-sys = { workspace = true, features = ["IdbKeyRange", "Window", "Performance"] } [lints] workspace = true diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index f2f3b1afbd1..42cf72489ca 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -20,14 +20,14 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] byteorder = { workspace = true } -qrcode = { version = "0.14.0", default-features = false } +qrcode = { version = "0.14.1", default-features = false } ruma-common = { workspace = true } thiserror = { workspace = true } vodozemac = { workspace = true } [dev-dependencies] -image = { version = "0.25.1", default-features = false } -qrcode = { version = "0.14.0", default-features = false, features = ["image"] } +image = { version = "0.25.5", default-features = false } +qrcode = { version = "0.14.1", default-features = false, features = ["image"] } [lints] workspace = true diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index 96f342f86a0..f23d45a9433 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -18,14 +18,14 @@ state-store = ["dep:matrix-sdk-base"] [dependencies] async-trait = { workspace = true } -deadpool-sqlite = "0.8.1" +deadpool-sqlite = "0.9.0" itertools = { workspace = true } matrix-sdk-base = { workspace = true, optional = true } matrix-sdk-crypto = { workspace = true, optional = true } matrix-sdk-store-encryption = { workspace = true } -rmp-serde = "1.1.1" +rmp-serde = { workspace = true } ruma = { workspace = true } -rusqlite = { version = "0.31.0", features = ["limits"] } +rusqlite = { version = "0.32.1", features = ["limits"] } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } @@ -35,13 +35,13 @@ vodozemac = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } -glob = "0.3.0" +glob = "0.3.1" matrix-sdk-base = { workspace = true, features = ["testing"] } matrix-sdk-crypto = { workspace = true, features = ["testing"] } matrix-sdk-test = { workspace = true } once_cell = { workspace = true } similar-asserts = { workspace = true } -tempfile = "3.3.0" +tempfile = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [lints] diff --git a/crates/matrix-sdk-store-encryption/Cargo.toml b/crates/matrix-sdk-store-encryption/Cargo.toml index 4328298929e..eda60adb6a3 100644 --- a/crates/matrix-sdk-store-encryption/Cargo.toml +++ b/crates/matrix-sdk-store-encryption/Cargo.toml @@ -15,13 +15,13 @@ js = ["dep:getrandom", "getrandom?/js"] [dependencies] base64 = { workspace = true } -blake3 = "1.5.0" +blake3 = "1.5.5" chacha20poly1305 = { version = "0.10.1", features = ["std"] } -getrandom = { version = "0.2.10", optional = true } -hmac = "0.12.1" -pbkdf2 = "0.12.2" +getrandom = { version = "0.2.15", optional = true } +hmac = { workspace = true } +pbkdf2 = { workspace = true } rand = { workspace = true } -rmp-serde = "1.1.2" +rmp-serde = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index ab8f743cc88..106220a3420 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -21,10 +21,10 @@ unstable-msc3956 = ["ruma/unstable-msc3956"] [dependencies] as_variant = { workspace = true } async_cell = "0.2.2" -async-once-cell = "0.5.2" +async-once-cell = "0.5.4" async-rx = { workspace = true } async-stream = { workspace = true } -chrono = "0.4.23" +chrono = { workspace = true } eyeball = { workspace = true } eyeball-im = { workspace = true } eyeball-im-util = { workspace = true } @@ -33,11 +33,11 @@ futures-util = { workspace = true } fuzzy-matcher = "0.3.7" growable-bloom-filter = { workspace = true } imbl = { workspace = true, features = ["serde"] } -indexmap = "2.0.0" +indexmap = { workspace = true } itertools = { workspace = true } matrix-sdk = { workspace = true, features = ["experimental-sliding-sync", "e2e-encryption"] } matrix-sdk-base = { workspace = true } -mime = "0.3.16" +mime = { workspace = true } once_cell = { workspace = true } pin-project-lite = { workspace = true } ruma = { workspace = true, features = ["html", "unstable-msc3381"] } @@ -47,7 +47,7 @@ thiserror = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true, features = ["sync"] } tracing = { workspace = true, features = ["attributes"] } -unicode-normalization = "0.1.22" +unicode-normalization = { workspace = true } uniffi = { workspace = true, optional = true } [dev-dependencies] @@ -59,7 +59,7 @@ eyeball-im-util = { workspace = true } matrix-sdk = { workspace = true, features = ["testing", "sqlite"] } matrix-sdk-test = { workspace = true } stream_assert = { workspace = true } -tempfile = "3.3.0" +tempfile = { workspace = true } wiremock = { workspace = true } [lints] diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 2bf75fcb7ff..18f7c6f039a 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -69,34 +69,34 @@ anymap2 = "0.13.0" aquamarine = { workspace = true } assert_matches2 = { workspace = true, optional = true } as_variant = { workspace = true } -async-channel = "2.2.1" +async-channel = "2.3.1" async-stream = { workspace = true } async-trait = { workspace = true } -axum = { version = "0.7.4", optional = true } -bytes = "1.1.0" -bytesize = "1.1" -chrono = { version = "0.4.23", optional = true } -event-listener = "5.3.0" +axum = { version = "0.7.9", optional = true } +bytes = "1.8.0" +bytesize = "1.3" +chrono = { workspace = true, optional = true } +event-listener = "5.3.1" eyeball = { workspace = true } eyeball-im = { workspace = true } -eyre = { version = "0.6.8", optional = true } +eyre = { version = "0.6.12", optional = true } futures-core = { workspace = true } futures-util = { workspace = true } growable-bloom-filter = { workspace = true } http = { workspace = true } imbl = { workspace = true, features = ["serde"] } -indexmap = "2.0.2" +indexmap = { workspace = true } js_int = "0.2.2" language-tags = { version = "0.3.2", optional = true } -mas-oidc-client = { version = "0.10.0", default-features = false, optional = true } +mas-oidc-client = { version = "0.11.0", default-features = false, optional = true } matrix-sdk-base = { workspace = true } matrix-sdk-common = { workspace = true } matrix-sdk-ffi-macros = { workspace = true, optional = true } matrix-sdk-indexeddb = { workspace = true, optional = true } matrix-sdk-sqlite = { workspace = true, optional = true } matrix-sdk-test = { workspace = true, optional = true } -mime = "0.3.16" -mime2ext = "0.1.52" +mime = { workspace = true } +mime2ext = "0.1.53" pin-project-lite = { workspace = true } rand = { workspace = true , optional = true } ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965", "unstable-msc3930", "unstable-msc3245-v1-compat", "unstable-msc2867"] } @@ -104,31 +104,31 @@ serde = { workspace = true } serde_html_form = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true, optional = true } -tempfile = "3.3.0" +tempfile = { workspace = true } thiserror = { workspace = true } tokio-stream = { workspace = true, features = ["sync"] } -tower = { version = "0.4.13", features = ["util"], optional = true } +tower = { version = "0.5.1", features = ["util"], optional = true } tracing = { workspace = true, features = ["attributes"] } uniffi = { workspace = true, optional = true } url = { workspace = true, features = ["serde"] } urlencoding = "2.1.3" -uuid = { version = "1.4.1", features = ["serde", "v4"], optional = true } +uuid = { workspace = true, features = ["serde", "v4"], optional = true } vodozemac = { workspace = true } zeroize = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -gloo-timers = { version = "0.3.0", features = ["futures"] } +gloo-timers = { workspace = true, features = ["futures"] } reqwest = { workspace = true } tokio = { workspace = true, features = ["macros"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] backoff = { version = "0.4.0", features = ["tokio"] } -openidconnect = { version = "4.0.0-alpha.2", optional = true } +openidconnect = { version = "4.0.0-rc.1", optional = true } # only activate reqwest's stream feature on non-wasm, the wasm part seems to not # support *sending* streams, which makes it useless for us. reqwest = { workspace = true, features = ["stream"] } tokio = { workspace = true, features = ["fs", "rt", "macros"] } -tokio-util = "0.7.9" +tokio-util = "0.7.12" wiremock = { workspace = true, optional = true } [dev-dependencies] @@ -148,10 +148,10 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } tokio-test = "0.4.4" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.33" +wasm-bindgen-test = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -proptest = "1.4.0" +proptest = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } wiremock = { workspace = true } From a0c86d964528d4cccd35d3add1761397a60c7db1 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Nov 2024 18:02:27 +0100 Subject: [PATCH 622/979] feat(utd_hook): Report historical expected UTD with new reason This PR introduces a new variant to `UtdCause` specifically for device-historical messages (`HistoricalMessage`). These messages cannot be decrypted if key storage is inaccessible. Applications can leverage this new variant to provide more informative error messages to users. --- .../matrix-sdk-crypto/src/types/events/mod.rs | 2 +- .../src/types/events/utd_cause.rs | 292 ++++++++++++++++-- .../src/timeline/controller/state.rs | 2 +- .../src/timeline/event_handler.rs | 43 ++- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 17 +- crates/matrix-sdk-ui/src/timeline/traits.rs | 10 +- crates/matrix-sdk/src/room/mod.rs | 29 +- 7 files changed, 346 insertions(+), 49 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/mod.rs b/crates/matrix-sdk-crypto/src/types/events/mod.rs index 1ebc7239887..8c766554e8f 100644 --- a/crates/matrix-sdk-crypto/src/types/events/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/events/mod.rs @@ -31,7 +31,7 @@ mod utd_cause; use ruma::serde::Raw; pub use to_device::{ToDeviceCustomEvent, ToDeviceEvent, ToDeviceEvents}; -pub use utd_cause::UtdCause; +pub use utd_cause::{CryptoContextInfo, UtdCause}; /// A trait for event contents to define their event type. pub trait EventType { diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index 5a260ad50d9..685dc24d03b 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -15,7 +15,7 @@ use matrix_sdk_common::deserialized_responses::{ UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, }; -use ruma::{events::AnySyncTimelineEvent, serde::Raw}; +use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch}; use serde::Deserialize; /// Our best guess at the reason why an event can't be decrypted. @@ -47,6 +47,16 @@ pub enum UtdCause { /// data was obtained from an insecure source (imported from a file, /// obtained from a legacy (asymmetric) backup, unsafe key forward, etc.) UnknownDevice = 4, + + /// We are missing the keys for this event, but it is a "device-historical" + /// message and no backup is accessible or usable. + /// + /// Device-historical means that the message was sent before the current + /// device existed (but the current user was probably a member of the room + /// at the time the message was sent). Not to + /// be confused with pre-join or pre-invite messages (see + /// [`UtdCause::SentBeforeWeJoined`] for that). + HistoricalMessage = 5, } /// MSC4115 membership info in the unsigned area. @@ -65,10 +75,25 @@ enum Membership { Join, } +/// Contextual crypto information used by [`UtdCause::determine`] to properly +/// identify an Unable-To-Decrypt cause in addition to the +/// [`UnableToDecryptInfo`] and raw event info. +#[derive(Debug, Clone, Copy)] +pub struct CryptoContextInfo { + /// The current device creation timestamp, used as a heuristic to determine + /// if an event is device-historical or not (sent before the current device + /// existed). + pub device_creation_ts: MilliSecondsSinceUnixEpoch, + /// True if key storage is correctly set up and can be used by the current + /// client to download and decrypt message keys. + pub is_backup_configured: bool, +} + impl UtdCause { /// Decide the cause of this UTD, based on the evidence we have. pub fn determine( - raw_event: Option<&Raw>, + raw_event: &Raw, + crypto_context_info: CryptoContextInfo, unable_to_decrypt_info: &UnableToDecryptInfo, ) -> Self { // TODO: in future, use more information to give a richer answer. E.g. @@ -76,16 +101,27 @@ impl UtdCause { UnableToDecryptReason::MissingMegolmSession | UnableToDecryptReason::UnknownMegolmMessageIndex => { // Look in the unsigned area for a `membership` field. - if let Some(raw_event) = raw_event { - if let Ok(Some(unsigned)) = - raw_event.get_field::("unsigned") + if let Some(unsigned) = + raw_event.get_field::("unsigned").ok().flatten() + { + if let Membership::Leave = unsigned.membership { + // We were not a member - this is the cause of the UTD + return UtdCause::SentBeforeWeJoined; + } + } + + if let Ok(timeline_event) = raw_event.deserialize() { + if crypto_context_info.is_backup_configured + && timeline_event.origin_server_ts() + < crypto_context_info.device_creation_ts { - if let Membership::Leave = unsigned.membership { - // We were not a member - this is the cause of the UTD - return UtdCause::SentBeforeWeJoined; - } + // It's a device-historical message and there is no accessible + // backup. The key is missing and it + // is expected. + return UtdCause::HistoricalMessage; } } + UtdCause::Unknown } @@ -111,10 +147,10 @@ mod tests { use matrix_sdk_common::deserialized_responses::{ DeviceLinkProblem, UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, }; - use ruma::{events::AnySyncTimelineEvent, serde::Raw}; + use ruma::{events::AnySyncTimelineEvent, serde::Raw, uint, MilliSecondsSinceUnixEpoch}; use serde_json::{json, value::to_raw_value}; - use crate::types::events::UtdCause; + use crate::types::events::{utd_cause::CryptoContextInfo, UtdCause}; #[test] fn test_a_missing_raw_event_means_we_guess_unknown() { @@ -122,7 +158,8 @@ mod tests { // is unknown. assert_eq!( UtdCause::determine( - None, + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession, @@ -137,7 +174,8 @@ mod tests { // If our JSON contains no membership info, then we guess the UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -153,7 +191,8 @@ mod tests { // we guess the UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": 3 } }))), + &raw_event(json!({ "unsigned": { "membership": 3 } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -169,7 +208,8 @@ mod tests { // UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "invite" } }),)), + &raw_event(json!({ "unsigned": { "membership": "invite" } }),), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -185,7 +225,8 @@ mod tests { // UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "join" } }))), + &raw_event(json!({ "unsigned": { "membership": "join" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -201,7 +242,8 @@ mod tests { // until we have MSC3061. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + &raw_event(json!({ "unsigned": { "membership": "leave" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -218,7 +260,8 @@ mod tests { // even if membership=leave. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + &raw_event(json!({ "unsigned": { "membership": "leave" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MalformedEncryptedEvent @@ -233,9 +276,8 @@ mod tests { // Before MSC4115 is merged, we support the unstable prefix too. assert_eq!( UtdCause::determine( - Some(&raw_event( - json!({ "unsigned": { "io.element.msc4115.membership": "leave" } }) - )), + &raw_event(json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -249,7 +291,8 @@ mod tests { fn test_verification_violation_is_passed_through() { assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -265,7 +308,8 @@ mod tests { fn test_unsigned_device_is_passed_through() { assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -281,7 +325,8 @@ mod tests { fn test_unknown_device_is_passed_through() { assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -293,7 +338,206 @@ mod tests { ); } + #[test] + fn test_historical_expected_reason_depending_on_origin_ts_for_missing_session() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let older_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts - 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &raw_event(json!({})), + older_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::Unknown + ); + + let newer_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::HistoricalMessage + ); + } + + #[test] + fn test_historical_expected_reason_depending_on_origin_ts_for_ratcheted_session() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let older_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts - 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &raw_event(json!({})), + older_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::Unknown + ); + + let newer_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::HistoricalMessage + ); + } + + #[test] + fn test_historical_expected_reason_depending_on_origin_only_for_correct_reason() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let newer_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::HistoricalMessage + ); + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MalformedEncryptedEvent + } + ), + UtdCause::Unknown + ); + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MegolmDecryptionFailure + } + ), + UtdCause::Unknown + ); + } + + #[test] + fn test_historical_expected_only_if_backup_configured() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let crypto_context_info = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: false, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + crypto_context_info, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::Unknown + ); + + assert_eq!( + UtdCause::determine( + &utd_event, + crypto_context_info, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::Unknown + ); + } + + fn a_utd_event_with_origin_ts(origin_server_ts: i32) -> Raw { + raw_event(json!({ + "type": "m.room.encrypted", + "event_id": "$0", + // the values don't matter much but the expected fields should be there. + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "FOO", + "sender_key": "SENDERKEYSENDERKEY", + "device_id": "ABCDEFGH", + "session_id": "A0", + }, + "sender": "@bob:localhost", + "origin_server_ts": origin_server_ts, + "unsigned": { "membership": "join" } + })) + } + fn raw_event(value: serde_json::Value) -> Raw { Raw::from_json(to_raw_value(&value).unwrap()) } + + fn some_crypto_context_info() -> CryptoContextInfo { + CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch(uint!(42)), + is_backup_configured: false, + } + } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 8ead599f6cc..13adcbd2ab5 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -493,7 +493,7 @@ impl TimelineStateTransaction<'_> { event.sender().to_owned(), event.origin_server_ts(), event.transaction_id().map(ToOwned::to_owned), - TimelineEventKind::from_event(event, &room_version, utd_info), + TimelineEventKind::from_event(event, &raw, room_data_provider, utd_info).await, should_add, ) } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index aad559c0469..cc01448714b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -47,7 +47,6 @@ use ruma::{ }, serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, - RoomVersionId, }; use tracing::{debug, error, field::debug, info, instrument, trace, warn}; @@ -71,6 +70,7 @@ use crate::{ controller::PendingEdit, event_item::{ReactionInfo, ReactionStatus}, reactions::PendingReaction, + traits::RoomDataProvider, RepliedToEvent, }, }; @@ -118,6 +118,7 @@ impl Flow { pub(super) struct TimelineEventContext { pub(super) sender: OwnedUserId, pub(super) sender_profile: Option, + /// The event's `origin_server_ts` field (or creation time for local echo). pub(super) timestamp: MilliSecondsSinceUnixEpoch, pub(super) is_own_event: bool, pub(super) read_receipts: IndexMap, @@ -141,10 +142,7 @@ pub(super) enum TimelineEventKind { }, /// An encrypted event that could not be decrypted - UnableToDecrypt { - content: RoomEncryptedEventContent, - unable_to_decrypt_info: UnableToDecryptInfo, - }, + UnableToDecrypt { content: RoomEncryptedEventContent, utd_cause: UtdCause }, /// Some remote event that was redacted a priori, i.e. we never had the /// original content, so we'll just display a dummy redacted timeline @@ -180,15 +178,27 @@ pub(super) enum TimelineEventKind { } impl TimelineEventKind { - /// Creates a new `TimelineEventKind` with the given event and room version. - pub fn from_event( + /// Creates a new `TimelineEventKind`. + /// + /// # Arguments + /// + /// * `event` - The event for which we should create a `TimelineEventKind`. + /// * `raw_event` - The [`Raw`] JSON for `event`. (Required so that we can + /// access `unsigned` data.) + /// * `room_data_provider` - An object which will provide information about + /// the room containing the event. + /// * `unable_to_decrypt_info` - If `event` represents a failure to decrypt, + /// information about that failure. Otherwise, `None`. + pub async fn from_event( event: AnySyncTimelineEvent, - room_version: &RoomVersionId, + raw_event: &Raw, + room_data_provider: &P, unable_to_decrypt_info: Option, ) -> Self { + let room_version = room_data_provider.room_version(); match event { AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(ev)) => { - if let Some(redacts) = ev.redacts(room_version).map(ToOwned::to_owned) { + if let Some(redacts) = ev.redacts(&room_version).map(ToOwned::to_owned) { Self::Redaction { redacts } } else { Self::RedactedMessage { event_type: ev.event_type() } @@ -198,7 +208,12 @@ impl TimelineEventKind { Some(AnyMessageLikeEventContent::RoomEncrypted(content)) => { // An event which is still encrypted. if let Some(unable_to_decrypt_info) = unable_to_decrypt_info { - Self::UnableToDecrypt { content, unable_to_decrypt_info } + let utd_cause = UtdCause::determine( + raw_event, + room_data_provider.crypto_context_info().await, + &unable_to_decrypt_info, + ); + Self::UnableToDecrypt { content, utd_cause } } else { // If we get here, it means that some part of the code has created a // `SyncTimelineEvent` containing an `m.room.encrypted` event @@ -426,17 +441,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } }, - TimelineEventKind::UnableToDecrypt { content, unable_to_decrypt_info } => { + TimelineEventKind::UnableToDecrypt { content, utd_cause } => { // TODO: Handle replacements if the replaced event is also UTD - let raw_event = self.ctx.flow.raw_event(); - let cause = UtdCause::determine(raw_event, &unable_to_decrypt_info); - self.add_item(TimelineItemContent::unable_to_decrypt(content, cause), None); + self.add_item(TimelineItemContent::unable_to_decrypt(content, utd_cause), None); // Let the hook know that we ran into an unable-to-decrypt that is added to the // timeline. if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() { if let Some(event_id) = &self.ctx.flow.event_id() { - hook.on_utd(event_id, cause).await; + hook.on_utd(event_id, utd_cause).await; } } } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 943b641b099..b1d1cc29573 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -17,7 +17,9 @@ use std::{ collections::{BTreeMap, HashMap}, future::ready, + ops::Sub, sync::Arc, + time::{Duration, SystemTime}, }; use eyeball::{SharedObservable, Subscriber}; @@ -33,7 +35,9 @@ use matrix_sdk::{ send_queue::RoomSendQueueUpdate, BoxFuture, }; -use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo, RoomState}; +use matrix_sdk_base::{ + crypto::types::events::CryptoContextInfo, latest_event::LatestEvent, RoomInfo, RoomState, +}; use matrix_sdk_test::{ event_factory::EventFactory, EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID, }; @@ -376,6 +380,17 @@ impl RoomDataProvider for TestRoomDataProvider { RoomVersionId::V10 } + fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo> { + ready(CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch::from_system_time( + SystemTime::now().sub(Duration::from_secs(60 * 3)), + ) + .unwrap_or(MilliSecondsSinceUnixEpoch::now()), + is_backup_configured: false, + }) + .boxed() + } + fn profile_from_user_id<'a>(&'a self, _user_id: &'a UserId) -> BoxFuture<'a, Option> { ready(None).boxed() } diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index 24015c46774..d4ad9bdf288 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -20,8 +20,8 @@ use indexmap::IndexMap; #[cfg(test)] use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement}; use matrix_sdk::{ - deserialized_responses::TimelineEvent, event_cache::paginator::PaginableRoom, BoxFuture, - Result, Room, + crypto::types::events::CryptoContextInfo, deserialized_responses::TimelineEvent, + event_cache::paginator::PaginableRoom, BoxFuture, Result, Room, }; use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo}; use ruma::{ @@ -76,6 +76,8 @@ pub(super) trait RoomDataProvider: fn own_user_id(&self) -> &UserId; fn room_version(&self) -> RoomVersionId; + fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo>; + fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option>; fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option; @@ -121,6 +123,10 @@ impl RoomDataProvider for Room { (**self).clone_info().room_version_or_default() } + fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo> { + async move { self.crypto_context_info().await }.boxed() + } + fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option> { async move { match self.get_member_no_sync(user_id).await { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 285411de6e5..fcafbca63b9 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -48,11 +48,6 @@ use matrix_sdk_base::{ }; use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timeout::timeout}; use mime::Mime; -#[cfg(feature = "e2e-encryption")] -use ruma::events::{ - room::encrypted::OriginalSyncRoomEncryptedEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, - SyncMessageLikeEvent, -}; use ruma::{ api::client::{ config::{set_global_account_data, set_room_account_data}, @@ -113,6 +108,14 @@ use ruma::{ EventId, Int, MatrixToUri, MatrixUri, MxcUri, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId, }; +#[cfg(feature = "e2e-encryption")] +use ruma::{ + events::{ + room::encrypted::OriginalSyncRoomEncryptedEvent, AnySyncMessageLikeEvent, + AnySyncTimelineEvent, SyncMessageLikeEvent, + }, + MilliSecondsSinceUnixEpoch, +}; use serde::de::DeserializeOwned; use thiserror::Error; use tokio::sync::broadcast; @@ -139,6 +142,8 @@ use crate::{ utils::{IntoRawMessageLikeEventContent, IntoRawStateEventContent}, BaseRoom, Client, Error, HttpResult, Result, RoomState, TransmissionProgress, }; +#[cfg(feature = "e2e-encryption")] +use crate::{crypto::types::events::CryptoContextInfo, encryption::backups::BackupState}; pub mod edit; pub mod futures; @@ -610,6 +615,20 @@ impl Room { Ok(self.inner.is_encrypted()) } + /// Gets additional context info about the client crypto. + #[cfg(feature = "e2e-encryption")] + pub async fn crypto_context_info(&self) -> CryptoContextInfo { + let encryption = self.client.encryption(); + CryptoContextInfo { + device_creation_ts: match encryption.get_own_device().await { + Ok(Some(device)) => device.first_time_seen_ts(), + // Should not happen, there will always be an own device + _ => MilliSecondsSinceUnixEpoch::now(), + }, + is_backup_configured: encryption.backups().state() == BackupState::Enabled, + } + } + fn are_events_visible(&self) -> bool { if let RoomState::Invited = self.inner.state() { return matches!( From 6fe5acfc9782c9ddd09f0919423e7eff854fbbf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 17:43:50 +0100 Subject: [PATCH 623/979] refactor(crypto): Move the requests module under the types module --- .../matrix-sdk-crypto/src/gossiping/machine.rs | 18 ++++++++++-------- crates/matrix-sdk-crypto/src/gossiping/mod.rs | 10 ++++++---- .../src/identities/manager.rs | 6 ++++-- crates/matrix-sdk-crypto/src/lib.rs | 9 ++++----- crates/matrix-sdk-crypto/src/machine/mod.rs | 2 +- crates/matrix-sdk-crypto/src/olm/account.rs | 2 +- .../matrix-sdk-crypto/src/olm/signing/mod.rs | 6 ++++-- .../src/session_manager/sessions.rs | 7 +++++-- crates/matrix-sdk-crypto/src/types/mod.rs | 1 + .../src/{requests.rs => types/requests/mod.rs} | 0 .../src/verification/machine.rs | 2 +- .../matrix-sdk-crypto/src/verification/mod.rs | 6 ++++-- .../src/verification/sas/mod.rs | 2 +- 13 files changed, 42 insertions(+), 29 deletions(-) rename crates/matrix-sdk-crypto/src/{requests.rs => types/requests/mod.rs} (100%) diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 913a9a82fff..7193a09c6d8 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -45,16 +45,18 @@ use crate::{ error::{EventError, OlmError, OlmResult}, identities::IdentityManager, olm::{InboundGroupSession, Session}, - requests::{OutgoingRequest, ToDeviceRequest}, session_manager::GroupSessionCache, store::{Changes, CryptoStoreError, SecretImportError, Store, StoreCache}, - types::events::{ - forwarded_room_key::ForwardedRoomKeyContent, - olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent}, - room::encrypted::EncryptedEvent, - room_key_request::RoomKeyRequestEvent, - secret_send::SecretSendContent, - EventType, + types::{ + events::{ + forwarded_room_key::ForwardedRoomKeyContent, + olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent}, + room::encrypted::EncryptedEvent, + room_key_request::RoomKeyRequestEvent, + secret_send::SecretSendContent, + EventType, + }, + requests::{OutgoingRequest, ToDeviceRequest}, }, Device, MegolmError, }; diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index 9287d267aa1..2593091b2eb 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -36,10 +36,12 @@ use ruma::{ use serde::{Deserialize, Serialize}; use crate::{ - requests::{OutgoingRequest, ToDeviceRequest}, - types::events::{ - olm_v1::DecryptedSecretSendEvent, - room_key_request::{RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo}, + types::{ + events::{ + olm_v1::DecryptedSecretSendEvent, + room_key_request::{RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo}, + }, + requests::{OutgoingRequest, ToDeviceRequest}, }, Device, }; diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index 2a06208cfbe..d07a959825f 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -33,12 +33,14 @@ use crate::{ error::OlmResult, identities::{DeviceData, OtherUserIdentityData, OwnUserIdentityData, UserIdentityData}, olm::{InboundGroupSession, PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType}, - requests::KeysQueryRequest, store::{ caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager, Result as StoreResult, Store, StoreCache, StoreCacheGuard, UserKeyQueryResult, }, - types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey}, + types::{ + requests::KeysQueryRequest, CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, + UserSigningPubkey, + }, CryptoStoreError, LocalTrust, OwnUserIdentity, SignatureError, UserIdentity, }; diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index d483e845b5d..5d16721c423 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -28,7 +28,6 @@ mod gossiping; mod identities; mod machine; pub mod olm; -pub mod requests; pub mod secret_storage; mod session_manager; pub mod store; @@ -97,15 +96,15 @@ use matrix_sdk_common::deserialized_responses::{DecryptedRoomEvent, UnableToDecr #[cfg(feature = "qrcode")] pub use matrix_sdk_qrcode; pub use olm::{Account, CrossSigningStatus, EncryptionSettings, Session}; -pub use requests::{ - IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests, - OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest, -}; use serde::{Deserialize, Serialize}; pub use session_manager::CollectStrategy; pub use store::{ CrossSigningKeyExport, CryptoStoreError, SecretImportError, SecretInfo, TrackedUser, }; +pub use types::requests::{ + IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests, + OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest, +}; pub use verification::{ format_emojis, AcceptSettings, AcceptedProtocols, CancelInfo, Emoji, EmojiShortAuthString, Sas, SasState, Verification, VerificationRequest, VerificationRequestState, diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 9d822275157..ad25c92893a 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -70,7 +70,6 @@ use crate::{ KnownSenderData, OlmDecryptionInfo, PrivateCrossSigningIdentity, SenderData, SenderDataFinder, SessionType, StaticAccountData, }, - requests::{IncomingResponse, OutgoingRequest, UploadSigningKeysRequest}, session_manager::{GroupSessionManager, SessionManager}, store::{ Changes, CryptoStoreWrapper, DeviceChanges, IdentityChanges, IntoCryptoStore, MemoryStore, @@ -90,6 +89,7 @@ use crate::{ }, ToDeviceEvents, }, + requests::{IncomingResponse, OutgoingRequest, UploadSigningKeysRequest}, EventEncryptionAlgorithm, Signatures, }, utilities::timestamp_to_iso8601, diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index b10340feeea..efdd1402ab8 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -63,7 +63,6 @@ use crate::{ error::{EventError, OlmResult, SessionCreationError}, identities::DeviceData, olm::SenderData, - requests::UploadSigningKeysRequest, store::{Changes, DeviceChanges, Store}, types::{ events::{ @@ -73,6 +72,7 @@ use crate::{ ToDeviceEncryptedEventContent, }, }, + requests::UploadSigningKeysRequest, CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, OneTimeKey, SignedKey, }, OlmError, SignatureError, diff --git a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs index 293d3a49533..061ada9ff71 100644 --- a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs @@ -32,9 +32,11 @@ use vodozemac::Ed25519Signature; use super::StaticAccountData; use crate::{ error::SignatureError, - requests::UploadSigningKeysRequest, store::SecretImportError, - types::{DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey}, + types::{ + requests::UploadSigningKeysRequest, DeviceKeys, MasterPubkey, SelfSigningPubkey, + UserSigningPubkey, + }, Account, DeviceData, OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData, }; diff --git a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs index 2ae2306c330..e1e3aab1725 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs @@ -34,9 +34,12 @@ use vodozemac::Curve25519PublicKey; use crate::{ error::OlmResult, gossiping::GossipMachine, - requests::{OutgoingRequest, ToDeviceRequest}, store::{Changes, Result as StoreResult, Store}, - types::{events::EventType, EventEncryptionAlgorithm}, + types::{ + events::EventType, + requests::{OutgoingRequest, ToDeviceRequest}, + EventEncryptionAlgorithm, + }, DeviceData, }; diff --git a/crates/matrix-sdk-crypto/src/types/mod.rs b/crates/matrix-sdk-crypto/src/types/mod.rs index c76f0b472bc..bd22d3a78ec 100644 --- a/crates/matrix-sdk-crypto/src/types/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/mod.rs @@ -47,6 +47,7 @@ mod device_keys; pub mod events; mod one_time_keys; pub mod qr_login; +pub mod requests; pub use self::{backup::*, cross_signing::*, device_keys::*, one_time_keys::*}; use crate::store::BackupDecryptionKey; diff --git a/crates/matrix-sdk-crypto/src/requests.rs b/crates/matrix-sdk-crypto/src/types/requests/mod.rs similarity index 100% rename from crates/matrix-sdk-crypto/src/requests.rs rename to crates/matrix-sdk-crypto/src/types/requests/mod.rs diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index 892fa255183..0e62d2f935d 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -38,8 +38,8 @@ use super::{ }; use crate::{ olm::{PrivateCrossSigningIdentity, StaticAccountData}, - requests::OutgoingRequest, store::{CryptoStoreError, CryptoStoreWrapper}, + types::requests::OutgoingRequest, DeviceData, OtherUserIdentityData, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, }; diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 1598114fb06..a42b9eddb71 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -746,9 +746,11 @@ pub(crate) mod tests { use super::{event_enums::OutgoingContent, VerificationStore}; use crate::{ olm::PrivateCrossSigningIdentity, - requests::{OutgoingRequest, OutgoingRequests}, store::{Changes, CryptoStore, CryptoStoreWrapper, IdentityChanges, MemoryStore}, - types::events::ToDeviceEvents, + types::{ + events::ToDeviceEvents, + requests::{OutgoingRequest, OutgoingRequests}, + }, Account, DeviceData, OtherUserIdentityData, OutgoingVerificationRequest, OwnUserIdentityData, }; diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index caf43a249ab..0b2f0dc7a85 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -43,8 +43,8 @@ use super::{ use crate::{ identities::{DeviceData, UserIdentityData}, olm::StaticAccountData, - requests::{OutgoingVerificationRequest, RoomMessageRequest}, store::CryptoStoreError, + types::requests::{OutgoingVerificationRequest, RoomMessageRequest}, Emoji, ToDeviceRequest, }; From 46064680ce83fe420501ab2dabe7b0b778f743b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 18:02:57 +0100 Subject: [PATCH 624/979] refactor!(crypto): Don't re-export the request types from the request module --- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 4 ++-- .../matrix-sdk-crypto-ffi/src/responses.rs | 11 ++++++---- crates/matrix-sdk-base/src/client.rs | 4 ++-- crates/matrix-sdk-crypto/src/backups/mod.rs | 4 ++-- .../src/gossiping/machine.rs | 7 ++++--- .../src/identities/device.rs | 3 ++- .../src/identities/manager.rs | 3 +-- .../matrix-sdk-crypto/src/identities/user.rs | 6 ++++-- crates/matrix-sdk-crypto/src/lib.rs | 4 ---- crates/matrix-sdk-crypto/src/machine/mod.rs | 9 ++++++--- .../src/machine/test_helpers.rs | 5 +++-- .../tests/decryption_verification_state.rs | 3 ++- .../src/machine/tests/mod.rs | 3 ++- .../machine/tests/send_encrypted_to_device.rs | 4 ++-- .../src/olm/group_sessions/outbound.rs | 3 ++- .../src/session_manager/group_sessions/mod.rs | 10 +++++++--- .../group_sessions/share_strategy.rs | 2 +- .../src/store/integration_tests.rs | 3 ++- .../src/verification/cache.rs | 4 +++- .../src/verification/event_enums.rs | 20 ++++++++++--------- .../src/verification/machine.rs | 7 ++++--- .../matrix-sdk-crypto/src/verification/mod.rs | 10 ++++------ .../src/verification/qrcode.rs | 4 ++-- .../src/verification/requests.rs | 8 +++++--- .../src/verification/sas/mod.rs | 4 ++-- .../matrix-sdk/src/encryption/backups/mod.rs | 6 ++++-- crates/matrix-sdk/src/encryption/mod.rs | 17 ++++++++++------ crates/matrix-sdk/src/sliding_sync/mod.rs | 2 +- 28 files changed, 98 insertions(+), 72 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 6403379ac6c..e4513042c54 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -17,8 +17,8 @@ use matrix_sdk_crypto::{ decrypt_room_key_export, encrypt_room_key_export, olm::ExportedRoomKey, store::{BackupDecryptionKey, Changes}, - DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, - UserIdentity as SdkUserIdentity, + types::requests::ToDeviceRequest, + DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity, }; use ruma::{ api::{ diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index 7f3a4501648..e46d02b49b3 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -4,9 +4,12 @@ use std::collections::HashMap; use http::Response; use matrix_sdk_crypto::{ - CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest, - OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest, - UploadSigningKeysRequest as RustUploadSigningKeysRequest, + types::requests::{ + IncomingResponse, KeysBackupRequest, OutgoingRequest, + OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest, + UploadSigningKeysRequest as RustUploadSigningKeysRequest, + }, + CrossSigningBootstrapRequests, }; use ruma::{ api::client::{ @@ -136,7 +139,7 @@ pub enum Request { impl From for Request { fn from(r: OutgoingRequest) -> Self { - use matrix_sdk_crypto::OutgoingRequests::*; + use matrix_sdk_crypto::types::requests::OutgoingRequests::*; match r.request() { KeysUpload(u) => { diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 34ef269366c..05de8660e3c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -26,8 +26,8 @@ use eyeball_im::{Vector, VectorDiff}; use futures_util::Stream; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::{ - store::DynCryptoStore, CollectStrategy, DecryptionSettings, EncryptionSettings, - EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult, ToDeviceRequest, + store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings, + EncryptionSettings, EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult, TrustRequirement, }; #[cfg(feature = "e2e-encryption")] diff --git a/crates/matrix-sdk-crypto/src/backups/mod.rs b/crates/matrix-sdk-crypto/src/backups/mod.rs index fc98dfa03d0..b7433ec37e2 100644 --- a/crates/matrix-sdk-crypto/src/backups/mod.rs +++ b/crates/matrix-sdk-crypto/src/backups/mod.rs @@ -38,8 +38,8 @@ use tracing::{debug, info, instrument, trace, warn}; use crate::{ olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject}, store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store}, - types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures}, - CryptoStoreError, Device, KeysBackupRequest, RoomKeyImportResult, SignatureError, + types::{requests::KeysBackupRequest, MegolmV1AuthData, RoomKeyBackupInfo, Signatures}, + CryptoStoreError, Device, RoomKeyImportResult, SignatureError, }; mod keys; diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 7193a09c6d8..ac28ba58628 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -1117,6 +1117,7 @@ mod tests { use crate::{ gossiping::KeyForwardDecision, olm::OutboundGroupSession, + types::requests::OutgoingRequests, types::{ events::{ forwarded_room_key::ForwardedRoomKeyContent, olm_v1::AnyDecryptedOlmEvent, @@ -1124,7 +1125,7 @@ mod tests { }, EventEncryptionAlgorithm, }, - EncryptionSettings, OutgoingRequests, + EncryptionSettings, }; use crate::{ identities::{DeviceData, IdentityManager, LocalTrust}, @@ -1311,7 +1312,7 @@ mod tests { fn extract_content<'a>( recipient: &UserId, - request: &'a crate::OutgoingRequest, + request: &'a crate::types::requests::OutgoingRequest, ) -> &'a Raw { request .request() @@ -1344,7 +1345,7 @@ mod tests { fn request_to_event( recipient: &UserId, sender: &UserId, - request: &crate::OutgoingRequest, + request: &crate::types::requests::OutgoingRequest, ) -> crate::types::events::ToDeviceEvent where C: crate::types::events::EventType diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index 7052b2edaaf..d518c12ac7a 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -51,10 +51,11 @@ use crate::{ room::encrypted::ToDeviceEncryptedEventContent, room_key_withheld::WithheldCode, EventType, }, + requests::{OutgoingVerificationRequest, ToDeviceRequest}, DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures, SignedKey, }, verification::VerificationMachine, - Account, OutgoingVerificationRequest, Sas, ToDeviceRequest, VerificationRequest, + Account, Sas, VerificationRequest, }; pub enum MaybeEncryptedRoomKey { diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index d07a959825f..8883b80ab74 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -1230,9 +1230,8 @@ pub(crate) mod testing { identities::IdentityManager, olm::{Account, PrivateCrossSigningIdentity}, store::{CryptoStoreWrapper, MemoryStore, PendingChanges, Store}, - types::DeviceKeys, + types::{requests::UploadSigningKeysRequest, DeviceKeys}, verification::VerificationMachine, - UploadSigningKeysRequest, }; pub fn user_id() -> &'static UserId { diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 09111a68328..45436c29128 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -36,9 +36,11 @@ use tracing::error; use crate::{ error::SignatureError, store::{Changes, IdentityChanges, Store}, - types::{MasterPubkey, SelfSigningPubkey, UserSigningPubkey}, + types::{ + requests::OutgoingVerificationRequest, MasterPubkey, SelfSigningPubkey, UserSigningPubkey, + }, verification::VerificationMachine, - CryptoStoreError, DeviceData, OutgoingVerificationRequest, VerificationRequest, + CryptoStoreError, DeviceData, VerificationRequest, }; /// Enum over the different user identity types we can have. diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 5d16721c423..c8cd61fc52b 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -101,10 +101,6 @@ pub use session_manager::CollectStrategy; pub use store::{ CrossSigningKeyExport, CryptoStoreError, SecretImportError, SecretInfo, TrackedUser, }; -pub use types::requests::{ - IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests, - OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest, -}; pub use verification::{ format_emojis, AcceptSettings, AcceptedProtocols, CancelInfo, Emoji, EmojiShortAuthString, Sas, SasState, Verification, VerificationRequest, VerificationRequestState, diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index ad25c92893a..c19b2c29a69 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -89,13 +89,16 @@ use crate::{ }, ToDeviceEvents, }, - requests::{IncomingResponse, OutgoingRequest, UploadSigningKeysRequest}, + requests::{ + IncomingResponse, KeysQueryRequest, OutgoingRequest, ToDeviceRequest, + UploadSigningKeysRequest, + }, EventEncryptionAlgorithm, Signatures, }, utilities::timestamp_to_iso8601, verification::{Verification, VerificationMachine, VerificationRequest}, - CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, KeysQueryRequest, - LocalTrust, RoomEventDecryptionResult, SignatureError, ToDeviceRequest, TrustRequirement, + CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, LocalTrust, + RoomEventDecryptionResult, SignatureError, TrustRequirement, }; /// State machine implementation of the Olm/Megolm encryption protocol used for diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 684b509436a..0c3d4a44e20 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -34,8 +34,9 @@ use ruma::{ use serde_json::json; use crate::{ - store::Changes, types::events::ToDeviceEvent, CrossSigningBootstrapRequests, DeviceData, - OlmMachine, OutgoingRequests, + store::Changes, + types::{events::ToDeviceEvent, requests::OutgoingRequests}, + CrossSigningBootstrapRequests, DeviceData, OlmMachine, }; /// These keys need to be periodically uploaded to the server. diff --git a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs index 027d38b63a7..6194efc82c0 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs @@ -42,11 +42,12 @@ use crate::{ room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme}, ToDeviceEvent, }, + requests::OutgoingRequests, CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, SelfSigningPubkey, }, utilities::json_convert, CryptoStoreError, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, OlmMachine, - OtherUserIdentityData, OutgoingRequests, TrustRequirement, UserIdentity, + OtherUserIdentityData, TrustRequirement, UserIdentity, }; #[async_test] diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 0de4cbcc8ad..3a608a8aca3 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -66,12 +66,13 @@ use crate::{ }, ToDeviceEvent, }, + requests::{OutgoingRequests, ToDeviceRequest}, DeviceKeys, SignedKey, SigningKeys, }, utilities::json_convert, verification::tests::bob_id, Account, DecryptionSettings, DeviceData, EncryptionSettings, MegolmError, OlmError, - OutgoingRequests, RoomEventDecryptionResult, ToDeviceRequest, TrustRequirement, + RoomEventDecryptionResult, TrustRequirement, }; mod decryption_verification_state; diff --git a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs index 83745ff8edb..41faf70c991 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs @@ -22,9 +22,9 @@ use crate::{ test_helpers::{get_machine_pair, get_machine_pair_with_session}, tests, }, - types::events::ToDeviceEvent, + types::{events::ToDeviceEvent, requests::ToDeviceRequest}, utilities::json_convert, - EncryptionSyncChanges, OlmError, ToDeviceRequest, + EncryptionSyncChanges, OlmError, }; #[async_test] diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index 7c91adb5a47..e7a51644704 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -56,9 +56,10 @@ use crate::{ room_key::{MegolmV1AesSha2Content as MegolmV1AesSha2RoomKeyContent, RoomKeyContent}, room_key_withheld::{RoomKeyWithheldContent, WithheldCode}, }, + requests::ToDeviceRequest, EventEncryptionAlgorithm, }, - DeviceData, ToDeviceRequest, + DeviceData, }; const ONE_HOUR: Duration = Duration::from_secs(60 * 60); diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs index 1fc60ffd9c9..72c44ca7365 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs @@ -41,8 +41,11 @@ use crate::{ ShareInfo, ShareState, }, store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store}, - types::events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode}, - Device, DeviceData, EncryptionSettings, OlmError, ToDeviceRequest, + types::{ + events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode}, + requests::ToDeviceRequest, + }, + Device, DeviceData, EncryptionSettings, OlmError, }; #[derive(Clone, Debug)] @@ -806,9 +809,10 @@ mod tests { WithheldCode, }, }, + requests::ToDeviceRequest, DeviceKeys, EventEncryptionAlgorithm, }, - EncryptionSettings, LocalTrust, OlmMachine, ToDeviceRequest, + EncryptionSettings, LocalTrust, OlmMachine, }; fn alice_id() -> &'static UserId { diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 4797fbe60c1..20ab2dfb8e8 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -1368,7 +1368,7 @@ mod tests { machine .mark_request_as_sent( &TransactionId::new(), - crate::IncomingResponse::KeysQuery(&kq_response), + crate::types::requests::IncomingResponse::KeysQuery(&kq_response), ) .await .unwrap(); diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index c9292499447..f454a3e9567 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -66,10 +66,11 @@ macro_rules! cryptostore_integration_tests { secret_send::SecretSendContent, ToDeviceEvent, }, + requests::ToDeviceRequest, DeviceKeys, EventEncryptionAlgorithm, }, - GossippedSecret, LocalTrust, DeviceData, SecretInfo, ToDeviceRequest, TrackedUser, + GossippedSecret, LocalTrust, DeviceData, SecretInfo, TrackedUser, vodozemac::{ megolm::{GroupSession, SessionConfig}, }, diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index 8f770ba9cb2..f5e23344a5d 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -24,9 +24,11 @@ use tracing::debug; use tracing::{trace, warn}; use super::{event_enums::OutgoingContent, FlowId, Sas, Verification}; +use crate::types::requests::{ + OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, +}; #[cfg(feature = "qrcode")] use crate::QrVerification; -use crate::{OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest}; #[derive(Clone, Debug, Default)] pub struct VerificationCache { diff --git a/crates/matrix-sdk-crypto/src/verification/event_enums.rs b/crates/matrix-sdk-crypto/src/verification/event_enums.rs index caed048943b..45fdd5043bd 100644 --- a/crates/matrix-sdk-crypto/src/verification/event_enums.rs +++ b/crates/matrix-sdk-crypto/src/verification/event_enums.rs @@ -679,9 +679,9 @@ impl From<(OwnedRoomId, AnyMessageLikeEventContent)> for OutgoingContent { } } -use crate::{ - types::events::ToDeviceEvents, OutgoingRequest, OutgoingVerificationRequest, - RoomMessageRequest, ToDeviceRequest, +use crate::types::{ + events::ToDeviceEvents, + requests::{OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest}, }; impl TryFrom for OutgoingContent { @@ -765,13 +765,15 @@ impl TryFrom for OutgoingContent { type Error = String; fn try_from(value: OutgoingRequest) -> Result { + use crate::types::requests::OutgoingRequests; + match value.request() { - crate::OutgoingRequests::KeysUpload(_) - | crate::OutgoingRequests::KeysQuery(_) - | crate::OutgoingRequests::SignatureUpload(_) - | crate::OutgoingRequests::KeysClaim(_) => Err("Invalid request type".to_owned()), - crate::OutgoingRequests::ToDeviceRequest(r) => Self::try_from(r.clone()), - crate::OutgoingRequests::RoomMessage(r) => Ok(Self::from(r.clone())), + OutgoingRequests::KeysUpload(_) + | OutgoingRequests::KeysQuery(_) + | OutgoingRequests::SignatureUpload(_) + | OutgoingRequests::KeysClaim(_) => Err("Invalid request type".to_owned()), + OutgoingRequests::ToDeviceRequest(r) => Self::try_from(r.clone()), + OutgoingRequests::RoomMessage(r) => Ok(Self::from(r.clone())), } } } diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index 0e62d2f935d..5e7c0c15cf0 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -39,9 +39,10 @@ use super::{ use crate::{ olm::{PrivateCrossSigningIdentity, StaticAccountData}, store::{CryptoStoreError, CryptoStoreWrapper}, - types::requests::OutgoingRequest, - DeviceData, OtherUserIdentityData, OutgoingVerificationRequest, RoomMessageRequest, - ToDeviceRequest, + types::requests::{ + OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, + }, + DeviceData, OtherUserIdentityData, }; #[derive(Clone, Debug)] diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index a42b9eddb71..07da3685dc4 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -54,9 +54,8 @@ use crate::{ gossiping::{GossipMachine, GossipRequest}, olm::{PrivateCrossSigningIdentity, StaticAccountData}, store::{Changes, CryptoStoreWrapper}, - types::Signatures, - CryptoStoreError, DeviceData, LocalTrust, OutgoingVerificationRequest, OwnUserIdentityData, - UserIdentityData, + types::{requests::OutgoingVerificationRequest, Signatures}, + CryptoStoreError, DeviceData, LocalTrust, OwnUserIdentityData, UserIdentityData, }; #[derive(Clone, Debug)] @@ -749,10 +748,9 @@ pub(crate) mod tests { store::{Changes, CryptoStore, CryptoStoreWrapper, IdentityChanges, MemoryStore}, types::{ events::ToDeviceEvents, - requests::{OutgoingRequest, OutgoingRequests}, + requests::{OutgoingRequest, OutgoingRequests, OutgoingVerificationRequest}, }, - Account, DeviceData, OtherUserIdentityData, OutgoingVerificationRequest, - OwnUserIdentityData, + Account, DeviceData, OtherUserIdentityData, OwnUserIdentityData, }; pub(crate) fn request_to_event( diff --git a/crates/matrix-sdk-crypto/src/verification/qrcode.rs b/crates/matrix-sdk-crypto/src/verification/qrcode.rs index a5411b6e3e7..6907fe27b98 100644 --- a/crates/matrix-sdk-crypto/src/verification/qrcode.rs +++ b/crates/matrix-sdk-crypto/src/verification/qrcode.rs @@ -51,8 +51,8 @@ use super::{ VerificationStore, }; use crate::{ - CryptoStoreError, DeviceData, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, - UserIdentityData, + types::requests::{OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest}, + CryptoStoreError, DeviceData, UserIdentityData, }; const SECRET_SIZE: usize = 16; diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 3815ff49e5c..4c25a969807 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -52,8 +52,9 @@ use super::{ CancelInfo, Cancelled, FlowId, Verification, VerificationStore, }; use crate::{ - olm::StaticAccountData, CryptoStoreError, DeviceData, OutgoingVerificationRequest, - RoomMessageRequest, Sas, ToDeviceRequest, + olm::StaticAccountData, + types::requests::{OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest}, + CryptoStoreError, DeviceData, Sas, }; const SUPPORTED_METHODS: &[VerificationMethod] = &[ @@ -1634,6 +1635,7 @@ mod tests { use super::VerificationRequest; use crate::{ + types::requests::OutgoingVerificationRequest, verification::{ cache::VerificationCache, event_enums::{ @@ -1642,7 +1644,7 @@ mod tests { tests::{alice_id, bob_id, setup_stores}, FlowId, Verification, VerificationStore, }, - DeviceData, OutgoingVerificationRequest, VerificationRequestState, + DeviceData, VerificationRequestState, }; #[async_test] diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index 0b2f0dc7a85..ec8251c6d54 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -44,8 +44,8 @@ use crate::{ identities::{DeviceData, UserIdentityData}, olm::StaticAccountData, store::CryptoStoreError, - types::requests::{OutgoingVerificationRequest, RoomMessageRequest}, - Emoji, ToDeviceRequest, + types::requests::{OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest}, + Emoji, }; /// Short authentication string object. diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index 59457221d10..437c6d8746c 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -25,8 +25,10 @@ use std::collections::{BTreeMap, BTreeSet}; use futures_core::Stream; use futures_util::StreamExt; use matrix_sdk_base::crypto::{ - backups::MegolmV1BackupKey, store::BackupDecryptionKey, types::RoomKeyBackupInfo, - KeysBackupRequest, OlmMachine, RoomKeyImportResult, + backups::MegolmV1BackupKey, + store::BackupDecryptionKey, + types::{requests::KeysBackupRequest, RoomKeyBackupInfo}, + OlmMachine, RoomKeyImportResult, }; use ruma::{ api::client::{ diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 4d8d313aac8..aaf88c7d7cc 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -31,7 +31,10 @@ use futures_util::{ stream::{self, StreamExt}, }; use matrix_sdk_base::crypto::{ - CrossSigningBootstrapRequests, OlmMachine, OutgoingRequest, RoomMessageRequest, ToDeviceRequest, + types::requests::{ + OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, + }, + CrossSigningBootstrapRequests, OlmMachine, }; use matrix_sdk_common::executor::spawn; use ruma::{ @@ -377,7 +380,7 @@ impl Client { pub(crate) async fn mark_request_as_sent( &self, request_id: &TransactionId, - response: impl Into>, + response: impl Into>, ) -> Result<(), matrix_sdk_base::Error> { Ok(self .olm_machine() @@ -579,13 +582,15 @@ impl Client { pub(crate) async fn send_verification_request( &self, - request: matrix_sdk_base::crypto::OutgoingVerificationRequest, + request: OutgoingVerificationRequest, ) -> Result<()> { + use matrix_sdk_base::crypto::types::requests::OutgoingVerificationRequest::*; + match request { - matrix_sdk_base::crypto::OutgoingVerificationRequest::ToDevice(t) => { + ToDevice(t) => { self.send_to_device(&t).await?; } - matrix_sdk_base::crypto::OutgoingVerificationRequest::InRoom(r) => { + InRoom(r) => { self.room_send_helper(&r).await?; } } @@ -608,7 +613,7 @@ impl Client { } async fn send_outgoing_request(&self, r: OutgoingRequest) -> Result<()> { - use matrix_sdk_base::crypto::OutgoingRequests; + use matrix_sdk_base::crypto::types::requests::OutgoingRequests; match r.request() { OutgoingRequests::KeysQuery(request) => { diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 36f4f896a1e..bfbbb69cb17 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1693,7 +1693,7 @@ mod tests { #[async_test] #[cfg(feature = "e2e-encryption")] async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> { - use matrix_sdk_base::crypto::{IncomingResponse, OutgoingRequests}; + use matrix_sdk_base::crypto::types::requests::{IncomingResponse, OutgoingRequests}; use matrix_sdk_test::ruma_response_from_json; use ruma::user_id; From a94a5f1716e8987d5b71f6369f1e2a09b13b2623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 18:32:52 +0100 Subject: [PATCH 625/979] chore(crypto): Split out the requests module --- crates/matrix-sdk-crypto/src/lib.rs | 8 +- .../src/types/requests/enums.rs | 201 +++++++++ .../src/types/requests/keys_backup.rs | 27 ++ .../src/types/requests/keys_query.rs | 38 ++ .../src/types/requests/mod.rs | 421 +----------------- .../src/types/requests/room_message.rs | 32 ++ .../src/types/requests/signing_keys.rs | 32 ++ .../src/types/requests/to_device.rs | 136 ++++++ .../src/types/requests/verification.rs | 48 ++ 9 files changed, 537 insertions(+), 406 deletions(-) create mode 100644 crates/matrix-sdk-crypto/src/types/requests/enums.rs create mode 100644 crates/matrix-sdk-crypto/src/types/requests/keys_backup.rs create mode 100644 crates/matrix-sdk-crypto/src/types/requests/keys_query.rs create mode 100644 crates/matrix-sdk-crypto/src/types/requests/room_message.rs create mode 100644 crates/matrix-sdk-crypto/src/types/requests/signing_keys.rs create mode 100644 crates/matrix-sdk-crypto/src/types/requests/to_device.rs create mode 100644 crates/matrix-sdk-crypto/src/types/requests/verification.rs diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index c8cd61fc52b..157f0a04b03 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -449,7 +449,7 @@ pub enum RoomEventDecryptionResult { /// # use std::collections::BTreeMap; /// # use ruma::api::client::keys::upload_keys::v3::Response; /// # use anyhow::Result; -/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest}; +/// # use matrix_sdk_crypto::{OlmMachine, types::requests::OutgoingRequest}; /// # async fn send_request(request: OutgoingRequest) -> Result { /// # let response = unimplemented!(); /// # Ok(response) @@ -494,7 +494,7 @@ pub enum RoomEventDecryptionResult { /// # use std::collections::BTreeMap; /// # use ruma::api::client::keys::upload_keys::v3::Response; /// # use anyhow::Result; -/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest}; +/// # use matrix_sdk_crypto::{OlmMachine, types::requests::OutgoingRequest}; /// # async fn send_request(request: &OutgoingRequest) -> Result { /// # let response = unimplemented!(); /// # Ok(response) @@ -921,7 +921,7 @@ pub enum RoomEventDecryptionResult { /// # use anyhow::Result; /// # use ruma::UserId; /// # use ruma::api::client::keys::claim_keys::v3::{Response, Request}; -/// # use matrix_sdk_crypto::{OlmMachine, requests::ToDeviceRequest, EncryptionSettings}; +/// # use matrix_sdk_crypto::{OlmMachine, types::requests::ToDeviceRequest, EncryptionSettings}; /// # async fn send_request(request: &ToDeviceRequest) -> Result { /// # let response = unimplemented!(); /// # Ok(response) @@ -987,7 +987,7 @@ pub enum RoomEventDecryptionResult { /// # use serde_json::json; /// # use ruma::{UserId, RoomId, serde::Raw}; /// # use ruma::api::client::keys::claim_keys::v3::{Response, Request}; -/// # use matrix_sdk_crypto::{EncryptionSettings, OlmMachine, ToDeviceRequest}; +/// # use matrix_sdk_crypto::{EncryptionSettings, OlmMachine, types::requests::ToDeviceRequest}; /// # use tokio::sync::MutexGuard; /// # async fn send_request(request: &Request) -> Result { /// # let response = unimplemented!(); diff --git a/crates/matrix-sdk-crypto/src/types/requests/enums.rs b/crates/matrix-sdk-crypto/src/types/requests/enums.rs new file mode 100644 index 00000000000..98e2523fbbf --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/requests/enums.rs @@ -0,0 +1,201 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use ruma::{ + api::client::{ + backup::add_backup_keys::v3::Response as KeysBackupResponse, + keys::{ + claim_keys::v3::{Request as KeysClaimRequest, Response as KeysClaimResponse}, + get_keys::v3::Response as KeysQueryResponse, + upload_keys::v3::{Request as KeysUploadRequest, Response as KeysUploadResponse}, + upload_signatures::v3::{ + Request as SignatureUploadRequest, Response as SignatureUploadResponse, + }, + upload_signing_keys::v3::Response as SigningKeysUploadResponse, + }, + message::send_message_event::v3::Response as RoomMessageResponse, + to_device::send_event_to_device::v3::Response as ToDeviceResponse, + }, + TransactionId, +}; + +use super::{ + KeysQueryRequest, OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, + ToDeviceRequest, +}; + +/// Enum over the different outgoing requests we can have. +#[derive(Debug)] +pub enum OutgoingRequests { + /// The `/keys/upload` request, uploading device and one-time keys. + KeysUpload(KeysUploadRequest), + /// The `/keys/query` request, fetching the device and cross signing keys of + /// other users. + KeysQuery(KeysQueryRequest), + /// The request to claim one-time keys for a user/device pair from the + /// server, after the response is received an 1-to-1 Olm session will be + /// established with the user/device pair. + KeysClaim(KeysClaimRequest), + /// The to-device requests, this request is used for a couple of different + /// things, the main use is key requests/forwards and interactive device + /// verification. + ToDeviceRequest(ToDeviceRequest), + /// Signature upload request, this request is used after a successful device + /// or user verification is done. + SignatureUpload(SignatureUploadRequest), + /// A room message request, usually for sending in-room interactive + /// verification events. + RoomMessage(RoomMessageRequest), +} + +#[cfg(test)] +impl OutgoingRequests { + /// Test helper to destructure the [`OutgoingRequests`] as a + /// [`ToDeviceRequest`]. + pub fn to_device(&self) -> Option<&ToDeviceRequest> { + as_variant::as_variant!(self, Self::ToDeviceRequest) + } +} + +impl From for OutgoingRequests { + fn from(request: KeysQueryRequest) -> Self { + Self::KeysQuery(request) + } +} + +impl From for OutgoingRequests { + fn from(r: KeysClaimRequest) -> Self { + Self::KeysClaim(r) + } +} + +impl From for OutgoingRequests { + fn from(request: KeysUploadRequest) -> Self { + Self::KeysUpload(request) + } +} + +impl From for OutgoingRequests { + fn from(request: ToDeviceRequest) -> Self { + Self::ToDeviceRequest(request) + } +} + +impl From for OutgoingRequests { + fn from(request: RoomMessageRequest) -> Self { + Self::RoomMessage(request) + } +} + +impl From for OutgoingRequests { + fn from(request: SignatureUploadRequest) -> Self { + Self::SignatureUpload(request) + } +} + +impl From for OutgoingRequest { + fn from(r: OutgoingVerificationRequest) -> Self { + Self { request_id: r.request_id().to_owned(), request: Arc::new(r.into()) } + } +} + +impl From for OutgoingRequest { + fn from(r: SignatureUploadRequest) -> Self { + Self { request_id: TransactionId::new(), request: Arc::new(r.into()) } + } +} + +impl From for OutgoingRequest { + fn from(r: KeysUploadRequest) -> Self { + Self { request_id: TransactionId::new(), request: Arc::new(r.into()) } + } +} + +impl From for OutgoingRequests { + fn from(request: OutgoingVerificationRequest) -> Self { + match request { + OutgoingVerificationRequest::ToDevice(r) => OutgoingRequests::ToDeviceRequest(r), + OutgoingVerificationRequest::InRoom(r) => OutgoingRequests::RoomMessage(r), + } + } +} + +/// Enum over all the incoming responses we need to receive. +#[derive(Debug)] +pub enum IncomingResponse<'a> { + /// The `/keys/upload` response, notifying us about the amount of uploaded + /// one-time keys. + KeysUpload(&'a KeysUploadResponse), + /// The `/keys/query` response, giving us the device and cross signing keys + /// of other users. + KeysQuery(&'a KeysQueryResponse), + /// The to-device response, an empty response. + ToDevice(&'a ToDeviceResponse), + /// The key claiming requests, giving us new one-time keys of other users so + /// new Olm sessions can be created. + KeysClaim(&'a KeysClaimResponse), + /// The cross signing `/keys/upload` response, marking our private cross + /// signing identity as shared. + SigningKeysUpload(&'a SigningKeysUploadResponse), + /// The cross signing signature upload response. + SignatureUpload(&'a SignatureUploadResponse), + /// A room message response, usually for interactive verifications. + RoomMessage(&'a RoomMessageResponse), + /// Response for the server-side room key backup request. + KeysBackup(&'a KeysBackupResponse), +} + +impl<'a> From<&'a KeysUploadResponse> for IncomingResponse<'a> { + fn from(response: &'a KeysUploadResponse) -> Self { + IncomingResponse::KeysUpload(response) + } +} + +impl<'a> From<&'a KeysBackupResponse> for IncomingResponse<'a> { + fn from(response: &'a KeysBackupResponse) -> Self { + IncomingResponse::KeysBackup(response) + } +} + +impl<'a> From<&'a KeysQueryResponse> for IncomingResponse<'a> { + fn from(response: &'a KeysQueryResponse) -> Self { + IncomingResponse::KeysQuery(response) + } +} + +impl<'a> From<&'a ToDeviceResponse> for IncomingResponse<'a> { + fn from(response: &'a ToDeviceResponse) -> Self { + IncomingResponse::ToDevice(response) + } +} + +impl<'a> From<&'a RoomMessageResponse> for IncomingResponse<'a> { + fn from(response: &'a RoomMessageResponse) -> Self { + IncomingResponse::RoomMessage(response) + } +} + +impl<'a> From<&'a KeysClaimResponse> for IncomingResponse<'a> { + fn from(response: &'a KeysClaimResponse) -> Self { + IncomingResponse::KeysClaim(response) + } +} + +impl<'a> From<&'a SignatureUploadResponse> for IncomingResponse<'a> { + fn from(response: &'a SignatureUploadResponse) -> Self { + IncomingResponse::SignatureUpload(response) + } +} diff --git a/crates/matrix-sdk-crypto/src/types/requests/keys_backup.rs b/crates/matrix-sdk-crypto/src/types/requests/keys_backup.rs new file mode 100644 index 00000000000..335f2e3d77d --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/requests/keys_backup.rs @@ -0,0 +1,27 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; + +use ruma::{api::client::backup::RoomKeyBackup, OwnedRoomId}; + +/// A request that will back up a batch of room keys to the server. +#[derive(Clone, Debug)] +pub struct KeysBackupRequest { + /// The backup version that these room keys should be part of. + pub version: String, + /// The map from room id to a backed up room key that we're going to upload + /// to the server. + pub rooms: BTreeMap, +} diff --git a/crates/matrix-sdk-crypto/src/types/requests/keys_query.rs b/crates/matrix-sdk-crypto/src/types/requests/keys_query.rs new file mode 100644 index 00000000000..02ba70598b1 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/requests/keys_query.rs @@ -0,0 +1,38 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::BTreeMap, time::Duration}; + +use ruma::{OwnedDeviceId, OwnedUserId}; + +/// Customized version of `ruma_client_api::keys::get_keys::v3::Request`, +/// without any references. +#[derive(Clone, Debug)] +pub struct KeysQueryRequest { + /// The time (in milliseconds) to wait when downloading keys from remote + /// servers. 10 seconds is the recommended default. + pub timeout: Option, + + /// The keys to be downloaded. An empty list indicates all devices for + /// the corresponding user. + pub device_keys: BTreeMap>, +} + +impl KeysQueryRequest { + pub(crate) fn new(users: impl Iterator) -> Self { + let device_keys = users.map(|u| (u, Vec::new())).collect(); + + Self { timeout: None, device_keys } + } +} diff --git a/crates/matrix-sdk-crypto/src/types/requests/mod.rs b/crates/matrix-sdk-crypto/src/types/requests/mod.rs index 11e3dfc4470..8d9aa1f720c 100644 --- a/crates/matrix-sdk-crypto/src/types/requests/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/requests/mod.rs @@ -14,341 +14,25 @@ //! Modules containing customized request types. -use std::{collections::BTreeMap, iter, sync::Arc, time::Duration}; - -#[cfg(test)] -use as_variant::as_variant; -use ruma::{ - api::client::{ - backup::{add_backup_keys::v3::Response as KeysBackupResponse, RoomKeyBackup}, - keys::{ - claim_keys::v3::{Request as KeysClaimRequest, Response as KeysClaimResponse}, - get_keys::v3::Response as KeysQueryResponse, - upload_keys::v3::{Request as KeysUploadRequest, Response as KeysUploadResponse}, - upload_signatures::v3::{ - Request as SignatureUploadRequest, Response as SignatureUploadResponse, - }, - upload_signing_keys::v3::Response as SigningKeysUploadResponse, - }, - message::send_message_event::v3::Response as RoomMessageResponse, - to_device::send_event_to_device::v3::Response as ToDeviceResponse, - }, - events::{ - AnyMessageLikeEventContent, AnyToDeviceEventContent, EventContent, ToDeviceEventType, - }, - serde::Raw, - to_device::DeviceIdOrAllDevices, - OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, TransactionId, UserId, -}; -use serde::{Deserialize, Serialize}; - -use crate::types::CrossSigningKey; - -/// Customized version of -/// `ruma_client_api::to_device::send_event_to_device::v3::Request` -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ToDeviceRequest { - /// Type of event being sent to each device. - pub event_type: ToDeviceEventType, - - /// A request identifier unique to the access token used to send the - /// request. - pub txn_id: OwnedTransactionId, - - /// A map of users to devices to a content for a message event to be - /// sent to the user's device. Individual message events can be sent - /// to devices, but all events must be of the same type. - /// The content's type for this field will be updated in a future - /// release, until then you can create a value using - /// `serde_json::value::to_raw_value`. - pub messages: - BTreeMap>>, -} - -impl ToDeviceRequest { - /// Create a new owned to-device request - /// - /// # Arguments - /// - /// * `recipient` - The ID of the user that should receive this to-device - /// event. - /// - /// * `recipient_device` - The device that should receive this to-device - /// event, or all devices. - /// - /// * `event_type` - The type of the event content that is getting sent out. - /// - /// * `content` - The content of the to-device event. - pub fn new( - recipient: &UserId, - recipient_device: impl Into, - event_type: &str, - content: Raw, - ) -> Self { - let event_type = ToDeviceEventType::from(event_type); - let user_messages = iter::once((recipient_device.into(), content)).collect(); - let messages = iter::once((recipient.to_owned(), user_messages)).collect(); - - ToDeviceRequest { event_type, txn_id: TransactionId::new(), messages } - } - - pub(crate) fn for_recipients( - recipient: &UserId, - recipient_devices: Vec, - content: &AnyToDeviceEventContent, - txn_id: OwnedTransactionId, - ) -> Self { - let event_type = content.event_type(); - let raw_content = Raw::new(content).expect("Failed to serialize to-device event"); - - if recipient_devices.is_empty() { - Self::new( - recipient, - DeviceIdOrAllDevices::AllDevices, - &event_type.to_string(), - raw_content, - ) - } else { - let device_messages = recipient_devices - .into_iter() - .map(|d| (DeviceIdOrAllDevices::DeviceId(d), raw_content.clone())) - .collect(); - - let messages = iter::once((recipient.to_owned(), device_messages)).collect(); - - ToDeviceRequest { event_type, txn_id, messages } - } - } - - pub(crate) fn with_id_raw( - recipient: &UserId, - recipient_device: impl Into, - content: Raw, - event_type: ToDeviceEventType, - txn_id: OwnedTransactionId, - ) -> Self { - let user_messages = iter::once((recipient_device.into(), content)).collect(); - let messages = iter::once((recipient.to_owned(), user_messages)).collect(); - - ToDeviceRequest { event_type, txn_id, messages } - } - - pub(crate) fn with_id( - recipient: &UserId, - recipient_device: impl Into, - content: &AnyToDeviceEventContent, - txn_id: OwnedTransactionId, - ) -> Self { - let event_type = content.event_type(); - let raw_content = Raw::new(content).expect("Failed to serialize to-device event"); - - let user_messages = iter::once((recipient_device.into(), raw_content)).collect(); - let messages = iter::once((recipient.to_owned(), user_messages)).collect(); - - ToDeviceRequest { event_type, txn_id, messages } - } - - /// Get the number of unique messages this request contains. - /// - /// *Note*: A single message may be sent to multiple devices, so this may or - /// may not be the number of devices that will receive the messages as well. - pub fn message_count(&self) -> usize { - self.messages.values().map(|d| d.len()).sum() - } -} - -/// Request that will publish a cross signing identity. -/// -/// This uploads the public cross signing key triplet. -#[derive(Debug, Clone)] -pub struct UploadSigningKeysRequest { - /// The user's master key. - pub master_key: Option, - /// The user's self-signing key. Must be signed with the accompanied master, - /// or by the user's most recently uploaded master key if no master key - /// is included in the request. - pub self_signing_key: Option, - /// The user's user-signing key. Must be signed with the accompanied master, - /// or by the user's most recently uploaded master key if no master key - /// is included in the request. - pub user_signing_key: Option, -} - -/// Customized version of -/// `ruma_client_api::keys::get_keys::v3::Request`, without any -/// references. -#[derive(Clone, Debug)] -pub struct KeysQueryRequest { - /// The time (in milliseconds) to wait when downloading keys from remote - /// servers. 10 seconds is the recommended default. - pub timeout: Option, - - /// The keys to be downloaded. An empty list indicates all devices for - /// the corresponding user. - pub device_keys: BTreeMap>, -} - -impl KeysQueryRequest { - pub(crate) fn new(users: impl Iterator) -> Self { - let device_keys = users.map(|u| (u, Vec::new())).collect(); - - Self { timeout: None, device_keys } - } -} - -/// Enum over the different outgoing requests we can have. -#[derive(Debug)] -pub enum OutgoingRequests { - /// The `/keys/upload` request, uploading device and one-time keys. - KeysUpload(KeysUploadRequest), - /// The `/keys/query` request, fetching the device and cross signing keys of - /// other users. - KeysQuery(KeysQueryRequest), - /// The request to claim one-time keys for a user/device pair from the - /// server, after the response is received an 1-to-1 Olm session will be - /// established with the user/device pair. - KeysClaim(KeysClaimRequest), - /// The to-device requests, this request is used for a couple of different - /// things, the main use is key requests/forwards and interactive device - /// verification. - ToDeviceRequest(ToDeviceRequest), - /// Signature upload request, this request is used after a successful device - /// or user verification is done. - SignatureUpload(SignatureUploadRequest), - /// A room message request, usually for sending in-room interactive - /// verification events. - RoomMessage(RoomMessageRequest), -} - -#[cfg(test)] -impl OutgoingRequests { - /// Test helper to destructure the [`OutgoingRequests`] as a - /// [`ToDeviceRequest`]. - pub fn to_device(&self) -> Option<&ToDeviceRequest> { - as_variant!(self, Self::ToDeviceRequest) - } -} - -impl From for OutgoingRequests { - fn from(request: KeysQueryRequest) -> Self { - Self::KeysQuery(request) - } -} - -impl From for OutgoingRequests { - fn from(r: KeysClaimRequest) -> Self { - Self::KeysClaim(r) - } -} - -impl From for OutgoingRequests { - fn from(request: KeysUploadRequest) -> Self { - Self::KeysUpload(request) - } -} - -impl From for OutgoingRequests { - fn from(request: ToDeviceRequest) -> Self { - Self::ToDeviceRequest(request) - } -} - -impl From for OutgoingRequests { - fn from(request: RoomMessageRequest) -> Self { - Self::RoomMessage(request) - } -} - -impl From for OutgoingRequests { - fn from(request: SignatureUploadRequest) -> Self { - Self::SignatureUpload(request) - } -} - -impl From for OutgoingRequest { - fn from(r: OutgoingVerificationRequest) -> Self { - Self { request_id: r.request_id().to_owned(), request: Arc::new(r.into()) } - } -} - -impl From for OutgoingRequest { - fn from(r: SignatureUploadRequest) -> Self { - Self { request_id: TransactionId::new(), request: Arc::new(r.into()) } - } -} - -impl From for OutgoingRequest { - fn from(r: KeysUploadRequest) -> Self { - Self { request_id: TransactionId::new(), request: Arc::new(r.into()) } - } -} - -/// Enum over all the incoming responses we need to receive. -#[derive(Debug)] -pub enum IncomingResponse<'a> { - /// The `/keys/upload` response, notifying us about the amount of uploaded - /// one-time keys. - KeysUpload(&'a KeysUploadResponse), - /// The `/keys/query` response, giving us the device and cross signing keys - /// of other users. - KeysQuery(&'a KeysQueryResponse), - /// The to-device response, an empty response. - ToDevice(&'a ToDeviceResponse), - /// The key claiming requests, giving us new one-time keys of other users so - /// new Olm sessions can be created. - KeysClaim(&'a KeysClaimResponse), - /// The cross signing `/keys/upload` response, marking our private cross - /// signing identity as shared. - SigningKeysUpload(&'a SigningKeysUploadResponse), - /// The cross signing signature upload response. - SignatureUpload(&'a SignatureUploadResponse), - /// A room message response, usually for interactive verifications. - RoomMessage(&'a RoomMessageResponse), - /// Response for the server-side room key backup request. - KeysBackup(&'a KeysBackupResponse), -} - -impl<'a> From<&'a KeysUploadResponse> for IncomingResponse<'a> { - fn from(response: &'a KeysUploadResponse) -> Self { - IncomingResponse::KeysUpload(response) - } -} - -impl<'a> From<&'a KeysBackupResponse> for IncomingResponse<'a> { - fn from(response: &'a KeysBackupResponse) -> Self { - IncomingResponse::KeysBackup(response) - } -} - -impl<'a> From<&'a KeysQueryResponse> for IncomingResponse<'a> { - fn from(response: &'a KeysQueryResponse) -> Self { - IncomingResponse::KeysQuery(response) - } -} - -impl<'a> From<&'a ToDeviceResponse> for IncomingResponse<'a> { - fn from(response: &'a ToDeviceResponse) -> Self { - IncomingResponse::ToDevice(response) - } -} - -impl<'a> From<&'a RoomMessageResponse> for IncomingResponse<'a> { - fn from(response: &'a RoomMessageResponse) -> Self { - IncomingResponse::RoomMessage(response) - } -} - -impl<'a> From<&'a KeysClaimResponse> for IncomingResponse<'a> { - fn from(response: &'a KeysClaimResponse) -> Self { - IncomingResponse::KeysClaim(response) - } -} - -impl<'a> From<&'a SignatureUploadResponse> for IncomingResponse<'a> { - fn from(response: &'a SignatureUploadResponse) -> Self { - IncomingResponse::SignatureUpload(response) - } -} +use std::sync::Arc; + +use ruma::{OwnedTransactionId, TransactionId}; + +mod enums; +mod keys_backup; +mod keys_query; +mod room_message; +mod signing_keys; +mod to_device; +mod verification; + +pub use enums::*; +pub use keys_backup::*; +pub use keys_query::*; +pub use room_message::*; +pub use signing_keys::*; +pub use to_device::*; +pub use verification::*; /// Outgoing request type, holds the unique ID of the request and the actual /// request. @@ -372,70 +56,3 @@ impl OutgoingRequest { &self.request } } - -/// Customized owned request type for sending out room messages. -#[derive(Clone, Debug)] -pub struct RoomMessageRequest { - /// The room to send the event to. - pub room_id: OwnedRoomId, - - /// The transaction ID for this event. - /// - /// Clients should generate an ID unique across requests with the - /// same access token; it will be used by the server to ensure - /// idempotency of requests. - pub txn_id: OwnedTransactionId, - - /// The event content to send. - pub content: AnyMessageLikeEventContent, -} - -/// A request that will back up a batch of room keys to the server. -#[derive(Clone, Debug)] -pub struct KeysBackupRequest { - /// The backup version that these room keys should be part of. - pub version: String, - /// The map from room id to a backed up room key that we're going to upload - /// to the server. - pub rooms: BTreeMap, -} - -/// An enum over the different outgoing verification based requests. -#[derive(Clone, Debug)] -pub enum OutgoingVerificationRequest { - /// The to-device verification request variant. - ToDevice(ToDeviceRequest), - /// The in-room verification request variant. - InRoom(RoomMessageRequest), -} - -impl OutgoingVerificationRequest { - /// Get the unique id of this request. - pub fn request_id(&self) -> &TransactionId { - match self { - OutgoingVerificationRequest::ToDevice(t) => &t.txn_id, - OutgoingVerificationRequest::InRoom(r) => &r.txn_id, - } - } -} - -impl From for OutgoingVerificationRequest { - fn from(r: ToDeviceRequest) -> Self { - OutgoingVerificationRequest::ToDevice(r) - } -} - -impl From for OutgoingVerificationRequest { - fn from(r: RoomMessageRequest) -> Self { - OutgoingVerificationRequest::InRoom(r) - } -} - -impl From for OutgoingRequests { - fn from(request: OutgoingVerificationRequest) -> Self { - match request { - OutgoingVerificationRequest::ToDevice(r) => OutgoingRequests::ToDeviceRequest(r), - OutgoingVerificationRequest::InRoom(r) => OutgoingRequests::RoomMessage(r), - } - } -} diff --git a/crates/matrix-sdk-crypto/src/types/requests/room_message.rs b/crates/matrix-sdk-crypto/src/types/requests/room_message.rs new file mode 100644 index 00000000000..85580c59b1b --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/requests/room_message.rs @@ -0,0 +1,32 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use ruma::{events::AnyMessageLikeEventContent, OwnedRoomId, OwnedTransactionId}; + +/// Customized owned request type for sending out room messages. +#[derive(Clone, Debug)] +pub struct RoomMessageRequest { + /// The room to send the event to. + pub room_id: OwnedRoomId, + + /// The transaction ID for this event. + /// + /// Clients should generate an ID unique across requests with the + /// same access token; it will be used by the server to ensure + /// idempotency of requests. + pub txn_id: OwnedTransactionId, + + /// The event content to send. + pub content: AnyMessageLikeEventContent, +} diff --git a/crates/matrix-sdk-crypto/src/types/requests/signing_keys.rs b/crates/matrix-sdk-crypto/src/types/requests/signing_keys.rs new file mode 100644 index 00000000000..76c461b117f --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/requests/signing_keys.rs @@ -0,0 +1,32 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::types::CrossSigningKey; + +/// Request that will publish a cross signing identity. +/// +/// This uploads the public cross signing key triplet. +#[derive(Debug, Clone)] +pub struct UploadSigningKeysRequest { + /// The user's master key. + pub master_key: Option, + /// The user's self-signing key. Must be signed with the accompanied master, + /// or by the user's most recently uploaded master key if no master key + /// is included in the request. + pub self_signing_key: Option, + /// The user's user-signing key. Must be signed with the accompanied master, + /// or by the user's most recently uploaded master key if no master key + /// is included in the request. + pub user_signing_key: Option, +} diff --git a/crates/matrix-sdk-crypto/src/types/requests/to_device.rs b/crates/matrix-sdk-crypto/src/types/requests/to_device.rs new file mode 100644 index 00000000000..cccd739d459 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/requests/to_device.rs @@ -0,0 +1,136 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::BTreeMap, iter}; + +use ruma::{ + events::{AnyToDeviceEventContent, EventContent, ToDeviceEventType}, + serde::Raw, + to_device::DeviceIdOrAllDevices, + OwnedDeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId, +}; +use serde::{Deserialize, Serialize}; + +/// Customized version of +/// `ruma_client_api::to_device::send_event_to_device::v3::Request` +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ToDeviceRequest { + /// Type of event being sent to each device. + pub event_type: ToDeviceEventType, + + /// A request identifier unique to the access token used to send the + /// request. + pub txn_id: OwnedTransactionId, + + /// A map of users to devices to a content for a message event to be + /// sent to the user's device. Individual message events can be sent + /// to devices, but all events must be of the same type. + /// The content's type for this field will be updated in a future + /// release, until then you can create a value using + /// `serde_json::value::to_raw_value`. + pub messages: + BTreeMap>>, +} + +impl ToDeviceRequest { + /// Create a new owned to-device request + /// + /// # Arguments + /// + /// * `recipient` - The ID of the user that should receive this to-device + /// event. + /// + /// * `recipient_device` - The device that should receive this to-device + /// event, or all devices. + /// + /// * `event_type` - The type of the event content that is getting sent out. + /// + /// * `content` - The content of the to-device event. + pub fn new( + recipient: &UserId, + recipient_device: impl Into, + event_type: &str, + content: Raw, + ) -> Self { + let event_type = ToDeviceEventType::from(event_type); + let user_messages = iter::once((recipient_device.into(), content)).collect(); + let messages = iter::once((recipient.to_owned(), user_messages)).collect(); + + ToDeviceRequest { event_type, txn_id: TransactionId::new(), messages } + } + + pub(crate) fn for_recipients( + recipient: &UserId, + recipient_devices: Vec, + content: &AnyToDeviceEventContent, + txn_id: OwnedTransactionId, + ) -> Self { + let event_type = content.event_type(); + let raw_content = Raw::new(content).expect("Failed to serialize to-device event"); + + if recipient_devices.is_empty() { + Self::new( + recipient, + DeviceIdOrAllDevices::AllDevices, + &event_type.to_string(), + raw_content, + ) + } else { + let device_messages = recipient_devices + .into_iter() + .map(|d| (DeviceIdOrAllDevices::DeviceId(d), raw_content.clone())) + .collect(); + + let messages = iter::once((recipient.to_owned(), device_messages)).collect(); + + ToDeviceRequest { event_type, txn_id, messages } + } + } + + pub(crate) fn with_id_raw( + recipient: &UserId, + recipient_device: impl Into, + content: Raw, + event_type: ToDeviceEventType, + txn_id: OwnedTransactionId, + ) -> Self { + let user_messages = iter::once((recipient_device.into(), content)).collect(); + let messages = iter::once((recipient.to_owned(), user_messages)).collect(); + + ToDeviceRequest { event_type, txn_id, messages } + } + + pub(crate) fn with_id( + recipient: &UserId, + recipient_device: impl Into, + content: &AnyToDeviceEventContent, + txn_id: OwnedTransactionId, + ) -> Self { + let event_type = content.event_type(); + let raw_content = Raw::new(content).expect("Failed to serialize to-device event"); + + let user_messages = iter::once((recipient_device.into(), raw_content)).collect(); + let messages = iter::once((recipient.to_owned(), user_messages)).collect(); + + ToDeviceRequest { event_type, txn_id, messages } + } + + /// Get the number of unique messages this request contains. + /// + /// *Note*: A single message may be sent to multiple devices, so this may or + /// may not be the number of devices that will receive the messages as well. + pub fn message_count(&self) -> usize { + self.messages.values().map(|d| d.len()).sum() + } +} diff --git a/crates/matrix-sdk-crypto/src/types/requests/verification.rs b/crates/matrix-sdk-crypto/src/types/requests/verification.rs new file mode 100644 index 00000000000..80ecdc588f8 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/requests/verification.rs @@ -0,0 +1,48 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use ruma::TransactionId; + +use super::{RoomMessageRequest, ToDeviceRequest}; + +/// An enum over the different outgoing verification based requests. +#[derive(Clone, Debug)] +pub enum OutgoingVerificationRequest { + /// The to-device verification request variant. + ToDevice(ToDeviceRequest), + /// The in-room verification request variant. + InRoom(RoomMessageRequest), +} + +impl OutgoingVerificationRequest { + /// Get the unique id of this request. + pub fn request_id(&self) -> &TransactionId { + match self { + OutgoingVerificationRequest::ToDevice(t) => &t.txn_id, + OutgoingVerificationRequest::InRoom(r) => &r.txn_id, + } + } +} + +impl From for OutgoingVerificationRequest { + fn from(r: ToDeviceRequest) -> Self { + OutgoingVerificationRequest::ToDevice(r) + } +} + +impl From for OutgoingVerificationRequest { + fn from(r: RoomMessageRequest) -> Self { + OutgoingVerificationRequest::InRoom(r) + } +} From 600a708e7b526fd335a196f7ec7b22fba97f6ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 18:34:15 +0100 Subject: [PATCH 626/979] refactor!(crypto): Rename the OutgoingRequests enum to AnyOutgoingRequest --- .../matrix-sdk-crypto-ffi/src/responses.rs | 2 +- .../src/gossiping/machine.rs | 4 ++-- .../src/machine/test_helpers.rs | 4 ++-- .../tests/decryption_verification_state.rs | 4 ++-- .../src/machine/tests/mod.rs | 6 ++--- .../src/types/requests/enums.rs | 22 +++++++++---------- .../src/types/requests/mod.rs | 4 ++-- .../src/verification/event_enums.rs | 14 ++++++------ .../matrix-sdk-crypto/src/verification/mod.rs | 4 ++-- crates/matrix-sdk/src/encryption/mod.rs | 14 ++++++------ crates/matrix-sdk/src/sliding_sync/mod.rs | 10 ++++----- 11 files changed, 44 insertions(+), 44 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index e46d02b49b3..071981ff073 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -139,7 +139,7 @@ pub enum Request { impl From for Request { fn from(r: OutgoingRequest) -> Self { - use matrix_sdk_crypto::types::requests::OutgoingRequests::*; + use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*; match r.request() { KeysUpload(u) => { diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index ac28ba58628..5b7a7055ab9 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -1117,7 +1117,7 @@ mod tests { use crate::{ gossiping::KeyForwardDecision, olm::OutboundGroupSession, - types::requests::OutgoingRequests, + types::requests::AnyOutgoingRequest, types::{ events::{ forwarded_room_key::ForwardedRoomKeyContent, olm_v1::AnyDecryptedOlmEvent, @@ -2066,7 +2066,7 @@ mod tests { assert_eq!(bob_machine.outgoing_to_device_requests().await.unwrap().len(), 1); assert_matches!( bob_machine.outgoing_to_device_requests().await.unwrap()[0].request(), - OutgoingRequests::KeysClaim(_) + AnyOutgoingRequest::KeysClaim(_) ); assert!(!bob_machine.inner.users_for_key_claim.read().unwrap().is_empty()); assert!(!bob_machine.inner.wait_queue.is_empty()); diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 0c3d4a44e20..fec64381f03 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -35,7 +35,7 @@ use serde_json::json; use crate::{ store::Changes, - types::{events::ToDeviceEvent, requests::OutgoingRequests}, + types::{events::ToDeviceEvent, requests::AnyOutgoingRequest}, CrossSigningBootstrapRequests, DeviceData, OlmMachine, }; @@ -215,7 +215,7 @@ pub fn bootstrap_requests_to_keys_query_response( // And if we have a device, add that if let Some(dk) = bootstrap_requests .upload_keys_req - .and_then(|req| as_variant!(req.request.as_ref(), OutgoingRequests::KeysUpload).cloned()) + .and_then(|req| as_variant!(req.request.as_ref(), AnyOutgoingRequest::KeysUpload).cloned()) .and_then(|keys_upload_request| keys_upload_request.device_keys) { let user_id: String = dk.get_field("user_id").unwrap().unwrap(); diff --git a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs index 6194efc82c0..82cb3d707a2 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs @@ -42,7 +42,7 @@ use crate::{ room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme}, ToDeviceEvent, }, - requests::OutgoingRequests, + requests::AnyOutgoingRequest, CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, SelfSigningPubkey, }, utilities::json_convert, @@ -498,7 +498,7 @@ async fn set_up_alice_cross_signing(alice: &OlmMachine, bob: &OlmMachine) { upload_signing_keys_req.self_signing_key.unwrap().try_into().unwrap(); let upload_keys_req = cross_signing_requests.upload_keys_req.unwrap().clone(); assert_let!( - OutgoingRequests::KeysUpload(device_upload_request) = upload_keys_req.request.as_ref() + AnyOutgoingRequest::KeysUpload(device_upload_request) = upload_keys_req.request.as_ref() ); bob.store() .save_device_data(&[DeviceData::try_from( diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 3a608a8aca3..f1c52747c6b 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -66,7 +66,7 @@ use crate::{ }, ToDeviceEvent, }, - requests::{OutgoingRequests, ToDeviceRequest}, + requests::{AnyOutgoingRequest, ToDeviceRequest}, DeviceKeys, SignedKey, SigningKeys, }, utilities::json_convert, @@ -449,7 +449,7 @@ async fn test_request_missing_secrets() { .unwrap() .into_iter() .filter(|outgoing| match outgoing.request.as_ref() { - OutgoingRequests::ToDeviceRequest(request) => { + AnyOutgoingRequest::ToDeviceRequest(request) => { request.event_type.to_string() == "m.secret.request" } _ => false, @@ -480,7 +480,7 @@ async fn test_request_missing_secrets_cross_signed() { .unwrap() .into_iter() .filter(|outgoing| match outgoing.request.as_ref() { - OutgoingRequests::ToDeviceRequest(request) => { + AnyOutgoingRequest::ToDeviceRequest(request) => { request.event_type.to_string() == "m.secret.request" } _ => false, diff --git a/crates/matrix-sdk-crypto/src/types/requests/enums.rs b/crates/matrix-sdk-crypto/src/types/requests/enums.rs index 98e2523fbbf..3be825e83ba 100644 --- a/crates/matrix-sdk-crypto/src/types/requests/enums.rs +++ b/crates/matrix-sdk-crypto/src/types/requests/enums.rs @@ -39,7 +39,7 @@ use super::{ /// Enum over the different outgoing requests we can have. #[derive(Debug)] -pub enum OutgoingRequests { +pub enum AnyOutgoingRequest { /// The `/keys/upload` request, uploading device and one-time keys. KeysUpload(KeysUploadRequest), /// The `/keys/query` request, fetching the device and cross signing keys of @@ -62,7 +62,7 @@ pub enum OutgoingRequests { } #[cfg(test)] -impl OutgoingRequests { +impl AnyOutgoingRequest { /// Test helper to destructure the [`OutgoingRequests`] as a /// [`ToDeviceRequest`]. pub fn to_device(&self) -> Option<&ToDeviceRequest> { @@ -70,37 +70,37 @@ impl OutgoingRequests { } } -impl From for OutgoingRequests { +impl From for AnyOutgoingRequest { fn from(request: KeysQueryRequest) -> Self { Self::KeysQuery(request) } } -impl From for OutgoingRequests { +impl From for AnyOutgoingRequest { fn from(r: KeysClaimRequest) -> Self { Self::KeysClaim(r) } } -impl From for OutgoingRequests { +impl From for AnyOutgoingRequest { fn from(request: KeysUploadRequest) -> Self { Self::KeysUpload(request) } } -impl From for OutgoingRequests { +impl From for AnyOutgoingRequest { fn from(request: ToDeviceRequest) -> Self { Self::ToDeviceRequest(request) } } -impl From for OutgoingRequests { +impl From for AnyOutgoingRequest { fn from(request: RoomMessageRequest) -> Self { Self::RoomMessage(request) } } -impl From for OutgoingRequests { +impl From for AnyOutgoingRequest { fn from(request: SignatureUploadRequest) -> Self { Self::SignatureUpload(request) } @@ -124,11 +124,11 @@ impl From for OutgoingRequest { } } -impl From for OutgoingRequests { +impl From for AnyOutgoingRequest { fn from(request: OutgoingVerificationRequest) -> Self { match request { - OutgoingVerificationRequest::ToDevice(r) => OutgoingRequests::ToDeviceRequest(r), - OutgoingVerificationRequest::InRoom(r) => OutgoingRequests::RoomMessage(r), + OutgoingVerificationRequest::ToDevice(r) => AnyOutgoingRequest::ToDeviceRequest(r), + OutgoingVerificationRequest::InRoom(r) => AnyOutgoingRequest::RoomMessage(r), } } } diff --git a/crates/matrix-sdk-crypto/src/types/requests/mod.rs b/crates/matrix-sdk-crypto/src/types/requests/mod.rs index 8d9aa1f720c..11d48156d9f 100644 --- a/crates/matrix-sdk-crypto/src/types/requests/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/requests/mod.rs @@ -42,7 +42,7 @@ pub struct OutgoingRequest { /// response. pub(crate) request_id: OwnedTransactionId, /// The underlying outgoing request. - pub(crate) request: Arc, + pub(crate) request: Arc, } impl OutgoingRequest { @@ -52,7 +52,7 @@ impl OutgoingRequest { } /// Get the underlying outgoing request. - pub fn request(&self) -> &OutgoingRequests { + pub fn request(&self) -> &AnyOutgoingRequest { &self.request } } diff --git a/crates/matrix-sdk-crypto/src/verification/event_enums.rs b/crates/matrix-sdk-crypto/src/verification/event_enums.rs index 45fdd5043bd..3fbdf0a774f 100644 --- a/crates/matrix-sdk-crypto/src/verification/event_enums.rs +++ b/crates/matrix-sdk-crypto/src/verification/event_enums.rs @@ -765,15 +765,15 @@ impl TryFrom for OutgoingContent { type Error = String; fn try_from(value: OutgoingRequest) -> Result { - use crate::types::requests::OutgoingRequests; + use crate::types::requests::AnyOutgoingRequest; match value.request() { - OutgoingRequests::KeysUpload(_) - | OutgoingRequests::KeysQuery(_) - | OutgoingRequests::SignatureUpload(_) - | OutgoingRequests::KeysClaim(_) => Err("Invalid request type".to_owned()), - OutgoingRequests::ToDeviceRequest(r) => Self::try_from(r.clone()), - OutgoingRequests::RoomMessage(r) => Ok(Self::from(r.clone())), + AnyOutgoingRequest::KeysUpload(_) + | AnyOutgoingRequest::KeysQuery(_) + | AnyOutgoingRequest::SignatureUpload(_) + | AnyOutgoingRequest::KeysClaim(_) => Err("Invalid request type".to_owned()), + AnyOutgoingRequest::ToDeviceRequest(r) => Self::try_from(r.clone()), + AnyOutgoingRequest::RoomMessage(r) => Ok(Self::from(r.clone())), } } } diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 07da3685dc4..ebf3934b643 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -748,7 +748,7 @@ pub(crate) mod tests { store::{Changes, CryptoStore, CryptoStoreWrapper, IdentityChanges, MemoryStore}, types::{ events::ToDeviceEvents, - requests::{OutgoingRequest, OutgoingRequests, OutgoingVerificationRequest}, + requests::{OutgoingRequest, AnyOutgoingRequest, OutgoingVerificationRequest}, }, Account, DeviceData, OtherUserIdentityData, OwnUserIdentityData, }; @@ -767,7 +767,7 @@ pub(crate) mod tests { request: &OutgoingRequest, ) -> ToDeviceEvents { match request.request() { - OutgoingRequests::ToDeviceRequest(r) => request_to_event(sender, &r.clone().into()), + AnyOutgoingRequest::ToDeviceRequest(r) => request_to_event(sender, &r.clone().into()), _ => panic!("Unsupported outgoing request"), } } diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index aaf88c7d7cc..4544f171f93 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -613,28 +613,28 @@ impl Client { } async fn send_outgoing_request(&self, r: OutgoingRequest) -> Result<()> { - use matrix_sdk_base::crypto::types::requests::OutgoingRequests; + use matrix_sdk_base::crypto::types::requests::AnyOutgoingRequest; match r.request() { - OutgoingRequests::KeysQuery(request) => { + AnyOutgoingRequest::KeysQuery(request) => { self.keys_query(r.request_id(), request.device_keys.clone()).await?; } - OutgoingRequests::KeysUpload(request) => { + AnyOutgoingRequest::KeysUpload(request) => { self.keys_upload(r.request_id(), request).await?; } - OutgoingRequests::ToDeviceRequest(request) => { + AnyOutgoingRequest::ToDeviceRequest(request) => { let response = self.send_to_device(request).await?; self.mark_request_as_sent(r.request_id(), &response).await?; } - OutgoingRequests::SignatureUpload(request) => { + AnyOutgoingRequest::SignatureUpload(request) => { let response = self.send(request.clone(), None).await?; self.mark_request_as_sent(r.request_id(), &response).await?; } - OutgoingRequests::RoomMessage(request) => { + AnyOutgoingRequest::RoomMessage(request) => { let response = self.room_send_helper(request).await?; self.mark_request_as_sent(r.request_id(), &response).await?; } - OutgoingRequests::KeysClaim(request) => { + AnyOutgoingRequest::KeysClaim(request) => { let response = self.send(request.clone(), None).await?; self.mark_request_as_sent(r.request_id(), &response).await?; } diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index bfbbb69cb17..e0a0c536585 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1693,7 +1693,7 @@ mod tests { #[async_test] #[cfg(feature = "e2e-encryption")] async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> { - use matrix_sdk_base::crypto::types::requests::{IncomingResponse, OutgoingRequests}; + use matrix_sdk_base::crypto::types::requests::{IncomingResponse, AnyOutgoingRequest}; use matrix_sdk_test::ruma_response_from_json; use ruma::user_id; @@ -1716,8 +1716,8 @@ mod tests { let outgoing_requests = olm_machine.outgoing_requests().await?; assert_eq!(outgoing_requests.len(), 2); - assert_matches!(outgoing_requests[0].request(), OutgoingRequests::KeysUpload(_)); - assert_matches!(outgoing_requests[1].request(), OutgoingRequests::KeysQuery(_)); + assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysUpload(_)); + assert_matches!(outgoing_requests[1].request(), AnyOutgoingRequest::KeysQuery(_)); // Fake responses. olm_machine @@ -1745,7 +1745,7 @@ mod tests { let outgoing_requests = olm_machine.outgoing_requests().await?; assert_eq!(outgoing_requests.len(), 1); - assert_matches!(outgoing_requests[0].request(), OutgoingRequests::KeysQuery(_)); + assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysQuery(_)); olm_machine .mark_request_as_sent( @@ -1788,7 +1788,7 @@ mod tests { assert_eq!(outgoing_requests.len(), 1); assert_matches!( outgoing_requests[0].request(), - OutgoingRequests::KeysQuery(request) => { + AnyOutgoingRequest::KeysQuery(request) => { assert!(request.device_keys.contains_key(alice)); assert!(request.device_keys.contains_key(bob)); assert!(request.device_keys.contains_key(me)); From e99939db857ca36054c0023ef68cc181236fdee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 27 Nov 2024 18:36:22 +0100 Subject: [PATCH 627/979] refactor(crypto): Rename the IncomingResponse enum to AnyIncomingResponse --- .../matrix-sdk-crypto-ffi/src/responses.rs | 18 +++++------ crates/matrix-sdk-crypto/src/machine/mod.rs | 20 ++++++------- .../group_sessions/share_strategy.rs | 2 +- .../src/types/requests/enums.rs | 30 +++++++++---------- .../matrix-sdk-crypto/src/verification/mod.rs | 2 +- crates/matrix-sdk/src/encryption/mod.rs | 2 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 10 +++---- 7 files changed, 42 insertions(+), 42 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index 071981ff073..379498a24ca 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use http::Response; use matrix_sdk_crypto::{ types::requests::{ - IncomingResponse, KeysBackupRequest, OutgoingRequest, + AnyIncomingResponse, KeysBackupRequest, OutgoingRequest, OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest as RustUploadSigningKeysRequest, }, @@ -341,16 +341,16 @@ impl From for OwnedResponse { } } -impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> { +impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> { fn from(r: &'a OwnedResponse) -> Self { match r { - OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r), - OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r), - OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r), - OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r), - OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r), - OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r), - OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r), + OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r), + OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r), + OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r), + OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r), + OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r), + OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r), + OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r), } } } diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index c19b2c29a69..d5c80809b38 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -90,7 +90,7 @@ use crate::{ ToDeviceEvents, }, requests::{ - IncomingResponse, KeysQueryRequest, OutgoingRequest, ToDeviceRequest, + AnyIncomingResponse, KeysQueryRequest, OutgoingRequest, ToDeviceRequest, UploadSigningKeysRequest, }, EventEncryptionAlgorithm, Signatures, @@ -579,34 +579,34 @@ impl OlmMachine { pub async fn mark_request_as_sent<'a>( &self, request_id: &TransactionId, - response: impl Into>, + response: impl Into>, ) -> OlmResult<()> { match response.into() { - IncomingResponse::KeysUpload(response) => { + AnyIncomingResponse::KeysUpload(response) => { Box::pin(self.receive_keys_upload_response(response)).await?; } - IncomingResponse::KeysQuery(response) => { + AnyIncomingResponse::KeysQuery(response) => { Box::pin(self.receive_keys_query_response(request_id, response)).await?; } - IncomingResponse::KeysClaim(response) => { + AnyIncomingResponse::KeysClaim(response) => { Box::pin( self.inner.session_manager.receive_keys_claim_response(request_id, response), ) .await?; } - IncomingResponse::ToDevice(_) => { + AnyIncomingResponse::ToDevice(_) => { Box::pin(self.mark_to_device_request_as_sent(request_id)).await?; } - IncomingResponse::SigningKeysUpload(_) => { + AnyIncomingResponse::SigningKeysUpload(_) => { Box::pin(self.receive_cross_signing_upload_response()).await?; } - IncomingResponse::SignatureUpload(_) => { + AnyIncomingResponse::SignatureUpload(_) => { self.inner.verification_machine.mark_request_as_sent(request_id); } - IncomingResponse::RoomMessage(_) => { + AnyIncomingResponse::RoomMessage(_) => { self.inner.verification_machine.mark_request_as_sent(request_id); } - IncomingResponse::KeysBackup(_) => { + AnyIncomingResponse::KeysBackup(_) => { Box::pin(self.inner.backup_machine.mark_request_as_sent(request_id)).await?; } }; diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 20ab2dfb8e8..7b3df39f41a 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -1368,7 +1368,7 @@ mod tests { machine .mark_request_as_sent( &TransactionId::new(), - crate::types::requests::IncomingResponse::KeysQuery(&kq_response), + crate::types::requests::AnyIncomingResponse::KeysQuery(&kq_response), ) .await .unwrap(); diff --git a/crates/matrix-sdk-crypto/src/types/requests/enums.rs b/crates/matrix-sdk-crypto/src/types/requests/enums.rs index 3be825e83ba..3da51ec62b7 100644 --- a/crates/matrix-sdk-crypto/src/types/requests/enums.rs +++ b/crates/matrix-sdk-crypto/src/types/requests/enums.rs @@ -135,7 +135,7 @@ impl From for AnyOutgoingRequest { /// Enum over all the incoming responses we need to receive. #[derive(Debug)] -pub enum IncomingResponse<'a> { +pub enum AnyIncomingResponse<'a> { /// The `/keys/upload` response, notifying us about the amount of uploaded /// one-time keys. KeysUpload(&'a KeysUploadResponse), @@ -158,44 +158,44 @@ pub enum IncomingResponse<'a> { KeysBackup(&'a KeysBackupResponse), } -impl<'a> From<&'a KeysUploadResponse> for IncomingResponse<'a> { +impl<'a> From<&'a KeysUploadResponse> for AnyIncomingResponse<'a> { fn from(response: &'a KeysUploadResponse) -> Self { - IncomingResponse::KeysUpload(response) + AnyIncomingResponse::KeysUpload(response) } } -impl<'a> From<&'a KeysBackupResponse> for IncomingResponse<'a> { +impl<'a> From<&'a KeysBackupResponse> for AnyIncomingResponse<'a> { fn from(response: &'a KeysBackupResponse) -> Self { - IncomingResponse::KeysBackup(response) + AnyIncomingResponse::KeysBackup(response) } } -impl<'a> From<&'a KeysQueryResponse> for IncomingResponse<'a> { +impl<'a> From<&'a KeysQueryResponse> for AnyIncomingResponse<'a> { fn from(response: &'a KeysQueryResponse) -> Self { - IncomingResponse::KeysQuery(response) + AnyIncomingResponse::KeysQuery(response) } } -impl<'a> From<&'a ToDeviceResponse> for IncomingResponse<'a> { +impl<'a> From<&'a ToDeviceResponse> for AnyIncomingResponse<'a> { fn from(response: &'a ToDeviceResponse) -> Self { - IncomingResponse::ToDevice(response) + AnyIncomingResponse::ToDevice(response) } } -impl<'a> From<&'a RoomMessageResponse> for IncomingResponse<'a> { +impl<'a> From<&'a RoomMessageResponse> for AnyIncomingResponse<'a> { fn from(response: &'a RoomMessageResponse) -> Self { - IncomingResponse::RoomMessage(response) + AnyIncomingResponse::RoomMessage(response) } } -impl<'a> From<&'a KeysClaimResponse> for IncomingResponse<'a> { +impl<'a> From<&'a KeysClaimResponse> for AnyIncomingResponse<'a> { fn from(response: &'a KeysClaimResponse) -> Self { - IncomingResponse::KeysClaim(response) + AnyIncomingResponse::KeysClaim(response) } } -impl<'a> From<&'a SignatureUploadResponse> for IncomingResponse<'a> { +impl<'a> From<&'a SignatureUploadResponse> for AnyIncomingResponse<'a> { fn from(response: &'a SignatureUploadResponse) -> Self { - IncomingResponse::SignatureUpload(response) + AnyIncomingResponse::SignatureUpload(response) } } diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index ebf3934b643..a9e26a2a7ed 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -748,7 +748,7 @@ pub(crate) mod tests { store::{Changes, CryptoStore, CryptoStoreWrapper, IdentityChanges, MemoryStore}, types::{ events::ToDeviceEvents, - requests::{OutgoingRequest, AnyOutgoingRequest, OutgoingVerificationRequest}, + requests::{AnyOutgoingRequest, OutgoingRequest, OutgoingVerificationRequest}, }, Account, DeviceData, OtherUserIdentityData, OwnUserIdentityData, }; diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 4544f171f93..f8c7909940d 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -380,7 +380,7 @@ impl Client { pub(crate) async fn mark_request_as_sent( &self, request_id: &TransactionId, - response: impl Into>, + response: impl Into>, ) -> Result<(), matrix_sdk_base::Error> { Ok(self .olm_machine() diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index e0a0c536585..fee7155b94c 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1693,7 +1693,7 @@ mod tests { #[async_test] #[cfg(feature = "e2e-encryption")] async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> { - use matrix_sdk_base::crypto::types::requests::{IncomingResponse, AnyOutgoingRequest}; + use matrix_sdk_base::crypto::types::requests::{AnyIncomingResponse, AnyOutgoingRequest}; use matrix_sdk_test::ruma_response_from_json; use ruma::user_id; @@ -1723,7 +1723,7 @@ mod tests { olm_machine .mark_request_as_sent( outgoing_requests[0].request_id(), - IncomingResponse::KeysUpload(&ruma_response_from_json(&json!({ + AnyIncomingResponse::KeysUpload(&ruma_response_from_json(&json!({ "one_time_key_counts": {} }))), ) @@ -1732,7 +1732,7 @@ mod tests { olm_machine .mark_request_as_sent( outgoing_requests[1].request_id(), - IncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ + AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ "device_keys": { alice: {}, bob: {}, @@ -1750,7 +1750,7 @@ mod tests { olm_machine .mark_request_as_sent( outgoing_requests[0].request_id(), - IncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ + AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ "device_keys": { me: {}, } @@ -1799,7 +1799,7 @@ mod tests { olm_machine .mark_request_as_sent( outgoing_requests[0].request_id(), - IncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ + AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({ "device_keys": { alice: {}, bob: {}, From d2ecd745f6dadfa5ebedc8e038fbdacce977e19b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 26 Nov 2024 17:25:34 +0100 Subject: [PATCH 628/979] chore(ui): Unify the logic for timeline item insertions This patch unifies the logic for inserting timeline items at `Start` and `End` positions. Both `TimelineItemPositions` can share the same implementation, making separate logic unnecessary. Previously, `End` included a duplicated events check as well, while `Start` did not, leading to inconsistency. The changes strictly involve moving and refactoring, with no functional modifications. --- .../src/timeline/event_handler.rs | 181 ++++++++++-------- 1 file changed, 102 insertions(+), 79 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index cc01448714b..ab0a6b52442 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -1085,88 +1085,71 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { self.items.push_back(item); } - Flow::Remote { position: TimelineItemPosition::Start { .. }, event_id, .. } => { - if self - .items - .iter() - .filter_map(|ev| ev.as_event()?.event_id()) - .any(|id| id == event_id) - { - trace!("Skipping back-paginated event that has already been seen"); - return; - } - - trace!("Adding new remote timeline item at the start"); - - let item = self.meta.new_timeline_item(item); - self.items.push_front(item); - } - Flow::Remote { - position: TimelineItemPosition::End { .. }, txn_id, event_id, .. + position: position @ TimelineItemPosition::Start { .. }, + txn_id, + event_id, + .. + } + | Flow::Remote { + position: position @ TimelineItemPosition::End { .. }, + txn_id, + event_id, + .. } => { - // Look if we already have a corresponding item somewhere, based on the - // transaction id (if a local echo) or the event id (if a - // duplicate remote event). - let result = rfind_event_item(self.items, |it| { - txn_id.is_some() && it.transaction_id() == txn_id.as_deref() - || it.event_id() == Some(event_id) - }); - - let mut removed_event_item_id = None; - - if let Some((idx, old_item)) = result { - if old_item.as_remote().is_some() { - // Item was previously received from the server. This should be very rare - // normally, but with the sliding- sync proxy, it is actually very - // common. - // NOTE: SS proxy workaround. - trace!(?item, old_item = ?*old_item, "Received duplicate event"); - - if old_item.content.is_redacted() && !item.content.is_redacted() { - warn!("Got original form of an event that was previously redacted"); - item.content = item.content.redact(&self.meta.room_version); - item.reactions.clear(); + // This block tries to find duplicated events. + + let removed_event_item_id = { + // Look if we already have a corresponding item somewhere, based on the + // transaction id (if this is a local echo) or the event id (if this is a + // duplicate remote event). + let result = rfind_event_item(self.items, |it| { + txn_id.is_some() && it.transaction_id() == txn_id.as_deref() + || it.event_id() == Some(event_id) + }); + + if let Some((idx, old_item)) = result { + if old_item.as_remote().is_some() { + // The item was previously received from the server. This should be very + // rare normally, but with the sliding- sync proxy, it is actually very + // common. + // NOTE: This is a SS proxy workaround. + trace!(?item, old_item = ?*old_item, "Received duplicate event"); + + if old_item.content.is_redacted() && !item.content.is_redacted() { + warn!("Got original form of an event that was previously redacted"); + item.content = item.content.redact(&self.meta.room_version); + item.reactions.clear(); + } } - } - // TODO: Check whether anything is different about the - // old and new item? + // TODO: Check whether anything is different about the + // old and new item? - transfer_details(&mut item, &old_item); + transfer_details(&mut item, &old_item); - let old_item_id = old_item.internal_id; + let old_item_id = old_item.internal_id; - if idx == self.items.len() - 1 { - // If the old item is the last one and no day divider - // changes need to happen, replace and return early. - trace!(idx, "Replacing existing event"); - self.items.set(idx, TimelineItem::new(item, old_item_id.to_owned())); - return; - } + if idx == self.items.len() - 1 { + // If the old item is the last one and no day divider + // changes need to happen, replace and return early. + trace!(idx, "Replacing existing event"); + self.items.set(idx, TimelineItem::new(item, old_item_id.to_owned())); + return; + } - // In more complex cases, remove the item before re-adding the item. - trace!("Removing local echo or duplicate timeline item"); - removed_event_item_id = Some(self.items.remove(idx).internal_id.clone()); + // In more complex cases, remove the item before re-adding the item. + trace!("Removing local echo or duplicate timeline item"); - // no return here, below code for adding a new event - // will run to re-add the removed item - } + // no return here, the below logic for adding a new event + // will run to re-add the removed item - // Local echoes that are pending should stick to the bottom, - // find the latest event that isn't that. - let latest_event_idx = self - .items - .iter() - .enumerate() - .rev() - .find_map(|(idx, item)| (!item.as_event()?.is_local_echo()).then_some(idx)); - - // Insert the next item after the latest event item that's not a - // pending local echo, or at the start if there is no such item. - let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); + Some(self.items.remove(idx).internal_id.clone()) + } else { + None + } + }; - trace!("Adding new remote timeline item after all non-pending events"); let new_item = match removed_event_item_id { // If a previous version of the same item (usually a local // echo) was removed and we now need to add it again, reuse @@ -1175,14 +1158,54 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { None => self.meta.new_timeline_item(item), }; - // Keep push semantics, if we're inserting at the front or the back. - if insert_idx == self.items.len() { - self.items.push_back(new_item); - } else if insert_idx == 0 { - self.items.push_front(new_item); - } else { - self.items.insert(insert_idx, new_item); - } + trace!("Adding new remote timeline item after all non-local events"); + + // We are about to insert the `new_item`, great! Though, we try to keep + // precise insertion semantics here, in this exact order: + // + // * _push back_ when the new item is inserted after all items, + // * _push front_ when the new item is inserted at index 0, + // * _insert_ otherwise. + // + // It means that the first inserted item will generate a _push back_ for + // example. + match position { + TimelineItemPosition::Start { .. } => { + trace!("Adding new remote timeline item at the front"); + self.items.push_front(new_item); + } + + TimelineItemPosition::End { .. } => { + // Local echoes that are pending should stick to the bottom, + // find the latest event that isn't that. + let latest_event_idx = + self.items.iter().enumerate().rev().find_map(|(idx, item)| { + (!item.as_event()?.is_local_echo()).then_some(idx) + }); + + // Insert the next item after the latest event item that's not a pending + // local echo, or at the start if there is no such item. + let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); + + // Let's prioritize push backs because it's the hot path. Events are more + // generally added at the back because they come from the sync most of the + // time. + if insert_idx == self.items.len() { + trace!("Adding new remote timeline item at the back"); + self.items.push_back(new_item); + } else if insert_idx == 0 { + trace!("Adding new remote timeline item at the front"); + self.items.push_front(new_item); + } else { + trace!(insert_idx, "Adding new remote timeline item at specific index"); + self.items.insert(insert_idx, new_item); + } + } + + p => unreachable!( + "An unexpected `TimelineItemPosition` has been received: {p:?}" + ), + }; } Flow::Remote { From 197da2c585961df9b97a2af18687945156e5a3f8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 28 Nov 2024 09:54:25 +0100 Subject: [PATCH 629/979] doc(timeline): tweak comments when inserting a new item A comment was duplicating (the first trace! that's removed here), and the second block comment only applied to new items, and was not as concise as it could be. --- .../src/timeline/event_handler.rs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index ab0a6b52442..0b878b89961 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -1158,17 +1158,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { None => self.meta.new_timeline_item(item), }; - trace!("Adding new remote timeline item after all non-local events"); - - // We are about to insert the `new_item`, great! Though, we try to keep - // precise insertion semantics here, in this exact order: - // - // * _push back_ when the new item is inserted after all items, - // * _push front_ when the new item is inserted at index 0, - // * _insert_ otherwise. - // - // It means that the first inserted item will generate a _push back_ for - // example. match position { TimelineItemPosition::Start { .. } => { trace!("Adding new remote timeline item at the front"); @@ -1187,9 +1176,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // local echo, or at the start if there is no such item. let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); - // Let's prioritize push backs because it's the hot path. Events are more - // generally added at the back because they come from the sync most of the - // time. + // Try to keep precise insertion semantics here, in this exact order: + // + // * _push back_ when the new item is inserted after all items (the + // assumption + // being that this is the hot path, because most of the time new events + // come from the sync), + // * _push front_ when the new item is inserted at index 0, + // * _insert_ otherwise. + if insert_idx == self.items.len() { trace!("Adding new remote timeline item at the back"); self.items.push_back(new_item); From 9bea0cff240352e880e406d500c00690f2e08f70 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 20 Nov 2024 17:30:06 +0100 Subject: [PATCH 630/979] feat(event cache): implement the sqlite backend for events --- .../src/event_cache/store/mod.rs | 4 + .../matrix-sdk-common/src/linked_chunk/mod.rs | 6 +- .../event_cache_store/003_events.sql | 46 +++ .../src/event_cache_store.rs | 265 +++++++++++++++++- 4 files changed, 311 insertions(+), 10 deletions(-) create mode 100644 crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql diff --git a/crates/matrix-sdk-base/src/event_cache/store/mod.rs b/crates/matrix-sdk-base/src/event_cache/store/mod.rs index d08218c5bd0..0162a7da27e 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/mod.rs @@ -138,6 +138,10 @@ pub enum EventCacheStoreError { #[error("Error encoding or decoding data from the event cache store: {0}")] Codec(#[from] Utf8Error), + /// The store failed to serialize or deserialize some data. + #[error("Error serializing or deserializing data from the event cache store: {0}")] + Serialization(#[from] serde_json::Error), + /// The database format has changed in a backwards incompatible way. #[error( "The database format of the event cache store changed in an incompatible way, \ diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index c9e9c4e7744..8131db3cec3 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -961,12 +961,12 @@ pub struct ChunkIdentifier(u64); impl ChunkIdentifier { /// Create a new [`ChunkIdentifier`]. - pub(super) fn new(identifier: u64) -> Self { + pub fn new(identifier: u64) -> Self { Self(identifier) } /// Get the underlying identifier. - fn index(&self) -> u64 { + pub fn index(&self) -> u64 { self.0 } } @@ -985,7 +985,7 @@ pub struct Position(ChunkIdentifier, usize); impl Position { /// Create a new [`Position`]. - pub(super) fn new(chunk_identifier: ChunkIdentifier, index: usize) -> Self { + pub fn new(chunk_identifier: ChunkIdentifier, index: usize) -> Self { Self(chunk_identifier, index) } diff --git a/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql b/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql new file mode 100644 index 00000000000..a372a842a66 --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql @@ -0,0 +1,46 @@ +CREATE TABLE "linked_chunks" ( + -- Identifier of the chunk, unique per room. + "id" INTEGER, + -- Which room does this chunk belong to? (hashed key shared with the two other tables) + "room_id" BLOB NOT NULL, + + -- Previous chunk in the linked list. + "previous" INTEGER, + -- Next chunk in the linked list. + "next" INTEGER, + -- Type of underlying entries: E for event, G for gaps + "type" TEXT CHECK("type" IN ('E', 'G')) NOT NULL +); + +CREATE UNIQUE INDEX "linked_chunks_id_and_room_id" ON linked_chunks (id, room_id); + +CREATE TABLE "gaps" ( + -- Which chunk does this gap refer to? + "chunk_id" INTEGER NOT NULL, + -- Which room does this event belong to? (hashed key shared with linked_chunks) + "room_id" BLOB NOT NULL, + + -- The previous batch token of a gap (encrypted value). + "prev_token" BLOB NOT NULL, + + -- If the owning chunk gets deleted, delete the entry too. + FOREIGN KEY(chunk_id, room_id) REFERENCES linked_chunks(id, room_id) ON DELETE CASCADE +); + +-- Items for an event chunk. +CREATE TABLE "events" ( + -- Which chunk does this event refer to? + "chunk_id" INTEGER NOT NULL, + -- Which room does this event belong to? (hashed key shared with linked_chunks) + "room_id" BLOB NOT NULL, + + -- `OwnedEventId` for events, can be null if malformed. + "event_id" TEXT, + -- JSON serialized `SyncTimelineEvent` (encrypted value). + "content" BLOB NOT NULL, + -- Position (index) in the chunk. + "position" INTEGER NOT NULL, + + -- If the owning chunk gets deleted, delete the entry too. + FOREIGN KEY(chunk_id, room_id) REFERENCES linked_chunks(id, room_id) ON DELETE CASCADE +); diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 5ee8d92324e..c7f1c1084d6 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -4,14 +4,14 @@ use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ event_cache::{store::EventCacheStore, Event, Gap}, - linked_chunk::Update, + linked_chunk::{ChunkIdentifier, Update}, media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; use ruma::{MilliSecondsSinceUnixEpoch, RoomId}; -use rusqlite::OptionalExtension; +use rusqlite::{OptionalExtension, Transaction}; use tokio::fs; -use tracing::debug; +use tracing::{debug, trace}; use crate::{ error::{Error, Result}, @@ -21,6 +21,7 @@ use crate::{ mod keys { // Tables + pub const LINKED_CHUNKS: &str = "linked_chunks"; pub const MEDIA: &str = "media"; } @@ -29,7 +30,7 @@ mod keys { /// This is used to figure whether the SQLite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in /// the [`run_migrations`] function. -const DATABASE_VERSION: u8 = 2; +const DATABASE_VERSION: u8 = 3; /// A SQLite-based event cache store. #[derive(Clone)] @@ -143,6 +144,14 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { .await?; } + if version < 3 { + conn.with_transaction(|txn| { + txn.execute_batch(include_str!("../migrations/event_cache_store/003_events.sql"))?; + txn.set_db_version(3) + }) + .await?; + } + Ok(()) } @@ -185,10 +194,208 @@ impl EventCacheStore for SqliteEventCacheStore { async fn handle_linked_chunk_updates( &self, - _room_id: &RoomId, - _updates: Vec>, + room_id: &RoomId, + updates: Vec>, ) -> Result<(), Self::Error> { - todo!() + let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, room_id); + + for up in updates { + match up { + Update::NewItemsChunk { previous, new, next } => { + let hashed_room_id = hashed_room_id.clone(); + + let previous = previous.as_ref().map(ChunkIdentifier::index); + let new = new.index(); + let next = next.as_ref().map(ChunkIdentifier::index); + + trace!( + %room_id, + "new events chunk (prev={previous:?}, i={new}, next={next:?})", + ); + + self.acquire() + .await? + .with_transaction(move |txn| { + insert_chunk(txn, &hashed_room_id, previous, new, next, "E") + }) + .await?; + } + + Update::NewGapChunk { previous, new, next, gap } => { + let hashed_room_id = hashed_room_id.clone(); + + let serialized = serde_json::to_vec(&gap.prev_token)?; + let prev_token = self.encode_value(serialized)?; + + let previous = previous.as_ref().map(ChunkIdentifier::index); + let new = new.index(); + let next = next.as_ref().map(ChunkIdentifier::index); + + trace!( + %room_id, + "new gap chunk (prev={previous:?}, i={new}, next={next:?})", + ); + + self.acquire() + .await? + .with_transaction(move |txn| -> rusqlite::Result<()> { + // Insert the chunk as a gap. + insert_chunk(txn, &hashed_room_id, previous, new, next, "G")?; + + // Insert the gap's value. + // XXX(bnjbvr): might as well inline in the linked_chunks table? better + // for flexibility to use another table though. + txn.execute( + r#" + INSERT INTO gaps(chunk_id, room_id, prev_token) + VALUES (?, ?, ?) + "#, + (new, hashed_room_id, prev_token), + )?; + + Ok(()) + }) + .await?; + } + + Update::RemoveChunk(chunk_identifier) => { + let hashed_room_id = hashed_room_id.clone(); + let chunk_id = chunk_identifier.index(); + + trace!(%room_id, "removing chunk @ {chunk_id}"); + + self.acquire() + .await? + .with_transaction(move |txn| -> rusqlite::Result<()> { + // Find chunk to delete. + let (previous, next): (Option, Option) = txn.query_row( + "SELECT previous, next FROM linked_chunks WHERE id = ? AND room_id = ?", + (chunk_id, &hashed_room_id), + |row| Ok((row.get(0)?, row.get(1)?)) + )?; + + // Replace its previous' next to its own next. + if let Some(previous) = previous { + txn.execute("UPDATE linked_chunks SET next = ? WHERE id = ? AND room_id = ?", (next, previous, &hashed_room_id))?; + } + + // Replace its next' previous to its own previous. + if let Some(next) = next { + txn.execute("UPDATE linked_chunks SET previous = ? WHERE id = ? AND room_id = ?", (previous, next, &hashed_room_id))?; + } + + // Now delete it, and let cascading delete corresponding entries in the + // other data tables. + txn.execute("DELETE FROM linked_chunks WHERE id = ? AND room_id = ?", (chunk_id, hashed_room_id))?; + + Ok(()) + }) + .await?; + } + + Update::PushItems { at, items } => { + let chunk_id = at.chunk_identifier().index(); + let hashed_room_id = hashed_room_id.clone(); + + trace!(%room_id, "pushing items @ {chunk_id}"); + + let this = self.clone(); + + self.acquire() + .await? + .with_transaction(move |txn| -> Result<(), Self::Error> { + for (i, event) in items.into_iter().enumerate() { + let serialized = serde_json::to_vec(&event)?; + let content = this.encode_value(serialized)?; + + let event_id = + event.event_id().map(|event_id| event_id.to_string()); + let index = at.index() + i; + + txn.execute( + r#" + INSERT INTO events(chunk_id, room_id, event_id, content, position) + VALUES (?, ?, ?, ?, ?) + "#, + (chunk_id, &hashed_room_id, event_id, content, index), + )?; + } + + Ok(()) + }) + .await?; + } + + Update::RemoveItem { at } => { + let hashed_room_id = hashed_room_id.clone(); + let chunk_id = at.chunk_identifier().index(); + let index = at.index(); + + trace!(%room_id, "removing item @ {chunk_id}:{index}"); + + self.acquire() + .await? + .with_transaction(move |txn| -> rusqlite::Result<()> { + // Remove the entry. + txn.execute("DELETE FROM events WHERE room_id = ? AND chunk_id = ? AND position = ?", (&hashed_room_id, chunk_id, index))?; + + // Decrement the index of each item after the one we're going to + // remove. + txn.execute( + r#" + UPDATE events + SET position = position - 1 + WHERE room_id = ? AND chunk_id = ? AND position > ? + "#, + (&hashed_room_id, chunk_id, index) + )?; + + Ok(()) + }) + .await?; + } + + Update::DetachLastItems { at } => { + let hashed_room_id = hashed_room_id.clone(); + let chunk_id = at.chunk_identifier().index(); + let index = at.index(); + + trace!(%room_id, "truncating items >= {chunk_id}:{index}"); + + self.acquire() + .await? + .with_transaction(move |txn| -> rusqlite::Result<()> { + // Remove these entries. + txn.execute("DELETE FROM events WHERE room_id = ? AND chunk_id = ? AND position >= ?", (&hashed_room_id, chunk_id, index))?; + Ok(()) + }) + .await?; + } + + Update::Clear => { + let hashed_room_id = hashed_room_id.clone(); + + trace!(%room_id, "clearing items"); + + self.acquire() + .await? + .with_transaction(move |txn| { + // Remove chunks, and let cascading do its job. + txn.execute( + "DELETE FROM linked_chunks WHERE room_id = ?", + (&hashed_room_id,), + ) + }) + .await?; + } + + Update::StartReattachItems | Update::EndReattachItems => { + // Nothing. + } + } + } + + Ok(()) } async fn add_media_content( @@ -280,6 +487,50 @@ impl EventCacheStore for SqliteEventCacheStore { } } +fn insert_chunk( + txn: &Transaction<'_>, + room_id: &Key, + previous: Option, + new: u64, + next: Option, + type_str: &str, +) -> rusqlite::Result<()> { + // First, insert the new chunk. + txn.execute( + r#" + INSERT INTO linked_chunks(id, room_id, previous, next, type) + VALUES (?, ?, ?, ?, ?) + "#, + (new, room_id, previous, next, type_str), + )?; + + // If this chunk has a previous one, update its `next` field. + if let Some(previous) = previous { + txn.execute( + r#" + UPDATE linked_chunks + SET next = ? + WHERE id = ? AND room_id = ? + "#, + (new, previous, room_id), + )?; + } + + // If this chunk has a next one, update its `previous` field. + if let Some(next) = next { + txn.execute( + r#" + UPDATE linked_chunks + SET previous = ? + WHERE id = ? AND room_id = ? + "#, + (new, next, room_id), + )?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use std::{ From e57d38cf571d1ad1406d1413ace0de75500faf4a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 27 Nov 2024 13:30:02 +0100 Subject: [PATCH 631/979] doc(common): add a note that a decrypted raw event always has a room id --- crates/matrix-sdk-common/src/deserialized_responses.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 65dcb153196..2eed248ac36 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -587,6 +587,11 @@ impl fmt::Debug for TimelineEventKind { /// A successfully-decrypted encrypted event. pub struct DecryptedRoomEvent { /// The decrypted event. + /// + /// Note: it's not an error that this contains an `AnyMessageLikeEvent`: an + /// encrypted payload *always contains* a room id, by the [spec]. + /// + /// [spec]: https://spec.matrix.org/v1.12/client-server-api/#mmegolmv1aes-sha2 pub event: Raw, /// The encryption info about the event. From c6ba71ae33fdf531b4cba6850f0741a3e8396d53 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 27 Nov 2024 13:33:16 +0100 Subject: [PATCH 632/979] feat(event cache): allow reloading from the store, and test functionalities This required adding support for *reading* out of the event cache, for the sqlite backend. This paves the way for the next PR (reload from the cache), and it should also help with testing at the `EventCacheStore` trait layer some day. --- crates/matrix-sdk-sqlite/src/error.rs | 3 + .../src/event_cache_store.rs | 623 +++++++++++++++++- 2 files changed, 622 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-sqlite/src/error.rs b/crates/matrix-sdk-sqlite/src/error.rs index ddfac2fbfec..3c59a1f555a 100644 --- a/crates/matrix-sdk-sqlite/src/error.rs +++ b/crates/matrix-sdk-sqlite/src/error.rs @@ -104,6 +104,9 @@ pub enum Error { #[error("An update keyed by unique ID touched more than one entry")] InconsistentUpdate, + + #[error("The store contains invalid data: {details}")] + InvalidData { details: String }, } macro_rules! impl_from { diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index c7f1c1084d6..92a5602759c 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -1,10 +1,12 @@ +#![allow(dead_code)] // Most of the unused code may be used soonish. + use std::{borrow::Cow, fmt, path::Path, sync::Arc}; use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ event_cache::{store::EventCacheStore, Event, Gap}, - linked_chunk::{ChunkIdentifier, Update}, + linked_chunk::{ChunkContent, ChunkIdentifier, Update}, media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; @@ -107,6 +109,193 @@ impl SqliteEventCacheStore { async fn acquire(&self) -> Result { Ok(self.pool.get().await?) } + + fn map_row_to_chunk( + row: &rusqlite::Row<'_>, + ) -> Result<(u64, Option, Option, String), rusqlite::Error> { + Ok(( + row.get::<_, u64>(0)?, + row.get::<_, Option>(1)?, + row.get::<_, Option>(2)?, + row.get::<_, String>(3)?, + )) + } + + async fn load_chunks(&self, room_id: &RoomId) -> Result> { + let room_id = room_id.to_owned(); + let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, &room_id); + + let this = self.clone(); + + let result = self + .acquire() + .await? + .with_transaction(move |txn| -> Result<_> { + let mut items = Vec::new(); + + for data in txn + .prepare( + "SELECT id, previous, next, type FROM linked_chunks WHERE room_id = ? ORDER BY id", + )? + .query_map((&hashed_room_id,), Self::map_row_to_chunk)? + { + let (id, previous, next, chunk_type) = data?; + let new = txn.rebuild_chunk( + &this, + &hashed_room_id, + previous, + id, + next, + chunk_type.as_str(), + )?; + items.push(new); + } + + Ok(items) + }) + .await?; + + Ok(result) + } + + async fn load_chunk_with_id( + &self, + room_id: &RoomId, + chunk_id: ChunkIdentifier, + ) -> Result { + let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, room_id); + + let this = self.clone(); + + self + .acquire() + .await? + .with_transaction(move |txn| -> Result<_> { + let (id, previous, next, chunk_type) = txn.query_row( + "SELECT id, previous, next, type FROM linked_chunks WHERE room_id = ? AND chunk_id = ?", + (&hashed_room_id, chunk_id.index()), + Self::map_row_to_chunk + )?; + txn.rebuild_chunk(&this, &hashed_room_id, previous, id, next, chunk_type.as_str()) + }).await + } +} + +trait TransactionExtForLinkedChunks { + fn rebuild_chunk( + &self, + store: &SqliteEventCacheStore, + room_id: &Key, + previous: Option, + index: u64, + next: Option, + chunk_type: &str, + ) -> Result; + + fn load_gap_content( + &self, + store: &SqliteEventCacheStore, + room_id: &Key, + chunk_id: ChunkIdentifier, + ) -> Result; + + fn load_events_content( + &self, + store: &SqliteEventCacheStore, + room_id: &Key, + chunk_id: ChunkIdentifier, + ) -> Result>; +} + +impl TransactionExtForLinkedChunks for Transaction<'_> { + fn rebuild_chunk( + &self, + store: &SqliteEventCacheStore, + room_id: &Key, + previous: Option, + id: u64, + next: Option, + chunk_type: &str, + ) -> Result { + let previous = previous.map(ChunkIdentifier::new); + let next = next.map(ChunkIdentifier::new); + let id = ChunkIdentifier::new(id); + + match chunk_type { + "G" => { + // It's a gap! There's at most one row for it in the database, so a + // call to `query_row` is sufficient. + let gap = self.load_gap_content(store, room_id, id)?; + Ok(RawLinkedChunk { content: ChunkContent::Gap(gap), previous, id, next }) + } + + "E" => { + // It's events! + let events = self.load_events_content(store, room_id, id)?; + Ok(RawLinkedChunk { content: ChunkContent::Items(events), previous, id, next }) + } + + other => { + // It's an error! + Err(Error::InvalidData { + details: format!("a linked chunk has an unknown type {other}"), + }) + } + } + } + + fn load_gap_content( + &self, + store: &SqliteEventCacheStore, + room_id: &Key, + chunk_id: ChunkIdentifier, + ) -> Result { + let encoded_prev_token: Vec = self.query_row( + "SELECT prev_token FROM gaps WHERE chunk_id = ? AND room_id = ?", + (chunk_id.index(), &room_id), + |row| row.get(0), + )?; + let prev_token_bytes = store.decode_value(&encoded_prev_token)?; + let prev_token = serde_json::from_slice(&prev_token_bytes)?; + Ok(Gap { prev_token }) + } + + fn load_events_content( + &self, + store: &SqliteEventCacheStore, + room_id: &Key, + chunk_id: ChunkIdentifier, + ) -> Result> { + // Retrieve all the events from the database. + let mut events = Vec::new(); + + for event_data in self + .prepare( + r#" + SELECT content FROM events + WHERE chunk_id = ? AND room_id = ? + ORDER BY position ASC + "#, + )? + .query_map((chunk_id.index(), &room_id), |row| row.get::<_, Vec>(0))? + { + let encoded_content = event_data?; + let serialized_content = store.decode_value(&encoded_content)?; + let sync_timeline_event = serde_json::from_slice(&serialized_content)?; + + events.push(sync_timeline_event); + } + + Ok(events) + } +} + +struct RawLinkedChunk { + content: ChunkContent, + + previous: Option, + id: ChunkIdentifier, + next: Option, } async fn create_pool(path: &Path) -> Result { @@ -538,14 +727,23 @@ mod tests { time::Duration, }; + use assert_matches::assert_matches; use matrix_sdk_base::{ - event_cache::store::{EventCacheStore, EventCacheStoreError}, + deserialized_responses::{ + AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, + TimelineEventKind, VerificationState, + }, + event_cache::{ + store::{EventCacheStore, EventCacheStoreError}, + Gap, + }, event_cache_store_integration_tests, event_cache_store_integration_tests_time, + linked_chunk::{ChunkContent, ChunkIdentifier, Position, Update}, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, }; - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use once_cell::sync::Lazy; - use ruma::{events::room::MediaSource, media::Method, mxc_uri, uint}; + use ruma::{events::room::MediaSource, media::Method, mxc_uri, push::Action, uint}; use tempfile::{tempdir, TempDir}; use super::SqliteEventCacheStore; @@ -640,6 +838,423 @@ mod tests { assert_eq!(contents[0], content, "file is not last access"); assert_eq!(contents[1], thumbnail_content, "thumbnail is not second-to-last access"); } + + #[async_test] + async fn test_linked_chunk_new_items_chunk() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = &DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, // Note: the store must link the next entry itself. + }, + Update::NewItemsChunk { + previous: Some(ChunkIdentifier::new(42)), + new: ChunkIdentifier::new(13), + next: Some(ChunkIdentifier::new(37)), /* But it's fine to explicitly pass + * the next link ahead of time. */ + }, + Update::NewItemsChunk { + previous: Some(ChunkIdentifier::new(13)), + new: ChunkIdentifier::new(37), + next: None, + }, + ], + ) + .await + .unwrap(); + + let mut chunks = store.load_chunks(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 3); + + { + // Chunks are ordered from smaller to bigger IDs. + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(13)); + assert_eq!(c.previous, Some(ChunkIdentifier::new(42))); + assert_eq!(c.next, Some(ChunkIdentifier::new(37))); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert!(events.is_empty()); + }); + + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(37)); + assert_eq!(c.previous, Some(ChunkIdentifier::new(13))); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert!(events.is_empty()); + }); + + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, Some(ChunkIdentifier::new(13))); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert!(events.is_empty()); + }); + } + } + + #[async_test] + async fn test_linked_chunk_new_gap_chunk() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = &DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![Update::NewGapChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + gap: Gap { prev_token: "raclette".to_owned() }, + }], + ) + .await + .unwrap(); + + let mut chunks = store.load_chunks(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 1); + + // Chunks are ordered from smaller to bigger IDs. + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Gap(gap) => { + assert_eq!(gap.prev_token, "raclette"); + }); + } + + #[async_test] + async fn test_linked_chunk_remove_chunk() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = &DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewGapChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + gap: Gap { prev_token: "raclette".to_owned() }, + }, + Update::RemoveChunk(ChunkIdentifier::new(42)), + ], + ) + .await + .unwrap(); + + let chunks = store.load_chunks(room_id).await.unwrap(); + assert!(chunks.is_empty()); + + // Check that cascading worked. Yes, sqlite, I doubt you. + let num_gaps: u64 = store + .acquire() + .await + .unwrap() + .with_transaction(|txn| { + txn.query_row("SELECT COUNT(*) FROM gaps", (), |row| row.get(0)) + }) + .await + .unwrap(); + + assert_eq!(num_gaps, 0); + } + + fn make_test_event(content: &str) -> SyncTimelineEvent { + let encryption_info = EncryptionInfo { + sender: (*ALICE).into(), + sender_device: None, + algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "1337".to_owned(), + sender_claimed_keys: Default::default(), + }, + verification_state: VerificationState::Verified, + }; + + let event = EventFactory::new() + .text_msg(content) + .room(*DEFAULT_TEST_ROOM_ID) + .sender(*ALICE) + .into_raw_timeline() + .cast(); + + SyncTimelineEvent { + kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { + event, + encryption_info, + unsigned_encryption_info: None, + }), + push_actions: vec![Action::Notify], + } + } + + #[track_caller] + fn check_event(event: &SyncTimelineEvent, text: &str) { + // Check push actions. + let actions = &event.push_actions; + assert_eq!(actions.len(), 1); + assert_matches!(&actions[0], Action::Notify); + + // Check content. + assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => { + // Check encryption fields. + assert_eq!(d.encryption_info.sender, *ALICE); + assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => { + assert_eq!(curve25519_key, "1337"); + }); + + // Check event. + let deserialized = d.event.deserialize().unwrap(); + assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => { + assert_eq!(msg.as_original().unwrap().content.body(), text); + }); + }); + } + + #[async_test] + async fn test_linked_chunk_push_items() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = &DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![make_test_event("hello"), make_test_event("world")], + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 2), + items: vec![make_test_event("who?")], + }, + ], + ) + .await + .unwrap(); + + let mut chunks = store.load_chunks(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 1); + + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert_eq!(events.len(), 3); + + check_event(&events[0], "hello"); + check_event(&events[1], "world"); + check_event(&events[2], "who?"); + }); + } + + #[async_test] + async fn test_linked_chunk_remove_item() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = *DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![make_test_event("hello"), make_test_event("world")], + }, + Update::RemoveItem { at: Position::new(ChunkIdentifier::new(42), 0) }, + ], + ) + .await + .unwrap(); + + let mut chunks = store.load_chunks(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 1); + + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert_eq!(events.len(), 1); + check_event(&events[0], "world"); + }); + + // Make sure the position has been updated for the remaining event. + let num_rows: u64 = store + .acquire() + .await + .unwrap() + .with_transaction(move |txn| { + txn.query_row( + "SELECT COUNT(*) FROM events WHERE chunk_id = 42 AND room_id = ? AND position = 0", + (room_id.as_bytes(),), + |row| row.get(0), + ) + }) + .await + .unwrap(); + assert_eq!(num_rows, 1); + } + + #[async_test] + async fn test_linked_chunk_detach_last_items() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = *DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![ + make_test_event("hello"), + make_test_event("world"), + make_test_event("howdy"), + ], + }, + Update::DetachLastItems { at: Position::new(ChunkIdentifier::new(42), 1) }, + ], + ) + .await + .unwrap(); + + let mut chunks = store.load_chunks(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 1); + + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert_eq!(events.len(), 1); + check_event(&events[0], "hello"); + }); + } + + #[async_test] + async fn test_linked_chunk_start_end_reattach_items() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = *DEFAULT_TEST_ROOM_ID; + + // Same updates and checks as test_linked_chunk_push_items, but with extra + // `StartReattachItems` and `EndReattachItems` updates, which must have no + // effects. + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![ + make_test_event("hello"), + make_test_event("world"), + make_test_event("howdy"), + ], + }, + Update::StartReattachItems, + Update::EndReattachItems, + ], + ) + .await + .unwrap(); + + let mut chunks = store.load_chunks(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 1); + + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert_eq!(events.len(), 3); + check_event(&events[0], "hello"); + check_event(&events[1], "world"); + check_event(&events[2], "howdy"); + }); + } + + #[async_test] + async fn test_linked_chunk_clear() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = *DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::NewGapChunk { + previous: Some(ChunkIdentifier::new(42)), + new: ChunkIdentifier::new(54), + next: None, + gap: Gap { prev_token: "fondue".to_owned() }, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![ + make_test_event("hello"), + make_test_event("world"), + make_test_event("howdy"), + ], + }, + Update::Clear, + ], + ) + .await + .unwrap(); + + let chunks = store.load_chunks(room_id).await.unwrap(); + assert!(chunks.is_empty()); + } } #[cfg(test)] From ce95b6089f97bfb85ac3f930d5fcd300e7b088b0 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 27 Nov 2024 13:36:39 +0100 Subject: [PATCH 633/979] doc(event cache): add the copyright notice and basic module doc comment --- .../matrix-sdk-sqlite/src/event_cache_store.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 92a5602759c..75179889dbe 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -1,3 +1,19 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A sqlite-based backend for the [`EventCacheStore`]. + #![allow(dead_code)] // Most of the unused code may be used soonish. use std::{borrow::Cow, fmt, path::Path, sync::Arc}; From 9ed65bc3219a899ea94af716a67eb838feabf212 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 28 Nov 2024 10:17:24 +0100 Subject: [PATCH 634/979] task(event cache): address review points --- .../event_cache_store/003_events.sql | 12 +-- .../src/event_cache_store.rs | 83 ++++++++++++++++--- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql b/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql index a372a842a66..1de6495cc0d 100644 --- a/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql +++ b/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql @@ -1,21 +1,21 @@ CREATE TABLE "linked_chunks" ( - -- Identifier of the chunk, unique per room. + -- Identifier of the chunk, unique per room. Corresponds to a `ChunkIdentifier`. "id" INTEGER, -- Which room does this chunk belong to? (hashed key shared with the two other tables) "room_id" BLOB NOT NULL, - -- Previous chunk in the linked list. + -- Previous chunk in the linked list. Corresponds to a `ChunkIdentifier`. "previous" INTEGER, - -- Next chunk in the linked list. + -- Next chunk in the linked list. Corresponds to a `ChunkIdentifier`. "next" INTEGER, - -- Type of underlying entries: E for event, G for gaps + -- Type of underlying entries: E for events, G for gaps "type" TEXT CHECK("type" IN ('E', 'G')) NOT NULL ); CREATE UNIQUE INDEX "linked_chunks_id_and_room_id" ON linked_chunks (id, room_id); CREATE TABLE "gaps" ( - -- Which chunk does this gap refer to? + -- Which chunk does this gap refer to? Corresponds to a `ChunkIdentifier`. "chunk_id" INTEGER NOT NULL, -- Which room does this event belong to? (hashed key shared with linked_chunks) "room_id" BLOB NOT NULL, @@ -29,7 +29,7 @@ CREATE TABLE "gaps" ( -- Items for an event chunk. CREATE TABLE "events" ( - -- Which chunk does this event refer to? + -- Which chunk does this event refer to? Corresponds to a `ChunkIdentifier`. "chunk_id" INTEGER NOT NULL, -- Which room does this event belong to? (hashed key shared with linked_chunks) "room_id" BLOB NOT NULL, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 75179889dbe..baea282e727 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -50,6 +50,13 @@ mod keys { /// the [`run_migrations`] function. const DATABASE_VERSION: u8 = 3; +/// The string used to identify a chunk of type events, in the `type` field in +/// the database. +const CHUNK_TYPE_EVENT_TYPE_STRING: &str = "E"; +/// The string used to identify a chunk of type gap, in the `type` field in the +/// database. +const CHUNK_TYPE_GAP_TYPE_STRING: &str = "G"; + /// A SQLite-based event cache store. #[derive(Clone)] pub struct SqliteEventCacheStore { @@ -149,6 +156,7 @@ impl SqliteEventCacheStore { .with_transaction(move |txn| -> Result<_> { let mut items = Vec::new(); + // Use `ORDER BY id` to get a deterministic ordering for testing purposes. for data in txn .prepare( "SELECT id, previous, next, type FROM linked_chunks WHERE room_id = ? ORDER BY id", @@ -238,14 +246,14 @@ impl TransactionExtForLinkedChunks for Transaction<'_> { let id = ChunkIdentifier::new(id); match chunk_type { - "G" => { + CHUNK_TYPE_GAP_TYPE_STRING => { // It's a gap! There's at most one row for it in the database, so a // call to `query_row` is sufficient. let gap = self.load_gap_content(store, room_id, id)?; Ok(RawLinkedChunk { content: ChunkContent::Gap(gap), previous, id, next }) } - "E" => { + CHUNK_TYPE_EVENT_TYPE_STRING => { // It's events! let events = self.load_events_content(store, room_id, id)?; Ok(RawLinkedChunk { content: ChunkContent::Items(events), previous, id, next }) @@ -421,7 +429,14 @@ impl EventCacheStore for SqliteEventCacheStore { self.acquire() .await? .with_transaction(move |txn| { - insert_chunk(txn, &hashed_room_id, previous, new, next, "E") + insert_chunk( + txn, + &hashed_room_id, + previous, + new, + next, + CHUNK_TYPE_EVENT_TYPE_STRING, + ) }) .await?; } @@ -445,11 +460,16 @@ impl EventCacheStore for SqliteEventCacheStore { .await? .with_transaction(move |txn| -> rusqlite::Result<()> { // Insert the chunk as a gap. - insert_chunk(txn, &hashed_room_id, previous, new, next, "G")?; + insert_chunk( + txn, + &hashed_room_id, + previous, + new, + next, + CHUNK_TYPE_GAP_TYPE_STRING, + )?; // Insert the gap's value. - // XXX(bnjbvr): might as well inline in the linked_chunks table? better - // for flexibility to use another table though. txn.execute( r#" INSERT INTO gaps(chunk_id, room_id, prev_token) @@ -967,27 +987,64 @@ mod tests { next: None, gap: Gap { prev_token: "raclette".to_owned() }, }, - Update::RemoveChunk(ChunkIdentifier::new(42)), + Update::NewGapChunk { + previous: Some(ChunkIdentifier::new(42)), + new: ChunkIdentifier::new(43), + next: None, + gap: Gap { prev_token: "fondue".to_owned() }, + }, + Update::NewGapChunk { + previous: Some(ChunkIdentifier::new(43)), + new: ChunkIdentifier::new(44), + next: None, + gap: Gap { prev_token: "tartiflette".to_owned() }, + }, + Update::RemoveChunk(ChunkIdentifier::new(43)), ], ) .await .unwrap(); - let chunks = store.load_chunks(room_id).await.unwrap(); - assert!(chunks.is_empty()); + let mut chunks = store.load_chunks(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 2); + + // Chunks are ordered from smaller to bigger IDs. + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, Some(ChunkIdentifier::new(44))); + assert_matches!(c.content, ChunkContent::Gap(gap) => { + assert_eq!(gap.prev_token, "raclette"); + }); + + let c = chunks.remove(0); + assert_eq!(c.id, ChunkIdentifier::new(44)); + assert_eq!(c.previous, Some(ChunkIdentifier::new(42))); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Gap(gap) => { + assert_eq!(gap.prev_token, "tartiflette"); + }); // Check that cascading worked. Yes, sqlite, I doubt you. - let num_gaps: u64 = store + let gaps = store .acquire() .await .unwrap() - .with_transaction(|txn| { - txn.query_row("SELECT COUNT(*) FROM gaps", (), |row| row.get(0)) + .with_transaction(|txn| -> rusqlite::Result<_> { + let mut gaps = Vec::new(); + for data in txn + .prepare("SELECT chunk_id FROM gaps ORDER BY chunk_id")? + .query_map((), |row| row.get::<_, u64>(0))? + { + gaps.push(data?); + } + Ok(gaps) }) .await .unwrap(); - assert_eq!(num_gaps, 0); + assert_eq!(gaps, vec![42, 44]); } fn make_test_event(content: &str) -> SyncTimelineEvent { From aa0eb760de055133ab6c370fd89b2c656d6e993d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 28 Nov 2024 10:38:42 +0100 Subject: [PATCH 635/979] test(event cache): add a test for reading events from multiple rooms This was to make sure that we can search by blob. --- .../src/event_cache_store.rs | 109 +++++++++++++++--- 1 file changed, 94 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index baea282e727..4b2c6cd263e 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -779,7 +779,9 @@ mod tests { }; use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use once_cell::sync::Lazy; - use ruma::{events::room::MediaSource, media::Method, mxc_uri, push::Action, uint}; + use ruma::{ + events::room::MediaSource, media::Method, mxc_uri, push::Action, room_id, uint, RoomId, + }; use tempfile::{tempdir, TempDir}; use super::SqliteEventCacheStore; @@ -1047,7 +1049,7 @@ mod tests { assert_eq!(gaps, vec![42, 44]); } - fn make_test_event(content: &str) -> SyncTimelineEvent { + fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent { let encryption_info = EncryptionInfo { sender: (*ALICE).into(), sender_device: None, @@ -1060,7 +1062,7 @@ mod tests { let event = EventFactory::new() .text_msg(content) - .room(*DEFAULT_TEST_ROOM_ID) + .room(room_id) .sender(*ALICE) .into_raw_timeline() .cast(); @@ -1115,11 +1117,14 @@ mod tests { }, Update::PushItems { at: Position::new(ChunkIdentifier::new(42), 0), - items: vec![make_test_event("hello"), make_test_event("world")], + items: vec![ + make_test_event(room_id, "hello"), + make_test_event(room_id, "world"), + ], }, Update::PushItems { at: Position::new(ChunkIdentifier::new(42), 2), - items: vec![make_test_event("who?")], + items: vec![make_test_event(room_id, "who?")], }, ], ) @@ -1160,7 +1165,10 @@ mod tests { }, Update::PushItems { at: Position::new(ChunkIdentifier::new(42), 0), - items: vec![make_test_event("hello"), make_test_event("world")], + items: vec![ + make_test_event(room_id, "hello"), + make_test_event(room_id, "world"), + ], }, Update::RemoveItem { at: Position::new(ChunkIdentifier::new(42), 0) }, ], @@ -1216,9 +1224,9 @@ mod tests { Update::PushItems { at: Position::new(ChunkIdentifier::new(42), 0), items: vec![ - make_test_event("hello"), - make_test_event("world"), - make_test_event("howdy"), + make_test_event(room_id, "hello"), + make_test_event(room_id, "world"), + make_test_event(room_id, "howdy"), ], }, Update::DetachLastItems { at: Position::new(ChunkIdentifier::new(42), 1) }, @@ -1262,9 +1270,9 @@ mod tests { Update::PushItems { at: Position::new(ChunkIdentifier::new(42), 0), items: vec![ - make_test_event("hello"), - make_test_event("world"), - make_test_event("howdy"), + make_test_event(room_id, "hello"), + make_test_event(room_id, "world"), + make_test_event(room_id, "howdy"), ], }, Update::StartReattachItems, @@ -1314,9 +1322,9 @@ mod tests { Update::PushItems { at: Position::new(ChunkIdentifier::new(42), 0), items: vec![ - make_test_event("hello"), - make_test_event("world"), - make_test_event("howdy"), + make_test_event(room_id, "hello"), + make_test_event(room_id, "world"), + make_test_event(room_id, "howdy"), ], }, Update::Clear, @@ -1328,6 +1336,77 @@ mod tests { let chunks = store.load_chunks(room_id).await.unwrap(); assert!(chunks.is_empty()); } + + #[async_test] + async fn test_linked_chunk_multiple_rooms() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room1 = room_id!("!realcheeselovers:raclette.fr"); + let room2 = room_id!("!realcheeselovers:fondue.ch"); + + // Check that applying updates to one room doesn't affect the others. + // Use the same chunk identifier in both rooms to battle-test search. + + store + .handle_linked_chunk_updates( + room1, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![ + make_test_event(room1, "best cheese is raclette"), + make_test_event(room1, "obviously"), + ], + }, + ], + ) + .await + .unwrap(); + + store + .handle_linked_chunk_updates( + room2, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![make_test_event(room1, "beaufort is the best")], + }, + ], + ) + .await + .unwrap(); + + // Check chunks from room 1. + let mut chunks_room1 = store.load_chunks(room1).await.unwrap(); + assert_eq!(chunks_room1.len(), 1); + + let c = chunks_room1.remove(0); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert_eq!(events.len(), 2); + check_event(&events[0], "best cheese is raclette"); + check_event(&events[1], "obviously"); + }); + + // Check chunks from room 2. + let mut chunks_room2 = store.load_chunks(room2).await.unwrap(); + assert_eq!(chunks_room2.len(), 1); + + let c = chunks_room2.remove(0); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert_eq!(events.len(), 1); + check_event(&events[0], "beaufort is the best"); + }); + } } #[cfg(test)] From daa984f7ded708d0e1a97783552c019f2b3f6087 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 28 Nov 2024 11:06:02 +0100 Subject: [PATCH 636/979] feat(event cache store): enable foreign keys pragma \o/ --- crates/matrix-sdk-sqlite/src/event_cache_store.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 4b2c6cd263e..d22d8f21eda 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -358,6 +358,9 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { } if version < 3 { + // Enable foreign keys for this database. + conn.execute_batch("PRAGMA foreign_keys = ON;").await?; + conn.with_transaction(|txn| { txn.execute_batch(include_str!("../migrations/event_cache_store/003_events.sql"))?; txn.set_db_version(3) From e1f0037fd5eb3b263a471a0c515daf6bf95cd784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 28 Nov 2024 11:38:14 +0100 Subject: [PATCH 637/979] chore: Define the lifetime of some const strings explicitly --- crates/matrix-sdk-base/src/deserialized_responses.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index b8f0f81701a..183a02da531 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -160,12 +160,12 @@ impl PartialEq for DisplayName { impl DisplayName { /// Regex pattern matching an MXID. - const MXID_PATTERN: &str = "@.+[:.].+"; + const MXID_PATTERN: &'static str = "@.+[:.].+"; /// Regex pattern matching some left-to-right formatting marks: /// * LTR and RTL marks U+200E and U+200F /// * LTR/RTL and other directional formatting marks U+202A - U+202F - const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]"; + const LEFT_TO_RIGHT_PATTERN: &'static str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]"; /// Regex pattern matching bunch of unicode control characters and otherwise /// misleading/invisible characters. @@ -176,7 +176,7 @@ impl DisplayName { /// * Blank/invisible characters (U2800, U2062-U2063) /// * Arabic Letter RTL mark U+061C /// * Zero width no-break space (BOM) U+FEFF - const HIDDEN_CHARACTERS_PATTERN: &str = + const HIDDEN_CHARACTERS_PATTERN: &'static str = "[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]"; /// Creates a new [`DisplayName`] from the given raw string. From 5564fe88528578ea28d2a6363b8a76773083811e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 29 Nov 2024 14:42:26 +0100 Subject: [PATCH 638/979] ci: Bump the mac OS runner to 15 --- .github/workflows/bindings_ci.yml | 4 ++-- .github/workflows/xtask.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 89cdf0a38dd..2bcab0fd3ef 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -131,7 +131,7 @@ jobs: test-apple: name: matrix-rust-components-swift needs: xtask - runs-on: macos-14 + runs-on: macos-15 if: github.event_name == 'push' || !github.event.pull_request.draft steps: @@ -186,7 +186,7 @@ jobs: test-crypto-apple-framework-generation: name: Generate Crypto FFI Apple XCFramework - runs-on: macos-14 + runs-on: macos-15 if: github.event_name == 'push' || !github.event.pull_request.draft steps: diff --git a/.github/workflows/xtask.yml b/.github/workflows/xtask.yml index c4e151448e2..1077ad76816 100644 --- a/.github/workflows/xtask.yml +++ b/.github/workflows/xtask.yml @@ -35,7 +35,7 @@ jobs: os-name: 🐧 cachekey-id: linux - - os: macos-14 + - os: macos-15 os-name: 🍏 cachekey-id: macos From 783c86aa7885aa9fd2c3083bc2de046e6791fc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 29 Nov 2024 17:00:41 +0100 Subject: [PATCH 639/979] ci: Build the Mac framework in release mode The dev profile fails with a linker issue about not finding the __chkstk_darwin symbol. --- .github/workflows/bindings_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 2bcab0fd3ef..32b86d26cf8 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -175,7 +175,7 @@ jobs: run: swift test - name: Build Framework - run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev + run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=release complement-crypto: name: "Run Complement Crypto tests" From 1072d0a019b75b84cf3c8cfafb28d1941b2ea665 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 29 Nov 2024 11:08:29 +0000 Subject: [PATCH 640/979] chore(js_tracing): Elide explicit lifetime to satisfy clippy --- crates/matrix-sdk-common/src/js_tracing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-common/src/js_tracing.rs b/crates/matrix-sdk-common/src/js_tracing.rs index 3668d8645d8..7f77a2f7e41 100644 --- a/crates/matrix-sdk-common/src/js_tracing.rs +++ b/crates/matrix-sdk-common/src/js_tracing.rs @@ -306,7 +306,7 @@ impl<'a> JsFieldVisitor<'a> { } } -impl<'a> tracing::field::Visit for JsFieldVisitor<'a> { +impl tracing::field::Visit for JsFieldVisitor<'_> { fn record_debug(&mut self, field: &Field, value: &dyn Debug) { if self.result.is_err() { return; From ba5881355d209b1202cd48c871e2531c4432aa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 29 Nov 2024 12:45:16 +0100 Subject: [PATCH 641/979] chore(test): Upgrade ctor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the `unexpected_cfgs` warning so it doesn't need to be disabled anymore. Signed-off-by: Kévin Commaille --- Cargo.lock | 4 ++-- crates/matrix-sdk-base/src/lib.rs | 1 - crates/matrix-sdk-crypto/src/lib.rs | 1 - crates/matrix-sdk-sqlite/src/lib.rs | 1 - crates/matrix-sdk-ui/src/lib.rs | 2 -- crates/matrix-sdk-ui/tests/integration/main.rs | 2 -- crates/matrix-sdk/src/lib.rs | 1 - crates/matrix-sdk/tests/integration/main.rs | 1 - testing/matrix-sdk-integration-testing/src/lib.rs | 1 - testing/matrix-sdk-test/Cargo.toml | 2 +- 10 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52a3ed545fa..6c3c29f8d6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1081,9 +1081,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", "syn", diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index aacbc1b20bf..0c4f394fd7e 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -16,7 +16,6 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] -#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. #![warn(missing_docs, missing_debug_implementations)] pub use matrix_sdk_common::*; diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 157f0a04b03..e8836195e82 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -17,7 +17,6 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![warn(missing_docs, missing_debug_implementations)] #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] -#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. pub mod backups; mod ciphers; diff --git a/crates/matrix-sdk-sqlite/src/lib.rs b/crates/matrix-sdk-sqlite/src/lib.rs index 8da15149c87..ff62c3f7f0c 100644 --- a/crates/matrix-sdk-sqlite/src/lib.rs +++ b/crates/matrix-sdk-sqlite/src/lib.rs @@ -15,7 +15,6 @@ not(any(feature = "state-store", feature = "crypto-store", feature = "event-cache")), allow(dead_code, unused_imports) )] -#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. #[cfg(feature = "crypto-store")] mod crypto_store; diff --git a/crates/matrix-sdk-ui/src/lib.rs b/crates/matrix-sdk-ui/src/lib.rs index cfb06210c4d..b0f07b9c6b9 100644 --- a/crates/matrix-sdk-ui/src/lib.rs +++ b/crates/matrix-sdk-ui/src/lib.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. - use ruma::html::HtmlSanitizerMode; mod events; diff --git a/crates/matrix-sdk-ui/tests/integration/main.rs b/crates/matrix-sdk-ui/tests/integration/main.rs index 1a9d9ac82ba..da8bc38c3ee 100644 --- a/crates/matrix-sdk-ui/tests/integration/main.rs +++ b/crates/matrix-sdk-ui/tests/integration/main.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation. - use itertools::Itertools as _; use matrix_sdk::deserialized_responses::TimelineEvent; use ruma::{events::AnyStateEvent, serde::Raw, EventId, RoomId}; diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 437f750d3bf..0c7864d3abe 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -17,7 +17,6 @@ #![warn(missing_debug_implementations, missing_docs)] #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation. pub use async_trait::async_trait; pub use bytes; diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 7dc0170e622..5af194c5193 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -1,6 +1,5 @@ // The http mocking library is not supported for wasm32 #![cfg(not(target_arch = "wasm32"))] -#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation. use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Client}; use matrix_sdk_test::test_json; use serde::Serialize; diff --git a/testing/matrix-sdk-integration-testing/src/lib.rs b/testing/matrix-sdk-integration-testing/src/lib.rs index ade85ecacbf..eeeeb11239b 100644 --- a/testing/matrix-sdk-integration-testing/src/lib.rs +++ b/testing/matrix-sdk-integration-testing/src/lib.rs @@ -1,5 +1,4 @@ #![cfg(test)] -#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation. matrix_sdk_test::init_tracing_for_tests!(); diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index d18256db996..75b8e2953f8 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -27,7 +27,7 @@ serde = { workspace = true } serde_json = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ctor = "0.2.0" +ctor = "0.2.9" tokio = { workspace = true, features = ["rt", "macros"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } wiremock = { workspace = true } From bcd0d20e2fe83ea9690dfe951cb19696300afd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 29 Nov 2024 12:53:44 +0100 Subject: [PATCH 642/979] test: Add a method to build `m.room.member` events in the EventFactory --- testing/matrix-sdk-test/src/event_factory.rs | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index 1a8b8dbe8df..16787599cc6 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -35,6 +35,7 @@ use ruma::{ relation::{Annotation, InReplyTo, Replacement, Thread}, room::{ encrypted::{EncryptedEventScheme, RoomEncryptedEventContent}, + member::{MembershipState, RoomMemberEventContent}, message::{ FormattedBody, ImageMessageEventContent, MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, @@ -175,6 +176,15 @@ where Raw::new(map).unwrap().cast() } + /// Build an event from the [`EventBuilder`] and convert it into a + /// serialized and [`Raw`] event. + /// + /// The generic argument `T` allows you to automatically cast the [`Raw`] + /// event into any desired type. + pub fn into_raw(self) -> Raw { + self.construct_json(true) + } + pub fn into_raw_timeline(self) -> Raw { self.construct_json(true) } @@ -343,6 +353,49 @@ impl EventFactory { self.event(RoomMessageEventContent::text_plain(content.into())) } + /// Create a new `m.room.member` event for the given member. + /// + /// The given member will be used as the `sender` as well as the `state_key` + /// of the `m.room.member` event, unless the `sender` was already using + /// [`EventFactory::sender()`], in that case only the state key will be + /// set to the given `member`. + /// + /// The `membership` field of the content is set to + /// [`MembershipState::Join`]. + /// + /// ``` + /// use matrix_sdk_test::event_factory::EventFactory; + /// use ruma::{ + /// events::{ + /// room::member::{MembershipState, RoomMemberEventContent}, + /// SyncStateEvent, + /// }, + /// room_id, + /// serde::Raw, + /// user_id, + /// }; + /// + /// let factory = EventFactory::new().room(room_id!("!test:localhost")); + /// + /// let event: Raw> = factory + /// .member(user_id!("@alice:localhost")) + /// .display_name("Alice") + /// .into_raw(); + /// ``` + pub fn member(&self, member: &UserId) -> EventBuilder { + let mut event = self.event(RoomMemberEventContent::new(MembershipState::Join)); + + if self.sender.is_some() { + event.sender = self.sender.clone(); + } else { + event.sender = Some(member.to_owned()); + } + + event.state_key = Some(member.to_string()); + + event + } + /// Create a new plain/html `m.room.message`. pub fn text_html( &self, @@ -463,3 +516,20 @@ impl EventFactory { self.next_ts.store(value, SeqCst); } } + +impl EventBuilder { + /// Set the `membership` of the `m.room.member` event to the given + /// [`MembershipState`]. + /// + /// The default is [`MembershipState::Join`]. + pub fn membership(mut self, state: MembershipState) -> Self { + self.content.membership = state; + self + } + + /// Set the display name of the `m.room.member` event. + pub fn display_name(mut self, display_name: impl Into) -> Self { + self.content.displayname = Some(display_name.into()); + self + } +} From 2d2215edbe4db06adcd8363cf8c1fb086a24dac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 29 Nov 2024 12:43:19 +0100 Subject: [PATCH 643/979] chore: Make use of the member event builder in the EventFactory --- crates/matrix-sdk-base/src/rooms/normal.rs | 73 ++++++++++------------ 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index d05ce29d1ad..641e6a12282 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1864,6 +1864,7 @@ mod tests { use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::{ async_test, + event_factory::EventFactory, test_json::{sync_events::PINNED_EVENTS, TAG}, ALICE, BOB, CAROL, }; @@ -1879,10 +1880,7 @@ mod tests { room::{ canonical_alias::RoomCanonicalAliasEventContent, encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent}, - member::{ - MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent, - SyncRoomMemberEvent, - }, + member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent}, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, }, @@ -2369,21 +2367,6 @@ mod tests { Raw::new(&ev_json).unwrap().cast() } - fn make_member_event(user_id: &UserId, name: &str) -> Raw { - let ev_json = json!({ - "type": "m.room.member", - "content": assign!(RoomMemberEventContent::new(MembershipState::Join), { - displayname: Some(name.to_owned()) - }), - "sender": user_id, - "state_key": user_id, - "event_id": "$h29iv0s1:example.com", - "origin_server_ts": 208, - }); - - Raw::new(&ev_json).unwrap().cast() - } - #[async_test] async fn test_display_name_for_joined_room_is_empty_if_no_info() { let (_, room) = make_room_test_helper(RoomState::Joined); @@ -2535,20 +2518,23 @@ mod tests { let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); let me = user_id!("@me:example.org"); + let mut changes = StateChanges::new("".to_owned()); let summary = assign!(RumaSummary::new(), { joined_member_count: Some(2u32.into()), heroes: vec![me.to_owned(), matthew.to_owned()], }); + let f = EventFactory::new().room(room_id!("!test:localhost")); + let members = changes .state .entry(room_id.to_owned()) .or_default() .entry(StateEventType::RoomMember) .or_default(); - members.insert(matthew.into(), make_member_event(matthew, "Matthew").cast()); - members.insert(me.into(), make_member_event(me, "Me").cast()); + members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw()); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); store.save_changes(&changes).await.unwrap(); @@ -2567,14 +2553,16 @@ mod tests { let me = user_id!("@me:example.org"); let mut changes = StateChanges::new("".to_owned()); + let f = EventFactory::new().room(room_id!("!test:localhost")); + let members = changes .state .entry(room_id.to_owned()) .or_default() .entry(StateEventType::RoomMember) .or_default(); - members.insert(matthew.into(), make_member_event(matthew, "Matthew").cast()); - members.insert(me.into(), make_member_event(me, "Me").cast()); + members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw()); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); store.save_changes(&changes).await.unwrap(); @@ -2598,6 +2586,8 @@ mod tests { let mut changes = StateChanges::new("".to_owned()); + let f = EventFactory::new().room(room_id!("!test:localhost")); + // Save members in two batches, so that there's no implied ordering in the // store. { @@ -2607,10 +2597,10 @@ mod tests { .or_default() .entry(StateEventType::RoomMember) .or_default(); - members.insert(carol.into(), make_member_event(carol, "Carol").cast()); - members.insert(bob.into(), make_member_event(bob, "Bob").cast()); - members.insert(fred.into(), make_member_event(fred, "Fred").cast()); - members.insert(me.into(), make_member_event(me, "Me").cast()); + members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw()); + members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw()); + members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw()); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); store.save_changes(&changes).await.unwrap(); } @@ -2621,9 +2611,9 @@ mod tests { .or_default() .entry(StateEventType::RoomMember) .or_default(); - members.insert(alice.into(), make_member_event(alice, "Alice").cast()); - members.insert(erica.into(), make_member_event(erica, "Erica").cast()); - members.insert(denis.into(), make_member_event(denis, "Denis").cast()); + members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw()); + members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw()); + members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw()); store.save_changes(&changes).await.unwrap(); } @@ -2651,6 +2641,8 @@ mod tests { let fred = user_id!("@fred:example.org"); let me = user_id!("@me:example.org"); + let f = EventFactory::new().room(room_id!("!test:localhost")); + let mut changes = StateChanges::new("".to_owned()); // Save members in two batches, so that there's no implied ordering in the @@ -2662,10 +2654,11 @@ mod tests { .or_default() .entry(StateEventType::RoomMember) .or_default(); - members.insert(carol.into(), make_member_event(carol, "Carol").cast()); - members.insert(bob.into(), make_member_event(bob, "Bob").cast()); - members.insert(fred.into(), make_member_event(fred, "Fred").cast()); - members.insert(me.into(), make_member_event(me, "Me").cast()); + members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw()); + members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw()); + members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw()); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); + store.save_changes(&changes).await.unwrap(); } @@ -2676,9 +2669,9 @@ mod tests { .or_default() .entry(StateEventType::RoomMember) .or_default(); - members.insert(alice.into(), make_member_event(alice, "Alice").cast()); - members.insert(erica.into(), make_member_event(erica, "Erica").cast()); - members.insert(denis.into(), make_member_event(denis, "Denis").cast()); + members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw()); + members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw()); + members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw()); store.save_changes(&changes).await.unwrap(); } @@ -2700,14 +2693,16 @@ mod tests { heroes: vec![me.to_owned(), matthew.to_owned()], }); + let f = EventFactory::new().room(room_id!("!test:localhost")); + let members = changes .state .entry(room_id.to_owned()) .or_default() .entry(StateEventType::RoomMember) .or_default(); - members.insert(matthew.into(), make_member_event(matthew, "Matthew").cast()); - members.insert(me.into(), make_member_event(me, "Me").cast()); + members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw()); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); store.save_changes(&changes).await.unwrap(); From d1a6956e778f412bd00032658ab4ee9f0d03701f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 29 Nov 2024 20:43:10 +0100 Subject: [PATCH 644/979] chore(sdk): Disable unused_async clippy lint for unimplemented method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/client/builder/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 86b189a1f38..f1606790517 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -650,6 +650,7 @@ async fn build_indexeddb_store_config( } #[cfg(all(not(target_arch = "wasm32"), feature = "indexeddb"))] +#[allow(clippy::unused_async)] async fn build_indexeddb_store_config( _name: &str, _passphrase: Option<&str>, From f94b2023410bf3f0ab3e666a06ee1c9736736430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 29 Nov 2024 19:37:59 +0100 Subject: [PATCH 645/979] chore(xtask): Upgrade xshell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gets rid of an unexpected_cfgs warning. Signed-off-by: Kévin Commaille --- Cargo.lock | 8 +++--- xtask/Cargo.toml | 2 +- xtask/src/ci.rs | 59 ++++++++++++++++++++++++++++-------------- xtask/src/fixup.rs | 17 ++++++++---- xtask/src/kotlin.rs | 15 +++++++---- xtask/src/main.rs | 17 ++++++++++-- xtask/src/release.rs | 32 ++++++++++++++--------- xtask/src/swift.rs | 22 ++++++++++------ xtask/src/workspace.rs | 8 +++--- 9 files changed, 120 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c3c29f8d6c..d5c88e89afc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6814,18 +6814,18 @@ dependencies = [ [[package]] name = "xshell" -version = "0.1.17" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaad2035244c56da05573d4d7fda5f903c60a5f35b9110e157a14a1df45a9f14" +checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" dependencies = [ "xshell-macros", ] [[package]] name = "xshell-macros" -version = "0.1.17" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4916a4a3cad759e499a3620523bf9545cc162d7a06163727dde97ce9aaa4cf39" +checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" [[package]] name = "xtask" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index d6c00f0e071..e21fcd51f1b 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -16,7 +16,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } fs_extra = "1" uniffi_bindgen = { workspace = true } -xshell = "0.1.17" +xshell = "0.2.7" [package.metadata.release] release = false diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 40c8db5df2a..05ea8695996 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -4,9 +4,9 @@ use std::{ }; use clap::{Args, Subcommand}; -use xshell::{cmd, pushd}; +use xshell::cmd; -use crate::{build_docs, workspace, DenyWarnings, Result, NIGHTLY}; +use crate::{build_docs, sh, workspace, DenyWarnings, Result, NIGHTLY}; const WASM_TIMEOUT_ENV_KEY: &str = "WASM_BINDGEN_TEST_TIMEOUT"; const WASM_TIMEOUT_VALUE: &str = "120"; @@ -100,7 +100,8 @@ enum WasmFeatureSet { impl CiArgs { pub fn run(self) -> Result<()> { - let _p = pushd(workspace::root_path()?)?; + let sh = sh(); + let _p = sh.push_dir(workspace::root_path()?); match self.cmd { Some(cmd) => match cmd { @@ -132,8 +133,10 @@ impl CiArgs { } fn check_bindings() -> Result<()> { - cmd!("rustup run stable cargo build -p matrix-sdk-crypto-ffi -p matrix-sdk-ffi").run()?; + let sh = sh(); + cmd!(sh, "rustup run stable cargo build -p matrix-sdk-crypto-ffi -p matrix-sdk-ffi").run()?; cmd!( + sh, " rustup run stable cargo run -p uniffi-bindgen -- generate --library @@ -145,6 +148,7 @@ fn check_bindings() -> Result<()> { ) .run()?; cmd!( + sh, " rustup run stable cargo run -p uniffi-bindgen -- generate --library @@ -160,27 +164,32 @@ fn check_bindings() -> Result<()> { } fn check_examples() -> Result<()> { - cmd!("rustup run stable cargo check -p example-*").run()?; + let sh = sh(); + cmd!(sh, "rustup run stable cargo check -p example-*").run()?; Ok(()) } fn check_style() -> Result<()> { - cmd!("rustup run {NIGHTLY} cargo fmt -- --check").run()?; + let sh = sh(); + cmd!(sh, "rustup run {NIGHTLY} cargo fmt -- --check").run()?; Ok(()) } fn check_typos() -> Result<()> { + let sh = sh(); // FIXME: Print install instructions if command-not-found (needs an xshell // change: https://github.com/matklad/xshell/issues/46) - cmd!("typos").run()?; + cmd!(sh, "typos").run()?; Ok(()) } fn check_clippy() -> Result<()> { - cmd!("rustup run {NIGHTLY} cargo clippy --all-targets --features testing -- -D warnings") + let sh = sh(); + cmd!(sh, "rustup run {NIGHTLY} cargo clippy --all-targets --features testing -- -D warnings") .run()?; cmd!( + sh, "rustup run {NIGHTLY} cargo clippy --workspace --all-targets --exclude matrix-sdk-crypto --exclude xtask --no-default-features @@ -190,6 +199,7 @@ fn check_clippy() -> Result<()> { .run()?; cmd!( + sh, "rustup run {NIGHTLY} cargo clippy --all-targets -p matrix-sdk-crypto --no-default-features -- -D warnings" ) @@ -223,11 +233,12 @@ fn run_feature_tests(cmd: Option) -> Result<()> { (FeatureSet::SsoLogin, "--features sso-login,testing"), ]); + let sh = sh(); let run = |arg_set: &str| { - cmd!("rustup run stable cargo nextest run -p matrix-sdk") + cmd!(sh, "rustup run stable cargo nextest run -p matrix-sdk") .args(arg_set.split_whitespace()) .run()?; - cmd!("rustup run stable cargo test --doc -p matrix-sdk") + cmd!(sh, "rustup run stable cargo test --doc -p matrix-sdk") .args(arg_set.split_whitespace()) .run() }; @@ -247,25 +258,31 @@ fn run_feature_tests(cmd: Option) -> Result<()> { } fn run_crypto_tests() -> Result<()> { - cmd!("rustup run stable cargo clippy -p matrix-sdk-crypto -- -D warnings").run()?; - cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --no-default-features --features testing").run()?; - cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --features=testing").run()?; - cmd!("rustup run stable cargo test --doc -p matrix-sdk-crypto --features=testing").run()?; + let sh = sh(); + cmd!(sh, "rustup run stable cargo clippy -p matrix-sdk-crypto -- -D warnings").run()?; + cmd!(sh, "rustup run stable cargo nextest run -p matrix-sdk-crypto --no-default-features --features testing").run()?; + cmd!(sh, "rustup run stable cargo nextest run -p matrix-sdk-crypto --features=testing") + .run()?; + cmd!(sh, "rustup run stable cargo test --doc -p matrix-sdk-crypto --features=testing").run()?; cmd!( + sh, "rustup run stable cargo clippy -p matrix-sdk-crypto --features=experimental-algorithms -- -D warnings" ) .run()?; cmd!( + sh, "rustup run stable cargo nextest run -p matrix-sdk-crypto --features=experimental-algorithms,testing" ).run()?; cmd!( + sh, "rustup run stable cargo test --doc -p matrix-sdk-crypto --features=experimental-algorithms,testing" ) .run()?; - cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto-ffi").run()?; + cmd!(sh, "rustup run stable cargo nextest run -p matrix-sdk-crypto-ffi").run()?; cmd!( + sh, "rustup run stable cargo nextest run -p matrix-sdk-sqlite --features crypto-store,testing" ) .run()?; @@ -308,8 +325,9 @@ fn run_wasm_checks(cmd: Option) -> Result<()> { ), ]); + let sh = sh(); let run = |arg_set: &str| { - cmd!("rustup run stable cargo clippy --target wasm32-unknown-unknown") + cmd!(sh, "rustup run stable cargo clippy --target wasm32-unknown-unknown") .args(arg_set.split_whitespace()) .args(["--", "-D", "warnings"]) .env(WASM_TIMEOUT_ENV_KEY, WASM_TIMEOUT_VALUE) @@ -370,14 +388,15 @@ fn run_wasm_pack_tests(cmd: Option) -> Result<()> { ), ]); + let sh = sh(); let run = |(folder, arg_set): (&str, &str)| { - let _pwd = pushd(folder)?; - cmd!("pwd").env(WASM_TIMEOUT_ENV_KEY, WASM_TIMEOUT_VALUE).run()?; // print dir so we know what might have failed - cmd!("wasm-pack test --node -- ") + let _pwd = sh.push_dir(folder); + cmd!(sh, "pwd").env(WASM_TIMEOUT_ENV_KEY, WASM_TIMEOUT_VALUE).run()?; // print dir so we know what might have failed + cmd!(sh, "wasm-pack test --node -- ") .args(arg_set.split_whitespace()) .env(WASM_TIMEOUT_ENV_KEY, WASM_TIMEOUT_VALUE) .run()?; - cmd!("wasm-pack test --firefox --headless --") + cmd!(sh, "wasm-pack test --firefox --headless --") .args(arg_set.split_whitespace()) .env(WASM_TIMEOUT_ENV_KEY, WASM_TIMEOUT_VALUE) .run() diff --git a/xtask/src/fixup.rs b/xtask/src/fixup.rs index 3bf078661ce..edf753c51d1 100644 --- a/xtask/src/fixup.rs +++ b/xtask/src/fixup.rs @@ -1,7 +1,7 @@ use clap::{Args, Subcommand}; -use xshell::{cmd, pushd}; +use xshell::cmd; -use crate::{workspace, Result, NIGHTLY}; +use crate::{sh, workspace, Result, NIGHTLY}; #[derive(Args)] pub struct FixupArgs { @@ -21,7 +21,8 @@ enum FixupCommand { impl FixupArgs { pub fn run(self) -> Result<()> { - let _p = pushd(workspace::root_path()?)?; + let sh = sh(); + let _p = sh.push_dir(workspace::root_path()?); match self.cmd { Some(cmd) => match cmd { @@ -41,25 +42,30 @@ impl FixupArgs { } fn fix_style() -> Result<()> { - cmd!("rustup run {NIGHTLY} cargo fmt").run()?; + let sh = sh(); + cmd!(sh, "rustup run {NIGHTLY} cargo fmt").run()?; Ok(()) } fn fix_typos() -> Result<()> { + let sh = sh(); // FIXME: Print install instructions if command-not-found (needs an xshell // change: https://github.com/matklad/xshell/issues/46) - cmd!("typos --write-changes").run()?; + cmd!(sh, "typos --write-changes").run()?; Ok(()) } fn fix_clippy() -> Result<()> { + let sh = sh(); cmd!( + sh, "rustup run {NIGHTLY} cargo clippy --all-targets --fix --allow-dirty --allow-staged -- -D warnings " ) .run()?; cmd!( + sh, "rustup run {NIGHTLY} cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged --exclude matrix-sdk-crypto --exclude xtask @@ -68,6 +74,7 @@ fn fix_clippy() -> Result<()> { ) .run()?; cmd!( + sh, "rustup run {NIGHTLY} cargo clippy --all-targets -p matrix-sdk-crypto --allow-dirty --allow-staged --fix --no-default-features -- -D warnings" diff --git a/xtask/src/kotlin.rs b/xtask/src/kotlin.rs index 8aca51478f9..4e1836211de 100644 --- a/xtask/src/kotlin.rs +++ b/xtask/src/kotlin.rs @@ -3,9 +3,9 @@ use std::fs::create_dir_all; use camino::{Utf8Path, Utf8PathBuf}; use clap::{Args, Subcommand, ValueEnum}; use uniffi_bindgen::{bindings::KotlinBindingGenerator, library_mode::generate_bindings}; -use xshell::{cmd, pushd}; +use xshell::cmd; -use crate::{workspace, Result}; +use crate::{sh, workspace, Result}; struct PackageValues { name: &'static str, @@ -59,7 +59,8 @@ enum KotlinCommand { impl KotlinArgs { pub fn run(self) -> Result<()> { - let _p = pushd(workspace::root_path()?)?; + let sh = sh(); + let _p = sh.push_dir(workspace::root_path()?); match self.cmd { KotlinCommand::BuildAndroidLibrary { @@ -130,8 +131,12 @@ fn build_for_android_target( dest_dir: &str, package_name: &str, ) -> Result { - cmd!("cargo ndk --target {target} -o {dest_dir} build --profile {profile} -p {package_name}") - .run()?; + let sh = sh(); + cmd!( + sh, + "cargo ndk --target {target} -o {dest_dir} build --profile {profile} -p {package_name}" + ) + .run()?; // The builtin dev profile has its files stored under target/debug, all // other targets have matching directory names diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 80d740f3f83..0534e008d95 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -5,13 +5,15 @@ mod release; mod swift; mod workspace; +use std::rc::Rc; + use ci::CiArgs; use clap::{Parser, Subcommand}; use fixup::FixupArgs; use kotlin::KotlinArgs; use release::ReleaseArgs; use swift::SwiftArgs; -use xshell::cmd; +use xshell::{cmd, Shell}; const NIGHTLY: &str = "nightly-2024-11-26"; @@ -66,11 +68,22 @@ fn build_docs( rustdocflags += " -Dwarnings"; } + let sh = sh(); // Keep in sync with .github/workflows/docs.yml - cmd!("rustup run {NIGHTLY} cargo doc --no-deps --workspace --features docsrs") + cmd!(sh, "rustup run {NIGHTLY} cargo doc --no-deps --workspace --features docsrs") .env("RUSTDOCFLAGS", rustdocflags) .args(extra_args) .run()?; Ok(()) } + +thread_local! { + /// The shared shell API. + static SH: Rc = Rc::new(Shell::new().unwrap()) +} + +/// Get a reference to the shared shell API. +fn sh() -> Rc { + SH.with(|sh| sh.clone()) +} diff --git a/xtask/src/release.rs b/xtask/src/release.rs index d31b49b8509..56fcf7ea1b0 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -1,9 +1,9 @@ use std::env; use clap::{Args, Subcommand, ValueEnum}; -use xshell::{cmd, pushd}; +use xshell::cmd; -use crate::{workspace, Result}; +use crate::{sh, workspace, Result}; #[derive(Args)] pub struct ReleaseArgs { @@ -74,7 +74,8 @@ impl ReleaseArgs { // // More info: https://git-cliff.org/docs/usage/monorepos if self.cmd != ReleaseCommand::Changelog { - let _p = pushd(workspace::root_path()?)?; + let sh = sh(); + let _p = sh.push_dir(workspace::root_path()?); } match self.cmd { @@ -87,14 +88,16 @@ impl ReleaseArgs { } fn check_prerequisites() { - if cmd!("cargo release --version").echo_cmd(false).ignore_stdout().run().is_err() { + let sh = sh(); + + if cmd!(sh, "cargo release --version").quiet().ignore_stdout().run().is_err() { eprintln!("This command requires cargo-release, please install it."); eprintln!("More info can be found at: https://github.com/crate-ci/cargo-release?tab=readme-ov-file#install"); std::process::exit(1); } - if cmd!("git cliff --version").echo_cmd(false).ignore_stdout().run().is_err() { + if cmd!(sh, "git cliff --version").quiet().ignore_stdout().run().is_err() { eprintln!("This command requires git-cliff, please install it."); eprintln!("More info can be found at: https://git-cliff.org/docs/installation/"); @@ -103,7 +106,8 @@ fn check_prerequisites() { } fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> { - let cmd = cmd!("cargo release --no-publish --no-tag --no-push"); + let sh = sh(); + let cmd = cmd!(sh, "cargo release --no-publish --no-tag --no-push"); let cmd = if execute { cmd.arg("--execute") } else { cmd }; let cmd = cmd.arg(version.as_str()); @@ -122,15 +126,17 @@ fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> { } fn publish(execute: bool) -> Result<()> { - let cmd = cmd!("cargo release tag"); + let sh = sh(); + + let cmd = cmd!(sh, "cargo release tag"); let cmd = if execute { cmd.arg("--execute") } else { cmd }; cmd.run()?; - let cmd = cmd!("cargo release publish"); + let cmd = cmd!(sh, "cargo release publish"); let cmd = if execute { cmd.arg("--execute") } else { cmd }; cmd.run()?; - let cmd = cmd!("cargo release push"); + let cmd = cmd!(sh, "cargo release push"); let cmd = if execute { cmd.arg("--execute") } else { cmd }; cmd.run()?; @@ -138,13 +144,14 @@ fn publish(execute: bool) -> Result<()> { } fn weekly_report() -> Result<()> { - let lines = cmd!("git log --pretty=format:%H --since='1 week ago'").read()?; + let sh = sh(); + let lines = cmd!(sh, "git log --pretty=format:%H --since='1 week ago'").read()?; let Some(start) = lines.split_whitespace().last() else { panic!("Could not find a start range for the git commit range.") }; - cmd!("git cliff --config cliff-weekly-report.toml {start}..HEAD").run()?; + cmd!(sh, "git cliff --config cliff-weekly-report.toml {start}..HEAD").run()?; Ok(()) } @@ -167,7 +174,8 @@ fn changelog() -> Result<()> { println!("Generating a changelog for {}.", crate_name); } - let command = cmd!("git cliff") + let sh = sh(); + let command = cmd!(sh, "git cliff") .arg("cliff") .arg("--config") .arg("../../cliff.toml") diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs index f05a09d13e6..31a38287a84 100644 --- a/xtask/src/swift.rs +++ b/xtask/src/swift.rs @@ -6,9 +6,9 @@ use std::{ use camino::{Utf8Path, Utf8PathBuf}; use clap::{Args, Subcommand}; use uniffi_bindgen::{bindings::SwiftBindingGenerator, library_mode::generate_bindings}; -use xshell::{cmd, pushd}; +use xshell::cmd; -use crate::{workspace, Result}; +use crate::{sh, workspace, Result}; /// Builds the SDK for Swift as a Static Library or XCFramework. #[derive(Args)] @@ -53,7 +53,8 @@ enum SwiftCommand { impl SwiftArgs { pub fn run(self) -> Result<()> { - let _p = pushd(workspace::root_path()?)?; + let sh = sh(); + let _p = sh.push_dir(workspace::root_path()?); match self.cmd { SwiftCommand::BuildLibrary => build_library(), @@ -147,7 +148,8 @@ fn build_library() -> Result<()> { create_dir_all(ffi_directory.as_path())?; - cmd!("rustup run stable cargo build -p matrix-sdk-ffi").run()?; + let sh = sh(); + cmd!(sh, "rustup run stable cargo build -p matrix-sdk-ffi").run()?; rename(lib_output_dir.join(FFI_LIBRARY_NAME), ffi_directory.join(FFI_LIBRARY_NAME))?; let swift_directory = root_directory.join("bindings/apple/generated/swift"); @@ -219,7 +221,8 @@ fn build_xcframework( if xcframework_path.exists() { remove_dir_all(&xcframework_path)?; } - let mut cmd = cmd!("xcodebuild -create-xcframework"); + let sh = sh(); + let mut cmd = cmd!(sh, "xcodebuild -create-xcframework"); for p in libs { cmd = cmd.arg("-library").arg(p).arg("-headers").arg(&headers_dir) } @@ -270,17 +273,19 @@ fn build_targets( profile: &str, sequentially: bool, ) -> Result>> { + let sh = sh(); + if sequentially { for target in &targets { let triple = target.triple; println!("-- Building for {}", target.description); - cmd!("rustup run stable cargo build -p matrix-sdk-ffi --target {triple} --profile {profile}") + cmd!(sh, "rustup run stable cargo build -p matrix-sdk-ffi --target {triple} --profile {profile}") .run()?; } } else { let triples = &targets.iter().map(|target| target.triple).collect::>(); - let mut cmd = cmd!("rustup run stable cargo build -p matrix-sdk-ffi"); + let mut cmd = cmd!(sh, "rustup run stable cargo build -p matrix-sdk-ffi"); for triple in triples { cmd = cmd.arg("--target").arg(triple); } @@ -316,6 +321,7 @@ fn lipo_platform_libraries( generated_dir: &Utf8PathBuf, ) -> Result> { let mut libs = Vec::new(); + let sh = sh(); for platform in platform_build_paths.keys() { let paths = platform_build_paths.get(platform).unwrap(); @@ -328,7 +334,7 @@ fn lipo_platform_libraries( create_dir_all(&output_folder)?; let output_path = output_folder.join(FFI_LIBRARY_NAME); - let mut cmd = cmd!("lipo -create"); + let mut cmd = cmd!(sh, "lipo -create"); for path in paths { cmd = cmd.arg(path); } diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs index 81f7529120f..e6db2045b0a 100644 --- a/xtask/src/workspace.rs +++ b/xtask/src/workspace.rs @@ -4,7 +4,7 @@ use camino::Utf8PathBuf; use serde::Deserialize; use xshell::cmd; -use crate::Result; +use crate::{sh, Result}; pub fn root_path() -> Result { #[derive(Deserialize)] @@ -13,7 +13,8 @@ pub fn root_path() -> Result { } let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); - let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; + let sh = sh(); + let metadata_json = cmd!(sh, "{cargo} metadata --no-deps --format-version 1").read()?; Ok(serde_json::from_str::(&metadata_json)?.workspace_root) } @@ -24,6 +25,7 @@ pub fn target_path() -> Result { } let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); - let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; + let sh = sh(); + let metadata_json = cmd!(sh, "{cargo} metadata --no-deps --format-version 1").read()?; Ok(serde_json::from_str::(&metadata_json)?.target_directory) } From 685386df13f9252749d23176eba3dab4049723fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 30 Nov 2024 17:12:04 +0100 Subject: [PATCH 646/979] chore(xtask): Fix scope of push_dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- xtask/src/release.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xtask/src/release.rs b/xtask/src/release.rs index 56fcf7ea1b0..2b33a07276f 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -73,9 +73,10 @@ impl ReleaseArgs { // make sure to not switch back to the workspace dir. // // More info: https://git-cliff.org/docs/usage/monorepos + let sh = sh(); + let _p; if self.cmd != ReleaseCommand::Changelog { - let sh = sh(); - let _p = sh.push_dir(workspace::root_path()?); + _p = sh.push_dir(workspace::root_path()?); } match self.cmd { From d2fecb6701a4a607fb3fa19fe19c54d2680cecf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 28 Nov 2024 18:03:29 +0100 Subject: [PATCH 647/979] fix(room_preview): When requesting a room summary, use fallback server names If no server names are provided for the room summary request and the room's server name doesn't match the current user's server name, add the room alias/id server name as a fallback value. This seems to fix room preview through federation. Also, when getting a summary for a room list item, if it's an invite one, add the server name of the inviter's user id as another possible fallback. Changelog: Use the inviter's server name and the server name from the room alias as fallback values for the via parameter when requesting the room summary from the homeserver. --- bindings/matrix-sdk-ffi/src/room_list.rs | 13 +++- crates/matrix-sdk/src/room_preview.rs | 94 +++++++++++++++++++++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 412ee88cce4..1c193b0ac8d 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -616,7 +616,8 @@ impl RoomListItem { // Do the thing. let client = self.inner.client(); - let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() { + let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias() + { let room_or_alias_id: OwnedRoomOrAliasId = alias.into(); (room_or_alias_id, Vec::new()) } else { @@ -624,6 +625,16 @@ impl RoomListItem { (room_or_alias_id, server_names) }; + // If no server names are provided and the room's membership is invited, + // add the server name from the sender's user id as a fallback value + if server_names.is_empty() { + if let Ok(invite_details) = self.inner.invite_details().await { + if let Some(inviter) = invite_details.inviter { + server_names.push(inviter.user_id().server_name().to_owned()); + } + } + } + let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?; Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview))) diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 58c9c68a501..a5385fde0d9 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -26,7 +26,7 @@ use ruma::{ events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}, room::RoomType, space::SpaceRoomJoinRule, - OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, + OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName, }; use tokio::try_join; use tracing::{instrument, warn}; @@ -233,6 +233,9 @@ impl RoomPreview { room_or_alias_id: &RoomOrAliasId, via: Vec, ) -> crate::Result { + let own_server_name = client.session_meta().map(|s| s.user_id.server_name()); + let via = ensure_server_names_is_not_empty(own_server_name, via, room_or_alias_id); + let request = ruma::api::client::room::get_summary::msc3266::Request::new( room_or_alias_id.to_owned(), via, @@ -372,3 +375,92 @@ async fn search_for_room_preview_in_room_directory( Ok(None) } + +// Make sure the server name of the room id/alias is +// included in the list of server names to send if no server names are provided +fn ensure_server_names_is_not_empty( + own_server_name: Option<&ServerName>, + server_names: Vec, + room_or_alias_id: &RoomOrAliasId, +) -> Vec { + let mut server_names = server_names; + + if let Some((own_server, alias_server)) = own_server_name.zip(room_or_alias_id.server_name()) { + if server_names.is_empty() && own_server != alias_server { + server_names.push(alias_server.to_owned()); + } + } + + server_names +} + +#[cfg(test)] +mod tests { + use ruma::{owned_server_name, room_alias_id, room_id, server_name, RoomOrAliasId, ServerName}; + + use crate::room_preview::ensure_server_names_is_not_empty; + + #[test] + fn test_ensure_server_names_is_not_empty_when_no_own_server_name_is_provided() { + let own_server_name: Option<&ServerName> = None; + let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into(); + + let server_names = + ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id); + + // There was no own server name to check against, so no additional server name + // was added + assert!(server_names.is_empty()); + } + + #[test] + fn test_ensure_server_names_is_not_empty_when_room_alias_or_id_has_no_server_name() { + let own_server_name: Option<&ServerName> = Some(server_name!("localhost")); + let room_or_alias_id: &RoomOrAliasId = room_id!("!test").into(); + + let server_names = + ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id); + + // The room id has no server name, so nothing could be added + assert!(server_names.is_empty()); + } + + #[test] + fn test_ensure_server_names_is_not_empty_with_same_server_name() { + let own_server_name: Option<&ServerName> = Some(server_name!("localhost")); + let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into(); + + let server_names = + ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id); + + // The room id's server name was the same as our own server name, so there's no + // need to add it + assert!(server_names.is_empty()); + } + + #[test] + fn test_ensure_server_names_is_not_empty_with_different_room_id_server_name() { + let own_server_name: Option<&ServerName> = Some(server_name!("localhost")); + let room_or_alias_id: &RoomOrAliasId = room_id!("!test:matrix.org").into(); + + let server_names = + ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id); + + // The server name in the room id was added + assert!(!server_names.is_empty()); + assert_eq!(server_names[0], owned_server_name!("matrix.org")); + } + + #[test] + fn test_ensure_server_names_is_not_empty_with_different_room_alias_server_name() { + let own_server_name: Option<&ServerName> = Some(server_name!("localhost")); + let room_or_alias_id: &RoomOrAliasId = room_alias_id!("#test:matrix.org").into(); + + let server_names = + ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id); + + // The server name in the room alias was added + assert!(!server_names.is_empty()); + assert_eq!(server_names[0], owned_server_name!("matrix.org")); + } +} From ad3d1fb6b34ee83b6c8ae659abb360b20939c5d5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 2 Dec 2024 11:25:10 +0100 Subject: [PATCH 648/979] refactor(ui): Use an iterator instead of `Vec` to represent events. This patch changes `TimelineStateTransaction::add_remote_events_at` to take an `IntoIterator>` for `events`. In the current code, it saves one `iter().map(Into::into).collect::>()`, but it will save another one when we will support `VectorDiff`s coming from the `EventCache`. It also avoids to allocate a vector to pass new events (this mostly happens in the test, but it can happen in real life). --- crates/matrix-sdk-ui/src/timeline/builder.rs | 6 +-- .../src/timeline/controller/mod.rs | 37 +++++++++++------ .../src/timeline/controller/state.rs | 41 +++++++++++++------ .../matrix-sdk-ui/src/timeline/pagination.rs | 2 +- .../matrix-sdk-ui/src/timeline/tests/basic.rs | 29 +++++++++---- .../matrix-sdk-ui/src/timeline/tests/echo.rs | 6 +-- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 4 +- .../src/timeline/tests/reactions.rs | 5 ++- .../src/timeline/tests/redaction.rs | 5 ++- 9 files changed, 88 insertions(+), 47 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 4de77f57862..8e131e21f6e 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -183,7 +183,7 @@ impl TimelineBuilder { if let Ok(events) = inner.reload_pinned_events().await { inner .replace_with_initial_remote_events( - events, + events.into_iter(), RemoteEventOrigin::Pagination, ) .await; @@ -238,7 +238,7 @@ impl TimelineBuilder { // current timeline. match room_event_cache.subscribe().await { Ok((events, _)) => { - inner.replace_with_initial_remote_events(events, RemoteEventOrigin::Sync).await; + inner.replace_with_initial_remote_events(events.into_iter(), RemoteEventOrigin::Sync).await; } Err(err) => { warn!("Error when re-inserting initial events into the timeline: {err}"); @@ -272,7 +272,7 @@ impl TimelineBuilder { trace!("Received new timeline events."); inner.add_events_at( - events, + events.into_iter(), TimelineNewItemPosition::End { origin: match origin { EventsOrigin::Sync => RemoteEventOrigin::Sync, } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 05de8cb3c89..42e70eba3e1 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -303,7 +303,11 @@ impl TimelineController

{ let has_events = !events.is_empty(); - self.replace_with_initial_remote_events(events, RemoteEventOrigin::Cache).await; + self.replace_with_initial_remote_events( + events.into_iter(), + RemoteEventOrigin::Cache, + ) + .await; Ok(has_events) } @@ -320,7 +324,7 @@ impl TimelineController

{ let has_events = !start_from_result.events.is_empty(); self.replace_with_initial_remote_events( - start_from_result.events.into_iter().map(Into::into).collect(), + start_from_result.events.into_iter(), RemoteEventOrigin::Pagination, ) .await; @@ -336,7 +340,7 @@ impl TimelineController

{ let has_events = !loaded_events.is_empty(); self.replace_with_initial_remote_events( - loaded_events, + loaded_events.into_iter(), RemoteEventOrigin::Pagination, ) .await; @@ -405,7 +409,7 @@ impl TimelineController

{ }; self.add_events_at( - pagination.events, + pagination.events.into_iter(), TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }, ) .await; @@ -432,7 +436,7 @@ impl TimelineController

{ }; self.add_events_at( - pagination.events, + pagination.events.into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Pagination }, ) .await; @@ -632,12 +636,16 @@ impl TimelineController

{ /// is the most recent. /// /// Returns the number of timeline updates that were made. - pub(super) async fn add_events_at( + pub(super) async fn add_events_at( &self, - events: Vec>, + events: Events, position: TimelineNewItemPosition, - ) -> HandleManyEventsResult { - if events.is_empty() { + ) -> HandleManyEventsResult + where + Events: IntoIterator + ExactSizeIterator, + ::Item: Into, + { + if events.len() == 0 { return Default::default(); } @@ -656,11 +664,14 @@ impl TimelineController

{ /// /// This is all done with a single lock guard, since we don't want the state /// to be modified between the clear and re-insertion of new events. - pub(super) async fn replace_with_initial_remote_events( + pub(super) async fn replace_with_initial_remote_events( &self, - events: Vec, + events: Events, origin: RemoteEventOrigin, - ) { + ) where + Events: IntoIterator + ExactSizeIterator, + ::Item: Into, + { let mut state = self.state.write().await; let track_read_markers = self.settings.track_read_receipts; @@ -676,7 +687,7 @@ impl TimelineController

{ // Previously we just had to check the new one wasn't empty because // we did a clear operation before so the current one would always be empty, but // now we may want to replace a populated timeline with an empty one. - if !state.items.is_empty() || !events.is_empty() { + if !state.items.is_empty() || events.len() > 0 { state .replace_with_remote_events( events, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 13adcbd2ab5..dd4ca784f5d 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -127,14 +127,19 @@ impl TimelineState { /// should be ordered in *reverse* topological order, that is, `events[0]` /// is the most recent. #[tracing::instrument(skip(self, events, room_data_provider, settings))] - pub(super) async fn add_remote_events_at( + pub(super) async fn add_remote_events_at( &mut self, - events: Vec>, + events: Events, position: TimelineNewItemPosition, - room_data_provider: &P, + room_data_provider: &RoomData, settings: &TimelineSettings, - ) -> HandleManyEventsResult { - if events.is_empty() { + ) -> HandleManyEventsResult + where + Events: IntoIterator + ExactSizeIterator, + ::Item: Into, + RoomData: RoomDataProvider, + { + if events.len() == 0 { return Default::default(); } @@ -292,13 +297,18 @@ impl TimelineState { /// Note: when the `position` is [`TimelineEnd::Front`], prepended events /// should be ordered in *reverse* topological order, that is, `events[0]` /// is the most recent. - pub(super) async fn replace_with_remote_events( + pub(super) async fn replace_with_remote_events( &mut self, - events: Vec, + events: Events, position: TimelineNewItemPosition, - room_data_provider: &P, + room_data_provider: &RoomData, settings: &TimelineSettings, - ) -> HandleManyEventsResult { + ) -> HandleManyEventsResult + where + Events: IntoIterator, + Events::Item: Into, + RoomData: RoomDataProvider, + { let mut txn = self.transaction(); txn.clear(); let result = txn.add_remote_events_at(events, position, room_data_provider, settings).await; @@ -352,13 +362,18 @@ impl TimelineStateTransaction<'_> { /// should be ordered in *reverse* topological order, that is, `events[0]` /// is the most recent. #[tracing::instrument(skip(self, events, room_data_provider, settings))] - pub(super) async fn add_remote_events_at( + pub(super) async fn add_remote_events_at( &mut self, - events: Vec>, + events: Events, position: TimelineNewItemPosition, - room_data_provider: &P, + room_data_provider: &RoomData, settings: &TimelineSettings, - ) -> HandleManyEventsResult { + ) -> HandleManyEventsResult + where + Events: IntoIterator, + Events::Item: Into, + RoomData: RoomDataProvider, + { let mut total = HandleManyEventsResult::default(); let position = position.into(); diff --git a/crates/matrix-sdk-ui/src/timeline/pagination.rs b/crates/matrix-sdk-ui/src/timeline/pagination.rs index afdbb48c1ca..abe49acac0f 100644 --- a/crates/matrix-sdk-ui/src/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/src/timeline/pagination.rs @@ -81,7 +81,7 @@ impl super::Timeline { // `matrix_sdk::event_cache::RoomEventCacheUpdate` from // `matrix_sdk::event_cache::RoomPagination::run_backwards`. self.controller - .add_events_at(events, TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }) + .add_events_at(events.into_iter(), TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }) .await; if num_events == 0 && !reached_start { diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index bcc0d7cd296..85cc7c2d48c 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -50,7 +50,7 @@ async fn test_initial_events() { timeline .controller .add_events_at( - vec![f.text_msg("A").sender(*ALICE), f.text_msg("B").sender(*BOB)], + [f.text_msg("A").sender(*ALICE), f.text_msg("B").sender(*BOB)].into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; @@ -92,7 +92,10 @@ async fn test_replace_with_initial_events_and_read_marker() { timeline .controller - .add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }) + .add_events_at( + [ev].into_iter(), + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, + ) .await; let items = timeline.controller.items().await; @@ -101,7 +104,10 @@ async fn test_replace_with_initial_events_and_read_marker() { assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "hey"); let ev = f.text_msg("yo").sender(*BOB).into_sync(); - timeline.controller.replace_with_initial_remote_events(vec![ev], RemoteEventOrigin::Sync).await; + timeline + .controller + .replace_with_initial_remote_events([ev].into_iter(), RemoteEventOrigin::Sync) + .await; let items = timeline.controller.items().await; assert_eq!(items.len(), 2); @@ -309,7 +315,7 @@ async fn test_dedup_initial() { timeline .controller .add_events_at( - vec![ + [ // two events event_a.clone(), event_b.clone(), @@ -318,7 +324,8 @@ async fn test_dedup_initial() { event_b, // … and a new event also came in event_c, - ], + ] + .into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; @@ -356,7 +363,7 @@ async fn test_internal_id_prefix() { timeline .controller .add_events_at( - vec![ev_a, ev_b, ev_c], + [ev_a, ev_b, ev_c].into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; @@ -522,7 +529,10 @@ async fn test_replace_with_initial_events_when_batched() { timeline .controller - .add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }) + .add_events_at( + [ev].into_iter(), + TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, + ) .await; let (items, mut stream) = timeline.controller.subscribe_batched().await; @@ -531,7 +541,10 @@ async fn test_replace_with_initial_events_when_batched() { assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "hey"); let ev = f.text_msg("yo").sender(*BOB).into_sync(); - timeline.controller.replace_with_initial_remote_events(vec![ev], RemoteEventOrigin::Sync).await; + timeline + .controller + .replace_with_initial_remote_events([ev].into_iter(), RemoteEventOrigin::Sync) + .await; // Assert there are more than a single Clear diff in the next batch: // Clear + PushBack (event) + PushFront (day divider) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index b68ccedc5b1..cd88dfd9b73 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -272,12 +272,12 @@ async fn test_no_read_marker_with_local_echo() { timeline .controller .replace_with_initial_remote_events( - vec![f - .text_msg("msg1") + [f.text_msg("msg1") .sender(user_id!("@a:b.c")) .event_id(event_id) .server_ts(MilliSecondsSinceUnixEpoch::now()) - .into_sync()], + .into_sync()] + .into_iter(), RemoteEventOrigin::Sync, ) .await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index b1d1cc29573..f708da78664 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -242,7 +242,7 @@ impl TestTimeline { let event = event.into(); self.controller .add_events_at( - vec![event], + [event].into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; @@ -264,7 +264,7 @@ impl TestTimeline { let timeline_event = TimelineEvent::new(event.cast()); self.controller .add_events_at( - vec![timeline_event], + [timeline_event].into_iter(), TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }, ) .await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs index c146abee637..5cd0282bd84 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs @@ -196,14 +196,15 @@ async fn test_initial_reaction_timestamp_is_stored() { timeline .controller .add_events_at( - vec![ + [ // Reaction comes first. f.reaction(&message_event_id, REACTION_KEY.to_owned()) .server_ts(reaction_timestamp) .into_sync(), // Event comes next. f.text_msg("A").event_id(&message_event_id).into_sync(), - ], + ] + .into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs index 73f3eff0eff..ea1e06d80c4 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs @@ -141,11 +141,12 @@ async fn test_reaction_redaction_timeline_filter() { timeline .controller .add_events_at( - vec![SyncTimelineEvent::new( + [SyncTimelineEvent::new( timeline .event_builder .make_sync_redacted_message_event(*ALICE, RedactedReactionEventContent::new()), - )], + )] + .into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, ) .await; From 27e1cded2e035360bb806359911215579bb20891 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 21 Nov 2024 15:37:17 +0100 Subject: [PATCH 649/979] feat(event cache): reload a linked chunk from a sqlite store --- .../src/event_cache/store/memory_store.rs | 12 +++++-- .../src/event_cache/store/mod.rs | 2 +- .../src/event_cache/store/traits.rs | 22 ++++++++++++- .../src/event_cache_store.rs | 33 +++++++++++++++++-- .../matrix-sdk/src/event_cache/room/events.rs | 4 +-- 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 92109029d3f..ef6c1d388d7 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -16,13 +16,13 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::{relational::RelationalLinkedChunk, Update}, + linked_chunk::{relational::RelationalLinkedChunk, LinkedChunk, Update}, ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; use ruma::{MxcUri, OwnedMxcUri, RoomId}; -use super::{EventCacheStore, EventCacheStoreError, Result}; +use super::{EventCacheStore, EventCacheStoreError, Result, DEFAULT_CHUNK_CAPACITY}; use crate::{ event_cache::{Event, Gap}, media::{MediaRequestParameters, UniqueKey as _}, @@ -93,6 +93,14 @@ impl EventCacheStore for MemoryStore { Ok(()) } + async fn reload_linked_chunk( + &self, + _room_id: &RoomId, + ) -> Result>, Self::Error> { + // TODO(hywan) + Ok(Default::default()) + } + async fn add_media_content( &self, request: &MediaRequestParameters, diff --git a/crates/matrix-sdk-base/src/event_cache/store/mod.rs b/crates/matrix-sdk-base/src/event_cache/store/mod.rs index 0162a7da27e..e6a2e4090f4 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/mod.rs @@ -36,7 +36,7 @@ pub use matrix_sdk_store_encryption::Error as StoreEncryptionError; pub use self::integration_tests::EventCacheStoreIntegrationTests; pub use self::{ memory_store::MemoryStore, - traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore}, + traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY}, }; /// The high-level public type to represent an `EventCacheStore` lock. diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 3c35862b3c5..939433727fb 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -15,7 +15,10 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; -use matrix_sdk_common::{linked_chunk::Update, AsyncTraitDeps}; +use matrix_sdk_common::{ + linked_chunk::{LinkedChunk, Update}, + AsyncTraitDeps, +}; use ruma::{MxcUri, RoomId}; use super::EventCacheStoreError; @@ -24,6 +27,10 @@ use crate::{ media::MediaRequestParameters, }; +/// A default capacity for linked chunks, when manipulating in conjunction with +/// an `EventCacheStore` implementation. +pub const DEFAULT_CHUNK_CAPACITY: usize = 128; + /// An abstract trait that can be used to implement different store backends /// for the event cache of the SDK. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -49,6 +56,12 @@ pub trait EventCacheStore: AsyncTraitDeps { updates: Vec>, ) -> Result<(), Self::Error>; + /// Reconstruct a full linked chunk by reloading it from storage. + async fn reload_linked_chunk( + &self, + room_id: &RoomId, + ) -> Result>, Self::Error>; + /// Add a media file's content in the media store. /// /// # Arguments @@ -151,6 +164,13 @@ impl EventCacheStore for EraseEventCacheStoreError { self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into) } + async fn reload_linked_chunk( + &self, + room_id: &RoomId, + ) -> Result>, Self::Error> { + self.0.reload_linked_chunk(room_id).await.map_err(Into::into) + } + async fn add_media_content( &self, request: &MediaRequestParameters, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index d22d8f21eda..7ecc932904a 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -21,8 +21,11 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc}; use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ - event_cache::{store::EventCacheStore, Event, Gap}, - linked_chunk::{ChunkContent, ChunkIdentifier, Update}, + event_cache::{ + store::{EventCacheStore, DEFAULT_CHUNK_CAPACITY}, + Event, Gap, + }, + linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunk, LinkedChunkBuilder, Update}, media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; @@ -626,6 +629,32 @@ impl EventCacheStore for SqliteEventCacheStore { Ok(()) } + async fn reload_linked_chunk( + &self, + room_id: &RoomId, + ) -> Result>, Self::Error> { + let chunks = self.load_chunks(room_id).await?; + + let mut builder = LinkedChunkBuilder::new(); + + for c in chunks { + match c.content { + ChunkContent::Gap(gap) => { + builder.push_gap(c.previous, c.id, c.next, gap); + } + ChunkContent::Items(items) => { + builder.push_items(c.previous, c.id, c.next, items); + } + } + } + + builder.with_update_history(); + + builder.build().map_err(|err| Error::InvalidData { + details: format!("when rebuilding a linked chunk: {err}"), + }) + } + async fn add_media_content( &self, request: &MediaRequestParameters, diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 7e5e004894e..670208fa188 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -16,7 +16,7 @@ use std::cmp::Ordering; use eyeball_im::VectorDiff; pub use matrix_sdk_base::event_cache::{Event, Gap}; -use matrix_sdk_base::linked_chunk::AsVector; +use matrix_sdk_base::{event_cache::store::DEFAULT_CHUNK_CAPACITY, linked_chunk::AsVector}; use matrix_sdk_common::linked_chunk::{ Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position, }; @@ -25,8 +25,6 @@ use tracing::{debug, error, warn}; use super::super::deduplicator::{Decoration, Deduplicator}; -const DEFAULT_CHUNK_CAPACITY: usize = 128; - /// This type represents all events of a single room. #[derive(Debug)] pub struct RoomEvents { From ed3b03f454c0ad2dbf24139504da64d649143e4a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 2 Dec 2024 11:16:02 +0100 Subject: [PATCH 650/979] feat(event cache): implement reloading a linked chunk from the memory store too --- .../src/event_cache/store/memory_store.rs | 22 ++- .../src/event_cache/store/mod.rs | 7 + .../src/linked_chunk/relational.rs | 140 ++++++++++++++++++ 3 files changed, 165 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index ef6c1d388d7..b7b81d1439e 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -16,7 +16,7 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::{relational::RelationalLinkedChunk, LinkedChunk, Update}, + linked_chunk::{relational::RelationalLinkedChunk, LinkedChunk, LinkedChunkBuilder, Update}, ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; @@ -95,10 +95,24 @@ impl EventCacheStore for MemoryStore { async fn reload_linked_chunk( &self, - _room_id: &RoomId, + room_id: &RoomId, ) -> Result>, Self::Error> { - // TODO(hywan) - Ok(Default::default()) + let inner = self.inner.read().unwrap(); + + let mut builder = LinkedChunkBuilder::new(); + + inner + .events + .reload_chunks(room_id, &mut builder) + .map_err(|err| EventCacheStoreError::InvalidData { details: err })?; + + builder.with_update_history(); + + let result = builder.build().map_err(|err| EventCacheStoreError::InvalidData { + details: format!("when rebuilding a linked chunk: {err}"), + })?; + + Ok(result) } async fn add_media_content( diff --git a/crates/matrix-sdk-base/src/event_cache/store/mod.rs b/crates/matrix-sdk-base/src/event_cache/store/mod.rs index e6a2e4090f4..454b34995f0 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/mod.rs @@ -148,6 +148,13 @@ pub enum EventCacheStoreError { current version: {0}, latest version: {1}" )] UnsupportedDatabaseVersion(usize, usize), + + /// The store contains invalid data. + #[error("The store contains invalid data: {details}")] + InvalidData { + /// Details why the data contained in the store was invalid. + details: String, + }, } impl EventCacheStoreError { diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index 445184cf1f2..c78a543755c 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -17,6 +17,7 @@ use ruma::{OwnedRoomId, RoomId}; +use super::LinkedChunkBuilder; use crate::linked_chunk::{ChunkIdentifier, Position, Update}; /// A row of the [`RelationalLinkedChunk::chunks`]. @@ -274,6 +275,90 @@ impl RelationalLinkedChunk { } } +impl RelationalLinkedChunk +where + Gap: Clone, + Item: Clone, +{ + /// Reloads the chunks. + /// + /// Return an error result if the data was malformed in the struct, with a + /// string message explaining details about the error. + pub fn reload_chunks( + &self, + room_id: &RoomId, + builder: &mut LinkedChunkBuilder, + ) -> Result<(), String> { + for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) { + // Find all items that correspond to the chunk. + let mut items = self + .items + .iter() + .filter(|row| { + row.room_id == room_id && row.position.chunk_identifier() == chunk_row.chunk + }) + .peekable(); + + // Look at the first chunk item type, to reconstruct the chunk at hand. + let Some(first) = items.peek() else { + // No items found for this chunk; don't include it in the builder. + return Err(format!("chunk {} had no corresponding row", chunk_row.chunk.index())); + }; + + match &first.item { + Either::Item(_) => { + // Collect all the related items. + let mut collected_items = Vec::new(); + for row in items { + match &row.item { + Either::Item(item) => { + collected_items.push((item.clone(), row.position.index())) + } + Either::Gap(_) => { + return Err(format!( + "unexpected gap in items chunk {}", + chunk_row.chunk.index() + )); + } + } + } + + // Sort them by their position. + collected_items.sort_unstable_by_key(|(_item, index)| *index); + + builder.push_items( + chunk_row.previous_chunk, + chunk_row.chunk, + chunk_row.next_chunk, + collected_items.into_iter().map(|(item, _index)| item), + ); + } + + Either::Gap(gap) => { + assert!(items.next().is_some(), "we just peeked the gap"); + + // We shouldn't have more than one item row for this chunk. + if items.next().is_some() { + return Err(format!( + "there shouldn't be more than one item row attached in gap chunk {}", + chunk_row.chunk.index() + )); + } + + builder.push_gap( + chunk_row.previous_chunk, + chunk_row.chunk, + chunk_row.next_chunk, + gap.clone(), + ); + } + } + } + + Ok(()) + } +} + impl Default for RelationalLinkedChunk { fn default() -> Self { Self::new() @@ -728,4 +813,59 @@ mod tests { assert!(relational_linked_chunk.chunks.is_empty()); assert!(relational_linked_chunk.items.is_empty()); } + + #[test] + fn test_rebuild_empty_linked_chunk() { + let mut builder = LinkedChunkBuilder::<3, _, _>::new(); + + let room_id = room_id!("!r0:matrix.org"); + + // When I rebuild a linked chunk from an empty store, + let relational_linked_chunk = RelationalLinkedChunk::::new(); + relational_linked_chunk.reload_chunks(room_id, &mut builder).unwrap(); + + let lc = builder.build().expect("building succeeds"); + + // The builder won't return a linked chunk. + assert!(lc.is_none()); + } + + #[test] + fn test_rebuild_linked_chunk() { + let mut builder = LinkedChunkBuilder::<3, _, _>::new(); + + let room_id = room_id!("!r0:matrix.org"); + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // new chunk + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] }, + // a gap chunk + Update::NewGapChunk { + previous: Some(CId::new(0)), + new: CId::new(1), + next: None, + gap: 'g', + }, + // another items chunk + Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None }, + // new items on 0 + Update::PushItems { at: Position::new(CId::new(2), 0), items: vec!['d', 'e', 'f'] }, + ], + ); + + relational_linked_chunk.reload_chunks(room_id, &mut builder).unwrap(); + + let lc = builder + .build() + .expect("building succeeds") + .expect("this leads to a non-empty linked chunk"); + + // The linked chunk is correctly reloaded. + assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']); + } } From cce322f9c87901082413e1c4cf1cb6a872433078 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 2 Dec 2024 11:46:41 +0100 Subject: [PATCH 651/979] test(event cache): add integration test for handling updates and reloading a linked chunk --- .../event_cache/store/integration_tests.rs | 180 +++++++++++++++++- .../src/event_cache_store.rs | 88 ++------- 2 files changed, 195 insertions(+), 73 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index e1ea613cf28..388490c98ac 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -14,13 +14,85 @@ //! Trait and macro of integration tests for `EventCacheStore` implementations. +use assert_matches::assert_matches; use async_trait::async_trait; +use matrix_sdk_common::{ + deserialized_responses::{ + AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind, + VerificationState, + }, + linked_chunk::{ChunkContent, Position, Update}, +}; +use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use ruma::{ - api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, uint, + api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, + push::Action, room_id, uint, RoomId, }; use super::DynEventCacheStore; -use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}; +use crate::{ + event_cache::Gap, + media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, +}; + +/// Create a test event with all data filled, for testing that linked chunk +/// correctly stores event data. +/// +/// Keep in sync with [`check_test_event`]. +pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent { + let encryption_info = EncryptionInfo { + sender: (*ALICE).into(), + sender_device: None, + algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "1337".to_owned(), + sender_claimed_keys: Default::default(), + }, + verification_state: VerificationState::Verified, + }; + + let event = EventFactory::new() + .text_msg(content) + .room(room_id) + .sender(*ALICE) + .into_raw_timeline() + .cast(); + + SyncTimelineEvent { + kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { + event, + encryption_info, + unsigned_encryption_info: None, + }), + push_actions: vec![Action::Notify], + } +} + +/// Check that an event created with [`make_test_event`] contains the expected +/// data. +/// +/// Keep in sync with [`make_test_event`]. +#[track_caller] +pub fn check_test_event(event: &SyncTimelineEvent, text: &str) { + // Check push actions. + let actions = &event.push_actions; + assert_eq!(actions.len(), 1); + assert_matches!(&actions[0], Action::Notify); + + // Check content. + assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => { + // Check encryption fields. + assert_eq!(d.encryption_info.sender, *ALICE); + assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => { + assert_eq!(curve25519_key, "1337"); + }); + + // Check event. + let deserialized = d.event.deserialize().unwrap(); + assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => { + assert_eq!(msg.as_original().unwrap().content.body(), text); + }); + }); +} /// `EventCacheStore` integration tests. /// @@ -34,6 +106,14 @@ pub trait EventCacheStoreIntegrationTests { /// Test replacing a MXID. async fn test_replace_media_key(&self); + + /// Test handling updates to a linked chunk and reloading these updates from + /// the store. + async fn test_handle_updates_and_rebuild_linked_chunk(&self); + + /// Test that rebuilding a linked chunk from an empty store doesn't return + /// anything. + async fn test_rebuild_empty_linked_chunk(&self); } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -182,6 +262,88 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { // Finding with the new request does work. assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello"); } + + async fn test_handle_updates_and_rebuild_linked_chunk(&self) { + use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId; + + let room_id = room_id!("!r0:matrix.org"); + + self.handle_linked_chunk_updates( + room_id, + vec![ + // new chunk + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec![ + make_test_event(room_id, "hello"), + make_test_event(room_id, "world"), + ], + }, + // a gap chunk + Update::NewGapChunk { + previous: Some(CId::new(0)), + new: CId::new(1), + next: None, + gap: Gap { prev_token: "parmesan".to_owned() }, + }, + // another items chunk + Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(2), 0), + items: vec![make_test_event(room_id, "sup")], + }, + ], + ) + .await + .unwrap(); + + // The linked chunk is correctly reloaded. + let lc = self.reload_linked_chunk(room_id).await.unwrap().expect("linked chunk not empty"); + + let mut chunks = lc.chunks(); + + { + let first = chunks.next().unwrap(); + // Note: we can't assert the previous/next chunks, as these fields and their + // getters are private. + assert_eq!(first.identifier(), CId::new(0)); + + assert_matches!(first.content(), ChunkContent::Items(events) => { + assert_eq!(events.len(), 2); + check_test_event(&events[0], "hello"); + check_test_event(&events[1], "world"); + }); + } + + { + let second = chunks.next().unwrap(); + assert_eq!(second.identifier(), CId::new(1)); + + assert_matches!(second.content(), ChunkContent::Gap(gap) => { + assert_eq!(gap.prev_token, "parmesan"); + }); + } + + { + let third = chunks.next().unwrap(); + assert_eq!(third.identifier(), CId::new(2)); + + assert_matches!(third.content(), ChunkContent::Items(events) => { + assert_eq!(events.len(), 1); + check_test_event(&events[0], "sup"); + }); + } + + assert!(chunks.next().is_none()); + } + + async fn test_rebuild_empty_linked_chunk(&self) { + // When I rebuild a linked chunk from an empty store, it's empty. + assert!(self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap().is_none()); + } } /// Macro building to allow your `EventCacheStore` implementation to run the @@ -236,6 +398,20 @@ macro_rules! event_cache_store_integration_tests { get_event_cache_store().await.unwrap().into_event_cache_store(); event_cache_store.test_replace_media_key().await; } + + #[async_test] + async fn test_handle_updates_and_rebuild_linked_chunk() { + let event_cache_store = + get_event_cache_store().await.unwrap().into_event_cache_store(); + event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await; + } + + #[async_test] + async fn test_rebuild_empty_linked_chunk() { + let event_cache_store = + get_event_cache_store().await.unwrap().into_event_cache_store(); + event_cache_store.test_rebuild_empty_linked_chunk().await; + } } }; } diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 7ecc932904a..5848122b9ce 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -797,23 +797,20 @@ mod tests { use assert_matches::assert_matches; use matrix_sdk_base::{ - deserialized_responses::{ - AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, - TimelineEventKind, VerificationState, - }, event_cache::{ - store::{EventCacheStore, EventCacheStoreError}, + store::{ + integration_tests::{check_test_event, make_test_event}, + EventCacheStore, EventCacheStoreError, + }, Gap, }, event_cache_store_integration_tests, event_cache_store_integration_tests_time, linked_chunk::{ChunkContent, ChunkIdentifier, Position, Update}, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, }; - use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; + use matrix_sdk_test::{async_test, DEFAULT_TEST_ROOM_ID}; use once_cell::sync::Lazy; - use ruma::{ - events::room::MediaSource, media::Method, mxc_uri, push::Action, room_id, uint, RoomId, - }; + use ruma::{events::room::MediaSource, media::Method, mxc_uri, room_id, uint}; use tempfile::{tempdir, TempDir}; use super::SqliteEventCacheStore; @@ -1081,57 +1078,6 @@ mod tests { assert_eq!(gaps, vec![42, 44]); } - fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent { - let encryption_info = EncryptionInfo { - sender: (*ALICE).into(), - sender_device: None, - algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { - curve25519_key: "1337".to_owned(), - sender_claimed_keys: Default::default(), - }, - verification_state: VerificationState::Verified, - }; - - let event = EventFactory::new() - .text_msg(content) - .room(room_id) - .sender(*ALICE) - .into_raw_timeline() - .cast(); - - SyncTimelineEvent { - kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { - event, - encryption_info, - unsigned_encryption_info: None, - }), - push_actions: vec![Action::Notify], - } - } - - #[track_caller] - fn check_event(event: &SyncTimelineEvent, text: &str) { - // Check push actions. - let actions = &event.push_actions; - assert_eq!(actions.len(), 1); - assert_matches!(&actions[0], Action::Notify); - - // Check content. - assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => { - // Check encryption fields. - assert_eq!(d.encryption_info.sender, *ALICE); - assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => { - assert_eq!(curve25519_key, "1337"); - }); - - // Check event. - let deserialized = d.event.deserialize().unwrap(); - assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => { - assert_eq!(msg.as_original().unwrap().content.body(), text); - }); - }); - } - #[async_test] async fn test_linked_chunk_push_items() { let store = get_event_cache_store().await.expect("creating cache store failed"); @@ -1174,9 +1120,9 @@ mod tests { assert_matches!(c.content, ChunkContent::Items(events) => { assert_eq!(events.len(), 3); - check_event(&events[0], "hello"); - check_event(&events[1], "world"); - check_event(&events[2], "who?"); + check_test_event(&events[0], "hello"); + check_test_event(&events[1], "world"); + check_test_event(&events[2], "who?"); }); } @@ -1218,7 +1164,7 @@ mod tests { assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { assert_eq!(events.len(), 1); - check_event(&events[0], "world"); + check_test_event(&events[0], "world"); }); // Make sure the position has been updated for the remaining event. @@ -1277,7 +1223,7 @@ mod tests { assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { assert_eq!(events.len(), 1); - check_event(&events[0], "hello"); + check_test_event(&events[0], "hello"); }); } @@ -1324,9 +1270,9 @@ mod tests { assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { assert_eq!(events.len(), 3); - check_event(&events[0], "hello"); - check_event(&events[1], "world"); - check_event(&events[2], "howdy"); + check_test_event(&events[0], "hello"); + check_test_event(&events[1], "world"); + check_test_event(&events[2], "howdy"); }); } @@ -1425,8 +1371,8 @@ mod tests { let c = chunks_room1.remove(0); assert_matches!(c.content, ChunkContent::Items(events) => { assert_eq!(events.len(), 2); - check_event(&events[0], "best cheese is raclette"); - check_event(&events[1], "obviously"); + check_test_event(&events[0], "best cheese is raclette"); + check_test_event(&events[1], "obviously"); }); // Check chunks from room 2. @@ -1436,7 +1382,7 @@ mod tests { let c = chunks_room2.remove(0); assert_matches!(c.content, ChunkContent::Items(events) => { assert_eq!(events.len(), 1); - check_event(&events[0], "beaufort is the best"); + check_test_event(&events[0], "beaufort is the best"); }); } } From 5da36d13c801cc6e9d685b1da1235d10d8339881 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 2 Dec 2024 12:45:05 +0100 Subject: [PATCH 652/979] fix(event cache): consider empty items chunks in the memory store --- .../src/linked_chunk/relational.rs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index c78a543755c..972e3cd8efb 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -301,8 +301,15 @@ where // Look at the first chunk item type, to reconstruct the chunk at hand. let Some(first) = items.peek() else { - // No items found for this chunk; don't include it in the builder. - return Err(format!("chunk {} had no corresponding row", chunk_row.chunk.index())); + // The only possibility is that we created an empty items chunk; mark it as + // such, and continue. + builder.push_items( + chunk_row.previous_chunk, + chunk_row.chunk, + chunk_row.next_chunk, + Vec::new(), + ); + continue; }; match &first.item { @@ -830,6 +837,31 @@ mod tests { assert!(lc.is_none()); } + #[test] + fn test_reload_linked_chunk_with_empty_items() { + let mut builder = LinkedChunkBuilder::<3, _, _>::new(); + + let room_id = room_id!("!r0:matrix.org"); + + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + // When I store an empty items chunks, + relational_linked_chunk.apply_updates( + room_id, + vec![Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }], + ); + + // It correctly gets reloaded as such. + relational_linked_chunk.reload_chunks(room_id, &mut builder).unwrap(); + + let lc = builder + .build() + .expect("building succeeds") + .expect("this leads to a non-empty linked chunk"); + + assert_items_eq!(lc, []); + } + #[test] fn test_rebuild_linked_chunk() { let mut builder = LinkedChunkBuilder::<3, _, _>::new(); From 17e17f0b9cd57be612ba4353522b06c7aae9a73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 2 Dec 2024 13:05:22 +0100 Subject: [PATCH 653/979] ci: Build the Mac framework using the reldbg profile --- .github/workflows/bindings_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 32b86d26cf8..f5bb2e1b85e 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -175,7 +175,7 @@ jobs: run: swift test - name: Build Framework - run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=release + run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg complement-crypto: name: "Run Complement Crypto tests" From 9f1e3c179bae081d834751f78f332f3eea94e2b9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 27 Nov 2024 14:44:41 +0100 Subject: [PATCH 654/979] feat(event cache): propagate linked chunk in-memory updates to storage (conditionally) --- .../src/linked_chunk/updates.rs | 2 +- crates/matrix-sdk/Cargo.toml | 2 +- crates/matrix-sdk/src/client/mod.rs | 2 +- crates/matrix-sdk/src/event_cache/mod.rs | 45 ++++- .../matrix-sdk/src/event_cache/pagination.rs | 188 ++++++++++-------- .../matrix-sdk/src/event_cache/room/events.rs | 11 +- crates/matrix-sdk/src/event_cache/room/mod.rs | 144 ++++++++++---- 7 files changed, 266 insertions(+), 128 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/updates.rs b/crates/matrix-sdk-common/src/linked_chunk/updates.rs index 8792f733921..e05944e989e 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/updates.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/updates.rs @@ -129,7 +129,7 @@ impl ObservableUpdates { /// Take new updates. /// /// Updates that have been taken will not be read again. - pub(super) fn take(&mut self) -> Vec> + pub fn take(&mut self) -> Vec> where Item: Clone, Gap: Clone, diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 18f7c6f039a..7a432a49ed1 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -97,6 +97,7 @@ matrix-sdk-sqlite = { workspace = true, optional = true } matrix-sdk-test = { workspace = true, optional = true } mime = { workspace = true } mime2ext = "0.1.53" +once_cell = { workspace = true } pin-project-lite = { workspace = true } rand = { workspace = true , optional = true } ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965", "unstable-msc3930", "unstable-msc3245-v1-compat", "unstable-msc2867"] } @@ -140,7 +141,6 @@ dirs = "5.0.1" futures-executor = { workspace = true } matrix-sdk-base = { workspace = true, features = ["testing"] } matrix-sdk-test = { workspace = true } -once_cell = { workspace = true } serde_urlencoded = "0.7.1" similar-asserts = { workspace = true } stream_assert = { workspace = true } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index dca0543e9f7..b288af97133 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -265,7 +265,7 @@ pub(crate) struct ClientInner { pub(crate) http_client: HttpClient, /// User session data. - base_client: BaseClient, + pub(super) base_client: BaseClient, /// Server capabilities, either prefilled during building or fetched from /// the server. diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 2d2aff291f0..0f4b5ffc4da 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -36,9 +36,12 @@ use std::{ use eyeball::Subscriber; use matrix_sdk_base::{ deserialized_responses::{AmbiguityChange, SyncTimelineEvent, TimelineEvent}, + event_cache::store::{EventCacheStoreError, EventCacheStoreLock}, + store_locks::LockStoreError, sync::RoomUpdates, }; use matrix_sdk_common::executor::{spawn, JoinHandle}; +use once_cell::sync::OnceCell; use ruma::{ events::{relation::RelationType, AnySyncEphemeralRoomEvent}, serde::Raw, @@ -86,6 +89,14 @@ pub enum EventCacheError { #[error("Error observed while back-paginating: {0}")] BackpaginationError(#[from] PaginatorError), + /// An error happening when interacting with storage. + #[error(transparent)] + Storage(#[from] EventCacheStoreError), + + /// An error happening when attempting to (cross-process) lock storage. + #[error(transparent)] + LockingStorage(#[from] LockStoreError), + /// The [`EventCache`] owns a weak reference to the [`Client`] it pertains /// to. It's possible this weak reference points to nothing anymore, at /// times where we try to use the client. @@ -141,6 +152,7 @@ impl EventCache { Self { inner: Arc::new(EventCacheInner { client, + store: Default::default(), multiple_room_updates_lock: Default::default(), by_room: Default::default(), drop_handles: Default::default(), @@ -149,6 +161,18 @@ impl EventCache { } } + /// Enable storing updates to storage, and reload events from storage. + /// + /// Has an effect only the first time it's called. It's safe to call it + /// multiple times. + pub fn enable_storage(&self) -> Result<()> { + let _ = self.inner.store.get_or_try_init::<_, EventCacheError>(|| { + let client = self.inner.client()?; + Ok(client.event_cache_store().clone()) + })?; + Ok(()) + } + /// Starts subscribing the [`EventCache`] to sync responses, if not done /// before. /// @@ -213,7 +237,9 @@ impl EventCache { async move { while ignore_user_list_stream.next().await.is_some() { info!("received an ignore user list change"); - inner.clear_all_rooms().await; + if let Err(err) = inner.clear_all_rooms().await { + error!("error when clearing room storage: {err}"); + } } } .instrument(span) @@ -247,7 +273,9 @@ impl EventCache { // no way to reconcile at the moment! // TODO: implement Smart Matching™, warn!(num_skipped, "Lagged behind room updates, clearing all rooms"); - inner.clear_all_rooms().await; + if let Err(err) = inner.clear_all_rooms().await { + error!("error when clearing storage: {err}"); + } } Err(RecvError::Closed) => { @@ -317,6 +345,12 @@ struct EventCacheInner { /// on the owning client. client: WeakClient, + /// Reference to the underlying store. + /// + /// Set to none if we shouldn't use storage for reading / writing linked + /// chunks. + store: Arc>, + /// A lock used when many rooms must be updated at once. /// /// [`Mutex`] is “fair”, as it is implemented as a FIFO. It is important to @@ -348,7 +382,7 @@ impl EventCacheInner { } /// Clears all the room's data. - async fn clear_all_rooms(&self) { + async fn clear_all_rooms(&self) -> Result<()> { // Note: one must NOT clear the `by_room` map, because if something subscribed // to a room update, they would never get any new update for that room, since // re-creating the `RoomEventCache` would create a new unrelated sender. @@ -362,8 +396,10 @@ impl EventCacheInner { // error if there aren't any.) let _ = room.inner.sender.send(RoomEventCacheUpdate::Clear); // Clear all the room state. - room.inner.state.write().await.reset(); + room.inner.state.write().await.reset().await?; } + + Ok(()) } /// Handles a single set of room updates at once. @@ -424,6 +460,7 @@ impl EventCacheInner { let room_event_cache = RoomEventCache::new( self.client.clone(), + self.store.clone(), room_id.to_owned(), self.all_events.clone(), ); diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index c5b605ab6e0..16b131c1836 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -134,12 +134,11 @@ impl RoomPagination { // Make sure the `RoomEvents` isn't updated while we are saving events from // backpagination. let mut state = self.inner.state.write().await; - let room_events = &mut state.events; // Check that the previous token still exists; otherwise it's a sign that the // room's timeline has been cleared. let gap_identifier = if let Some(token) = prev_token { - let gap_identifier = room_events.chunk_identifier(|chunk| { + let gap_identifier = state.events().chunk_identifier(|chunk| { matches!(chunk.content(), ChunkContent::Gap(Gap { ref prev_token }) if *prev_token == token) }); @@ -156,84 +155,87 @@ impl RoomPagination { let prev_token = paginator.prev_batch_token().map(|prev_token| Gap { prev_token }); - // Note: The chunk could be empty. - // - // If there's any event, they are presented in reverse order (i.e. the first one - // should be prepended first). - - let sync_events = events - .iter() - // Reverse the order of the events as `/messages` has been called with `dir=b` - // (backward). The `RoomEvents` API expects the first event to be the oldest. - .rev() - .cloned() - .map(SyncTimelineEvent::from); - - // There is a `token`/gap, let's replace it by new events! - if let Some(gap_identifier) = gap_identifier { - let new_position = { - // Replace the gap by new events. - let new_chunk = room_events - .replace_gap_at(sync_events, gap_identifier) - // SAFETY: we are sure that `gap_identifier` represents a valid - // `ChunkIdentifier` for a `Gap` chunk. - .expect("The `gap_identifier` must represent a `Gap`"); - - new_chunk.first_position() - }; - - // And insert a new gap if there is any `prev_token`. - if let Some(prev_token_gap) = prev_token { - room_events - .insert_gap_at(prev_token_gap, new_position) - // SAFETY: we are sure that `new_position` represents a valid - // `ChunkIdentifier` for an `Item` chunk. - .expect("The `new_position` must represent an `Item`"); - } + Ok(Some(state.with_events_mut(move |room_events| { + // Note: The chunk could be empty. + // + // If there's any event, they are presented in reverse order (i.e. the first one + // should be prepended first). + + let sync_events = events + .iter() + // Reverse the order of the events as `/messages` has been called with `dir=b` + // (backward). The `RoomEvents` API expects the first event to be the oldest. + .rev() + .cloned() + .map(SyncTimelineEvent::from); + + + // There is a `token`/gap, let's replace it by new events! + if let Some(gap_identifier) = gap_identifier { + let new_position = { + // Replace the gap by new events. + let new_chunk = room_events + .replace_gap_at(sync_events, gap_identifier) + // SAFETY: we are sure that `gap_identifier` represents a valid + // `ChunkIdentifier` for a `Gap` chunk. + .expect("The `gap_identifier` must represent a `Gap`"); + + new_chunk.first_position() + }; + + // And insert a new gap if there is any `prev_token`. + if let Some(prev_token_gap) = prev_token { + room_events + .insert_gap_at(prev_token_gap, new_position) + // SAFETY: we are sure that `new_position` represents a valid + // `ChunkIdentifier` for an `Item` chunk. + .expect("The `new_position` must represent an `Item`"); + } - trace!("replaced gap with new events from backpagination"); + trace!("replaced gap with new events from backpagination"); - // TODO: implement smarter reconciliation later - //let _ = self.sender.send(RoomEventCacheUpdate::Prepend { events }); + // TODO: implement smarter reconciliation later + //let _ = self.sender.send(RoomEventCacheUpdate::Prepend { events }); - return Ok(Some(BackPaginationOutcome { events, reached_start })); - } + return BackPaginationOutcome { events, reached_start }; + } - // There is no `token`/gap identifier. Let's assume we must prepend the new - // events. - let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); + // There is no `token`/gap identifier. Let's assume we must prepend the new + // events. + let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); - match first_event_pos { - // Is there a first item? Insert at this position. - Some(first_event_pos) => { - if let Some(prev_token_gap) = prev_token { - room_events + match first_event_pos { + // Is there a first item? Insert at this position. + Some(first_event_pos) => { + if let Some(prev_token_gap) = prev_token { + room_events .insert_gap_at(prev_token_gap, first_event_pos) // SAFETY: The `first_event_pos` can only be an `Item` chunk, it's // an invariant of `LinkedChunk`. Also, it can only represent a valid // `ChunkIdentifier` as the data structure isn't modified yet. .expect("`first_event_pos` must point to a valid `Item` chunk when inserting a gap"); - } + } - room_events + room_events .insert_events_at(sync_events, first_event_pos) // SAFETY: The `first_event_pos` can only be an `Item` chunk, it's // an invariant of `LinkedChunk`. The chunk it points to has not been // removed. .expect("The `first_event_pos` must point to a valid `Item` chunk when inserting events"); - } - - // There is no first item. Let's simply push. - None => { - if let Some(prev_token_gap) = prev_token { - room_events.push_gap(prev_token_gap); } - room_events.push_events(sync_events); + // There is no first item. Let's simply push. + None => { + if let Some(prev_token_gap) = prev_token { + room_events.push_gap(prev_token_gap); + } + + room_events.push_events(sync_events); + } } - } - Ok(Some(BackPaginationOutcome { events, reached_start })) + BackPaginationOutcome { events, reached_start } + }).await?)) } /// Get the latest pagination token, as stored in the room events linked @@ -254,7 +256,7 @@ impl RoomPagination { // Scope for the lock guard. let state = self.inner.state.read().await; // Fast-path: we do have a previous-batch token already. - if let Some(found) = get_oldest(&state.events) { + if let Some(found) = get_oldest(state.events()) { return Some(found); } // If we've already waited for an initial previous-batch token before, @@ -273,7 +275,7 @@ impl RoomPagination { let _ = timeout(wait_time, self.inner.pagination_batch_token_notifier.notified()).await; let mut state = self.inner.state.write().await; - let token = get_oldest(&state.events); + let token = get_oldest(state.events()); state.waited_for_initial_prev_token = true; token } @@ -341,15 +343,22 @@ mod tests { let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap(); // When I only have events in a room, - room_event_cache.inner.state.write().await.events.push_events([ - SyncTimelineEvent::new(sync_timeline_event!({ - "sender": "b@z.h", - "type": "m.room.message", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content": { "body":"yolo", "msgtype": "m.text" }, - })), - ]); + room_event_cache + .inner + .state + .write() + .await + .with_events_mut(|events| { + events.push_events([SyncTimelineEvent::new(sync_timeline_event!({ + "sender": "b@z.h", + "type": "m.room.message", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content": { "body":"yolo", "msgtype": "m.text" }, + }))]) + }) + .await + .unwrap(); let pagination = room_event_cache.pagination(); @@ -359,7 +368,7 @@ mod tests { assert!(found.is_none()); // Reset waited_for_initial_prev_token state. - pagination.inner.state.write().await.reset(); + pagination.inner.state.write().await.reset().await.unwrap(); // If I wait for a back-pagination token for 0 seconds, let before = Instant::now(); @@ -371,7 +380,7 @@ mod tests { assert!(waited.as_secs() < 1); // Reset waited_for_initial_prev_token state. - pagination.inner.state.write().await.reset(); + pagination.inner.state.write().await.reset().await.unwrap(); // If I wait for a back-pagination token for 1 second, let before = Instant::now(); @@ -400,15 +409,23 @@ mod tests { // When I have events and multiple gaps, in a room, { - let room_events = &mut room_event_cache.inner.state.write().await.events; - room_events.push_gap(Gap { prev_token: expected_token.clone() }); - room_events.push_events([SyncTimelineEvent::new(sync_timeline_event!({ - "sender": "b@z.h", - "type": "m.room.message", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content": { "body":"yolo", "msgtype": "m.text" }, - }))]); + room_event_cache + .inner + .state + .write() + .await + .with_events_mut(|room_events| { + room_events.push_gap(Gap { prev_token: expected_token.clone() }); + room_events.push_events([SyncTimelineEvent::new(sync_timeline_event!({ + "sender": "b@z.h", + "type": "m.room.message", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content": { "body":"yolo", "msgtype": "m.text" }, + }))]); + }) + .await + .unwrap(); } let pagination = room_event_cache.pagination(); @@ -463,8 +480,11 @@ mod tests { .state .write() .await - .events - .push_gap(Gap { prev_token: cloned_expected_token }); + .with_events_mut(|events| { + events.push_gap(Gap { prev_token: cloned_expected_token }) + }) + .await + .unwrap(); }); let pagination = room_event_cache.pagination(); diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 670208fa188..eec213c93a1 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -16,7 +16,10 @@ use std::cmp::Ordering; use eyeball_im::VectorDiff; pub use matrix_sdk_base::event_cache::{Event, Gap}; -use matrix_sdk_base::{event_cache::store::DEFAULT_CHUNK_CAPACITY, linked_chunk::AsVector}; +use matrix_sdk_base::{ + event_cache::store::DEFAULT_CHUNK_CAPACITY, + linked_chunk::{AsVector, ObservableUpdates}, +}; use matrix_sdk_common::linked_chunk::{ Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position, }; @@ -185,6 +188,12 @@ impl RoomEvents { self.chunks_updates_as_vectordiffs.take() } + /// Get a mutable reference to the [`LinkedChunk`] updates, aka + /// [`ObservableUpdates`]. + pub(super) fn updates(&mut self) -> &mut ObservableUpdates { + self.chunks.updates().expect("this is always built with an update history in the ctor") + } + /// Deduplicate `events` considering all events in `Self::chunks`. /// /// The returned tuple contains (i) the unique events, and (ii) the diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 7b007547016..1fd256cb07e 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -16,11 +16,13 @@ use std::{collections::BTreeMap, fmt, sync::Arc}; -use events::{Gap, RoomEvents}; +use events::Gap; use matrix_sdk_base::{ deserialized_responses::{AmbiguityChange, SyncTimelineEvent}, + event_cache::store::EventCacheStoreLock, sync::{JoinedRoomUpdate, LeftRoomUpdate, Timeline}, }; +use once_cell::sync::OnceCell; use ruma::{ events::{ relation::RelationType, @@ -63,10 +65,11 @@ impl RoomEventCache { /// Create a new [`RoomEventCache`] using the given room and store. pub(super) fn new( client: WeakClient, + store: Arc>, room_id: OwnedRoomId, all_events_cache: Arc>, ) -> Self { - Self { inner: Arc::new(RoomEventCacheInner::new(client, room_id, all_events_cache)) } + Self { inner: Arc::new(RoomEventCacheInner::new(client, store, room_id, all_events_cache)) } } /// Subscribe to room updates for this room, after getting the initial list @@ -77,7 +80,7 @@ impl RoomEventCache { &self, ) -> Result<(Vec, Receiver)> { let state = self.inner.state.read().await; - let events = state.events.events().map(|(_position, item)| item.clone()).collect(); + let events = state.events().events().map(|(_position, item)| item.clone()).collect(); Ok((events, self.inner.sender.subscribe())) } @@ -99,7 +102,7 @@ impl RoomEventCache { } let state = self.inner.state.read().await; - for (_pos, event) in state.events.revents() { + for (_pos, event) in state.events().revents() { if event.event_id().as_deref() == Some(event_id) { return Some(event.clone()); } @@ -231,19 +234,18 @@ impl RoomEventCacheInner { /// to handle new timeline events. fn new( client: WeakClient, + store: Arc>, room_id: OwnedRoomId, all_events_cache: Arc>, ) -> Self { let sender = Sender::new(32); let weak_room = WeakRoom::new(client, room_id); + let room_id = weak_room.room_id().to_owned(); Self { - room_id: weak_room.room_id().to_owned(), - state: RwLock::new(RoomEventCacheState { - events: RoomEvents::default(), - waited_for_initial_prev_token: false, - }), + room_id: room_id.clone(), + state: RwLock::new(RoomEventCacheState::new(room_id, store)), all_events: all_events_cache, sender, pagination_batch_token_notifier: Default::default(), @@ -355,14 +357,14 @@ impl RoomEventCacheInner { let mut state = self.state.write().await; // Reset the room's state. - state.reset(); + state.reset().await?; // Propagate to observers. let _ = self.sender.send(RoomEventCacheUpdate::Clear); // Push the new events. self.append_events_locked_impl( - &mut state.events, + &mut state, sync_timeline_events, prev_batch.clone(), ephemeral_events, @@ -385,8 +387,9 @@ impl RoomEventCacheInner { ephemeral_events: Vec>, ambiguity_changes: BTreeMap, ) -> Result<()> { + let mut state = self.state.write().await; self.append_events_locked_impl( - &mut self.state.write().await.events, + &mut state, sync_timeline_events, prev_batch, ephemeral_events, @@ -471,7 +474,7 @@ impl RoomEventCacheInner { /// This is a private implementation. It must not be exposed publicly. async fn append_events_locked_impl( &self, - room_events: &mut RoomEvents, + state: &mut RoomEventCacheState, sync_timeline_events: Vec, prev_batch: Option, ephemeral_events: Vec>, @@ -488,11 +491,15 @@ impl RoomEventCacheInner { // Add the previous back-pagination token (if present), followed by the timeline // events themselves. { - if let Some(prev_token) = &prev_batch { - room_events.push_gap(Gap { prev_token: prev_token.clone() }); - } + state + .with_events_mut(|room_events| { + if let Some(prev_token) = &prev_batch { + room_events.push_gap(Gap { prev_token: prev_token.clone() }); + } - room_events.push_events(sync_timeline_events.clone()); + room_events.push_events(sync_timeline_events.clone()); + }) + .await?; let mut cache = self.all_events.write().await; for ev in &sync_timeline_events { @@ -533,29 +540,94 @@ impl RoomEventCacheInner { } } -/// State for a single room's event cache. -/// -/// This contains all inner mutable state that ought to be updated at the same -/// time. -pub(super) struct RoomEventCacheState { - /// The events of the room. - pub events: RoomEvents, - - /// Have we ever waited for a previous-batch-token to come from sync, in the - /// context of pagination? We do this at most once per room, the first - /// time we try to run backward pagination. We reset that upon clearing - /// the timeline events. - pub waited_for_initial_prev_token: bool, -} +// Use a private module to hide `events` to this parent module. +mod private { + use std::sync::Arc; + + use matrix_sdk_base::event_cache::store::EventCacheStoreLock; + use once_cell::sync::OnceCell; + use ruma::OwnedRoomId; -impl RoomEventCacheState { - /// Resets this data structure as if it were brand new. - pub(super) fn reset(&mut self) { - self.events.reset(); - self.waited_for_initial_prev_token = false; + use super::events::RoomEvents; + use crate::event_cache::EventCacheError; + + /// State for a single room's event cache. + /// + /// This contains all the inner mutable states that ought to be updated at + /// the same time. + pub struct RoomEventCacheState { + /// The room this state relates to. + room: OwnedRoomId, + + /// Reference to the underlying backing store. + /// + /// Set to none if the room shouldn't read the linked chunk from + /// storage, and shouldn't store updates to storage. + store: Arc>, + + /// The events of the room. + events: RoomEvents, + + /// Have we ever waited for a previous-batch-token to come from sync, in + /// the context of pagination? We do this at most once per room, + /// the first time we try to run backward pagination. We reset + /// that upon clearing the timeline events. + pub waited_for_initial_prev_token: bool, + } + + impl RoomEventCacheState { + /// Create a new empty state. + pub fn new(room: OwnedRoomId, store: Arc>) -> Self { + Self { + room, + store, + events: RoomEvents::default(), + waited_for_initial_prev_token: false, + } + } + + /// Propagate changes to the underlying storage. + async fn propagate_changes(&mut self) -> Result<(), EventCacheError> { + let updates = self.events.updates().take(); + + if !updates.is_empty() { + if let Some(store) = self.store.get() { + let locked = store.lock().await?; + locked.handle_linked_chunk_updates(&self.room, updates).await?; + } + } + + Ok(()) + } + + /// Resets this data structure as if it were brand new. + pub async fn reset(&mut self) -> Result<(), EventCacheError> { + self.events.reset(); + self.propagate_changes().await?; + self.waited_for_initial_prev_token = false; + Ok(()) + } + + /// Returns a read-only reference to the underlying events. + pub fn events(&self) -> &RoomEvents { + &self.events + } + + /// Gives a temporary mutable handle to the underlying in-memory events, + /// and will propagate changes to the storage once done. + pub async fn with_events_mut O>( + &mut self, + func: F, + ) -> Result { + let output = func(&mut self.events); + self.propagate_changes().await?; + Ok(output) + } } } +pub(super) use private::RoomEventCacheState; + #[cfg(test)] mod tests { use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; From 019de4ffa01a50043da047c616d819fbe9269cd6 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 28 Nov 2024 12:46:57 +0100 Subject: [PATCH 655/979] test(event cache): add a test that the event cache correctly stores updates --- crates/matrix-sdk/src/event_cache/room/mod.rs | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 1fd256cb07e..399d7816044 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -630,15 +630,28 @@ pub(super) use private::RoomEventCacheState; #[cfg(test)] mod tests { + use std::sync::Arc; + + use assert_matches::assert_matches; + use assert_matches2::assert_let; + use matrix_sdk_base::{ + event_cache::store::{EventCacheStore as _, MemoryStore}, + linked_chunk::ChunkContent, + store::StoreConfig, + sync::{JoinedRoomUpdate, Timeline}, + }; use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; - use matrix_sdk_test::{async_test, event_factory::EventFactory}; + use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE}; use ruma::{ event_id, - events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation}, + events::{ + relation::RelationType, room::message::RoomMessageEventContentWithoutRelation, + AnySyncMessageLikeEvent, AnySyncTimelineEvent, + }, room_id, user_id, RoomId, }; - use crate::test_utils::logged_in_client; + use crate::test_utils::{client::MockClientBuilder, logged_in_client}; #[async_test] async fn test_event_with_redaction_relation() { @@ -875,6 +888,73 @@ mod tests { assert_eq!(related_event_id, associated_related_id); } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. + #[async_test] + async fn test_write_to_storage() { + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + let event_cache_store = Arc::new(MemoryStore::new()); + + let client = MockClientBuilder::new("http://localhost".to_owned()) + .store_config( + StoreConfig::new("hodlor".to_owned()).event_cache_store(event_cache_store.clone()), + ) + .build() + .await; + + let event_cache = client.event_cache(); + + // Don't forget to subscribe and like^W enable storage! + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + // Propagate an update for a message and a prev-batch token. + let timeline = Timeline { + limited: false, + prev_batch: Some("raclette".to_owned()), + events: vec![f.text_msg("hey yo").sender(*ALICE).into_sync()], + }; + + room_event_cache + .inner + .handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() }) + .await + .unwrap(); + + let linked_chunk = event_cache_store.reload_linked_chunk(room_id).await.unwrap().unwrap(); + + assert_eq!(linked_chunk.chunks().count(), 3); + + let mut chunks = linked_chunk.chunks(); + + // Invariant: there's always an empty items chunk at the beginning. + assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => { + assert_eq!(events.len(), 0) + }); + + // Then we have the gap. + assert_matches!(chunks.next().unwrap().content(), ChunkContent::Gap(gap) => { + assert_eq!(gap.prev_token, "raclette"); + }); + + // Then we have the stored event. + assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => { + assert_eq!(events.len(), 1); + let deserialized = events[0].raw().deserialize().unwrap(); + assert_let!(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) = deserialized); + assert_eq!(msg.as_original().unwrap().content.body(), "hey yo"); + }); + + // That's all, folks! + assert!(chunks.next().is_none()); + } + async fn assert_relations( room_id: &RoomId, original_event: SyncTimelineEvent, From 7de74e2c04e8efaa57274d3c6d4632ff8d94188e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 2 Dec 2024 12:26:44 +0100 Subject: [PATCH 656/979] feat(event cache): reload the linked chunk from the store, if storage's enabled --- .../src/event_cache/deduplicator.rs | 30 +++++++++++---- crates/matrix-sdk/src/event_cache/mod.rs | 6 ++- .../matrix-sdk/src/event_cache/room/events.rs | 20 ++++++++-- crates/matrix-sdk/src/event_cache/room/mod.rs | 37 ++++++++++--------- 4 files changed, 63 insertions(+), 30 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs index 9a6e8d933eb..7dbd54eeca0 100644 --- a/crates/matrix-sdk/src/event_cache/deduplicator.rs +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -18,6 +18,7 @@ use std::{collections::BTreeSet, fmt, sync::Mutex}; use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; +use tracing::warn; use super::room::events::{Event, RoomEvents}; @@ -46,16 +47,29 @@ impl Deduplicator { const APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS: usize = 1_000; const DESIRED_FALSE_POSITIVE_RATE: f64 = 0.01; - /// Create a new `Deduplicator`. + /// Create a new `Deduplicator` with no prior knowledge of known events. + #[cfg(test)] pub fn new() -> Self { - Self { - bloom_filter: Mutex::new( - GrowableBloomBuilder::new() - .estimated_insertions(Self::APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS) - .desired_error_ratio(Self::DESIRED_FALSE_POSITIVE_RATE) - .build(), - ), + Self::with_initial_events(std::iter::empty()) + } + + /// Create a new `Deduplicator` filled with initial events. + /// + /// This won't detect duplicates in the initial events, only learn about + /// those events. + pub fn with_initial_events<'a>(events: impl Iterator) -> Self { + let mut bloom_filter = GrowableBloomBuilder::new() + .estimated_insertions(Self::APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS) + .desired_error_ratio(Self::DESIRED_FALSE_POSITIVE_RATE) + .build(); + for e in events { + let Some(event_id) = e.event_id() else { + warn!("initial event in deduplicator had no event id"); + continue; + }; + bloom_filter.insert(event_id); } + Self { bloom_filter: Mutex::new(bloom_filter) } } /// Scan a collection of events and detect duplications. diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 0f4b5ffc4da..cc0ac6d14f4 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -42,6 +42,7 @@ use matrix_sdk_base::{ }; use matrix_sdk_common::executor::{spawn, JoinHandle}; use once_cell::sync::OnceCell; +use room::RoomEventCacheState; use ruma::{ events::{relation::RelationType, AnySyncEphemeralRoomEvent}, serde::Raw, @@ -458,9 +459,12 @@ impl EventCacheInner { return Ok(room.clone()); } + let room_state = + RoomEventCacheState::new(room_id.to_owned(), self.store.clone()).await?; + let room_event_cache = RoomEventCache::new( self.client.clone(), - self.store.clone(), + room_state, room_id.to_owned(), self.all_events.clone(), ); diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index eec213c93a1..6313ac3d3fa 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -52,14 +52,28 @@ impl Default for RoomEvents { impl RoomEvents { /// Build a new [`RoomEvents`] struct with zero events. pub fn new() -> Self { - let mut chunks = LinkedChunk::new_with_update_history(); + Self::with_initial_chunks(None) + } + + /// Build a new [`RoomEvents`] struct with prior chunks knowledge. + /// + /// The provided [`LinkedChunk`] must have been built with update history. + pub fn with_initial_chunks( + chunks: Option>, + ) -> Self { + let mut chunks = chunks.unwrap_or_else(LinkedChunk::new_with_update_history); + let chunks_updates_as_vectordiffs = chunks .as_vector() // SAFETY: The `LinkedChunk` has been built with `new_with_update_history`, so // `as_vector` must return `Some(…)`. - .expect("`LinkedChunk` must have been constructor with `new_with_update_history`"); + .expect("`LinkedChunk` must have been built with `new_with_update_history`"); + + // Let the deduplicator know about initial events. + let deduplicator = + Deduplicator::with_initial_events(chunks.items().map(|(_pos, event)| event)); - Self { chunks, chunks_updates_as_vectordiffs, deduplicator: Deduplicator::new() } + Self { chunks, chunks_updates_as_vectordiffs, deduplicator } } /// Clear all events. diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 399d7816044..2e2e6e30644 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -19,10 +19,8 @@ use std::{collections::BTreeMap, fmt, sync::Arc}; use events::Gap; use matrix_sdk_base::{ deserialized_responses::{AmbiguityChange, SyncTimelineEvent}, - event_cache::store::EventCacheStoreLock, sync::{JoinedRoomUpdate, LeftRoomUpdate, Timeline}, }; -use once_cell::sync::OnceCell; use ruma::{ events::{ relation::RelationType, @@ -65,11 +63,11 @@ impl RoomEventCache { /// Create a new [`RoomEventCache`] using the given room and store. pub(super) fn new( client: WeakClient, - store: Arc>, + state: RoomEventCacheState, room_id: OwnedRoomId, all_events_cache: Arc>, ) -> Self { - Self { inner: Arc::new(RoomEventCacheInner::new(client, store, room_id, all_events_cache)) } + Self { inner: Arc::new(RoomEventCacheInner::new(client, state, room_id, all_events_cache)) } } /// Subscribe to room updates for this room, after getting the initial list @@ -234,18 +232,15 @@ impl RoomEventCacheInner { /// to handle new timeline events. fn new( client: WeakClient, - store: Arc>, + state: RoomEventCacheState, room_id: OwnedRoomId, all_events_cache: Arc>, ) -> Self { let sender = Sender::new(32); - let weak_room = WeakRoom::new(client, room_id); - let room_id = weak_room.room_id().to_owned(); - Self { - room_id: room_id.clone(), - state: RwLock::new(RoomEventCacheState::new(room_id, store)), + room_id: weak_room.room_id().to_owned(), + state: RwLock::new(state), all_events: all_events_cache, sender, pagination_batch_token_notifier: Default::default(), @@ -576,14 +571,20 @@ mod private { } impl RoomEventCacheState { - /// Create a new empty state. - pub fn new(room: OwnedRoomId, store: Arc>) -> Self { - Self { - room, - store, - events: RoomEvents::default(), - waited_for_initial_prev_token: false, - } + /// Create a new state, or reload it from storage if it's been enabled. + pub async fn new( + room: OwnedRoomId, + store: Arc>, + ) -> Result { + let events = if let Some(store) = store.get() { + let locked = store.lock().await?; + let chunks = locked.reload_linked_chunk(&room).await?; + RoomEvents::with_initial_chunks(chunks) + } else { + RoomEvents::default() + }; + + Ok(Self { room, store, events, waited_for_initial_prev_token: false }) } /// Propagate changes to the underlying storage. From 2f9866cf043b62e48b47bf3e4eb16a6806dedf37 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 2 Dec 2024 13:19:21 +0100 Subject: [PATCH 657/979] test(event cache): test that the event cache correctly reads updates --- crates/matrix-sdk/src/event_cache/room/mod.rs | 110 +++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 2e2e6e30644..8e6b6c4a942 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -636,13 +636,16 @@ mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; use matrix_sdk_base::{ - event_cache::store::{EventCacheStore as _, MemoryStore}, - linked_chunk::ChunkContent, + event_cache::{ + store::{EventCacheStore as _, MemoryStore}, + Gap, + }, + linked_chunk::{ChunkContent, ChunkIdentifier, Position, Update}, store::StoreConfig, sync::{JoinedRoomUpdate, Timeline}, }; use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; - use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE}; + use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB}; use ruma::{ event_id, events::{ @@ -956,6 +959,107 @@ mod tests { assert!(chunks.next().is_none()); } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. + #[async_test] + async fn test_load_from_storage() { + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + let event_cache_store = Arc::new(MemoryStore::new()); + + let event_id1 = event_id!("$1"); + let event_id2 = event_id!("$2"); + + let ev1 = f.text_msg("hello world").sender(*ALICE).event_id(event_id1).into_sync(); + let ev2 = f.text_msg("how's it going").sender(*BOB).event_id(event_id2).into_sync(); + + // Prefill the store with some data. + event_cache_store + .handle_linked_chunk_updates( + room_id, + vec![ + // An empty items chunk. + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + // A gap chunk. + Update::NewGapChunk { + previous: Some(ChunkIdentifier::new(0)), + // Chunk IDs aren't supposed to be ordered, so use a random value here. + new: ChunkIdentifier::new(42), + next: None, + gap: Gap { prev_token: "cheddar".to_owned() }, + }, + // Another items chunk, non-empty this time. + Update::NewItemsChunk { + previous: Some(ChunkIdentifier::new(42)), + new: ChunkIdentifier::new(1), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(1), 0), + items: vec![ev1.clone()], + }, + // And another items chunk, non-empty again. + Update::NewItemsChunk { + previous: Some(ChunkIdentifier::new(1)), + new: ChunkIdentifier::new(2), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(2), 0), + items: vec![ev2.clone()], + }, + ], + ) + .await + .unwrap(); + + let client = MockClientBuilder::new("http://localhost".to_owned()) + .store_config( + StoreConfig::new("hodlor".to_owned()).event_cache_store(event_cache_store.clone()), + ) + .build() + .await; + + let event_cache = client.event_cache(); + + // Don't forget to subscribe and like^W enable storage! + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (items, _stream) = room_event_cache.subscribe().await.unwrap(); + + // The reloaded room must contain the two events. + assert_eq!(items.len(), 2); + assert_eq!(items[0].event_id().unwrap(), event_id1); + assert_eq!(items[1].event_id().unwrap(), event_id2); + + // A new update with one of these events leads to deduplication. + let timeline = Timeline { limited: false, prev_batch: None, events: vec![ev1] }; + room_event_cache + .inner + .handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() }) + .await + .unwrap(); + + // The stream doesn't report these changes *yet*. Use the items vector given + // when subscribing, to check that the items correspond to their new + // positions. The duplicated item is removed (so it's not the first + // element anymore), and it's added to the back of the list. + let (items, _stream) = room_event_cache.subscribe().await.unwrap(); + assert_eq!(items.len(), 2); + assert_eq!(items[0].event_id().unwrap(), event_id2); + assert_eq!(items[1].event_id().unwrap(), event_id1); + } + async fn assert_relations( room_id: &RoomId, original_event: SyncTimelineEvent, From 51cfaaacee38bddbb5203e303a5e6b4939b43a1b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 2 Dec 2024 15:55:47 +0100 Subject: [PATCH 658/979] refactor: Rename `TimelineMetadata::all_events` to `all_remote_events`. This patch renames the `all_events` field to `all_remote_events` in `TimelineMetada` for the sake of clarity. --- .../src/timeline/controller/mod.rs | 2 +- .../src/timeline/controller/state.rs | 45 ++++++++++--------- .../src/timeline/read_receipts.rs | 12 ++--- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 42e70eba3e1..29cd9fdd380 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1529,7 +1529,7 @@ impl TimelineController { /// it's folded into another timeline item. pub(crate) async fn latest_event_id(&self) -> Option { let state = self.state.read().await; - state.meta.all_events.back().map(|event_meta| &event_meta.event_id).cloned() + state.meta.all_remote_events.back().map(|event_meta| &event_meta.event_id).cloned() } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index dd4ca784f5d..d8a26daa53c 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -601,7 +601,7 @@ impl TimelineStateTransaction<'_> { read_receipts: if settings.track_read_receipts && should_add { self.meta.read_receipts.compute_event_receipts( &event_id, - &self.meta.all_events, + &self.meta.all_remote_events, matches!(position, TimelineItemPosition::End { .. }), ) } else { @@ -678,8 +678,8 @@ impl TimelineStateTransaction<'_> { items.commit(); } - /// Add or update an event in the [`TimelineMetadata::all_events`] - /// collection. + /// Add or update a remote event in the + /// [`TimelineMetadata::all_remote_events`] collection. /// /// This method also adjusts read receipt if needed. /// @@ -693,38 +693,43 @@ impl TimelineStateTransaction<'_> { room_data_provider: &P, settings: &TimelineSettings, ) -> bool { - // Detect if an event already exists in [`TimelineMetadata::all_events`]. + // Detect if an event already exists in [`TimelineMetadata::all_remote_events`]. // // Returns its position, in this case. fn event_already_exists( new_event_id: &EventId, - all_events: &VecDeque, + all_remote_events: &VecDeque, ) -> Option { - all_events.iter().position(|EventMeta { event_id, .. }| event_id == new_event_id) + all_remote_events.iter().position(|EventMeta { event_id, .. }| event_id == new_event_id) } match position { TimelineItemPosition::Start { .. } => { - if let Some(pos) = event_already_exists(event_meta.event_id, &self.meta.all_events) + if let Some(pos) = + event_already_exists(event_meta.event_id, &self.meta.all_remote_events) { - self.meta.all_events.remove(pos); + self.meta.all_remote_events.remove(pos); } - self.meta.all_events.push_front(event_meta.base_meta()) + self.meta.all_remote_events.push_front(event_meta.base_meta()) } TimelineItemPosition::End { .. } => { - if let Some(pos) = event_already_exists(event_meta.event_id, &self.meta.all_events) + if let Some(pos) = + event_already_exists(event_meta.event_id, &self.meta.all_remote_events) { - self.meta.all_events.remove(pos); + self.meta.all_remote_events.remove(pos); } - self.meta.all_events.push_back(event_meta.base_meta()); + self.meta.all_remote_events.push_back(event_meta.base_meta()); } TimelineItemPosition::UpdateDecrypted { .. } => { - if let Some(event) = - self.meta.all_events.iter_mut().find(|e| e.event_id == event_meta.event_id) + if let Some(event) = self + .meta + .all_remote_events + .iter_mut() + .find(|e| e.event_id == event_meta.event_id) { if event.visible != event_meta.visible { event.visible = event_meta.visible; @@ -899,9 +904,9 @@ pub(in crate::timeline) struct TimelineMetadata { /// the device has terabytes of RAM. next_internal_id: u64, - /// List of all the events as received in the timeline, even the ones that - /// are discarded in the timeline items. - pub all_events: VecDeque, + /// List of all the remote events as received in the timeline, even the ones + /// that are discarded in the timeline items. + pub all_remote_events: VecDeque, /// State helping matching reactions to their associated events, and /// stashing pending reactions. @@ -945,7 +950,7 @@ impl TimelineMetadata { ) -> Self { Self { own_user_id, - all_events: Default::default(), + all_remote_events: Default::default(), next_internal_id: Default::default(), reactions: Default::default(), pending_poll_events: Default::default(), @@ -965,7 +970,7 @@ impl TimelineMetadata { pub(crate) fn clear(&mut self) { // Note: we don't clear the next internal id to avoid bad cases of stale unique // ids across timeline clears. - self.all_events.clear(); + self.all_remote_events.clear(); self.reactions.clear(); self.pending_poll_events.clear(); self.pending_edits.clear(); @@ -993,7 +998,7 @@ impl TimelineMetadata { // We can make early returns here because we know all events since the end of // the timeline, so the first event encountered is the oldest one. - for meta in self.all_events.iter().rev() { + for meta in self.all_remote_events.iter().rev() { if meta.event_id == event_a { return Some(RelativePosition::Before); } diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs index 4287877670a..a97cbdc9340 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs @@ -390,7 +390,7 @@ impl TimelineStateTransaction<'_> { self.meta.read_receipts.maybe_update_read_receipt( full_receipt, is_own_user_id, - &self.meta.all_events, + &self.meta.all_remote_events, &mut self.items, ); } @@ -425,7 +425,7 @@ impl TimelineStateTransaction<'_> { self.meta.read_receipts.maybe_update_read_receipt( full_receipt, user_id == own_user_id, - &self.meta.all_events, + &self.meta.all_remote_events, &mut self.items, ); } @@ -454,7 +454,7 @@ impl TimelineStateTransaction<'_> { self.meta.read_receipts.maybe_update_read_receipt( full_receipt, is_own_event, - &self.meta.all_events, + &self.meta.all_remote_events, &mut self.items, ); } @@ -465,7 +465,7 @@ impl TimelineStateTransaction<'_> { // Find the previous visible event, if there is one. let Some(prev_event_meta) = self .meta - .all_events + .all_remote_events .iter() .rev() // Find the event item. @@ -495,7 +495,7 @@ impl TimelineStateTransaction<'_> { let read_receipts = self.meta.read_receipts.compute_event_receipts( &remote_prev_event_item.event_id, - &self.meta.all_events, + &self.meta.all_remote_events, false, ); @@ -585,7 +585,7 @@ impl TimelineState { // Find the corresponding visible event. self.meta - .all_events + .all_remote_events .iter() .rev() .skip_while(|ev| ev.event_id != *latest_receipt_id) From 80a48f53ad19a2aae050737a6e8dbb9fe502acb3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 2 Dec 2024 16:02:25 +0100 Subject: [PATCH 659/979] refactor: Rename `add_or_update_event` to `add_or_update_remote_event`. --- .../src/timeline/controller/state.rs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index d8a26daa53c..863848b63d9 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -537,7 +537,12 @@ impl TimelineStateTransaction<'_> { visible: false, }; let _event_added_or_updated = self - .add_or_update_event(event_meta, position, room_data_provider, settings) + .add_or_update_remote_event( + event_meta, + position, + room_data_provider, + settings, + ) .await; return HandleEventResult::default(); @@ -564,7 +569,12 @@ impl TimelineStateTransaction<'_> { visible: false, }; let _event_added_or_updated = self - .add_or_update_event(event_meta, position, room_data_provider, settings) + .add_or_update_remote_event( + event_meta, + position, + room_data_provider, + settings, + ) .await; } @@ -583,8 +593,9 @@ impl TimelineStateTransaction<'_> { visible: should_add, }; - let event_added_or_updated = - self.add_or_update_event(event_meta, position, room_data_provider, settings).await; + let event_added_or_updated = self + .add_or_update_remote_event(event_meta, position, room_data_provider, settings) + .await; // If the event has not been added or updated, it's because it's a duplicated // event. Let's return early. @@ -686,7 +697,7 @@ impl TimelineStateTransaction<'_> { /// It returns `true` if the event has been added or updated, `false` /// otherwise. The latter happens if the event already exists, i.e. if /// an existing event is requested to be added. - async fn add_or_update_event( + async fn add_or_update_remote_event( &mut self, event_meta: FullEventMeta<'_>, position: TimelineItemPosition, From ed1d406b726943694b1080993a29bf27d7b4e446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20F=C3=A9ron?= Date: Mon, 2 Dec 2024 17:00:19 +0100 Subject: [PATCH 660/979] fix(store): fix indexing issue in `ObservableMap` (#4346) Any `.remove` operation called on a `ObservableMap` did not re-index `values` _after_ the removed position, which means any later operation on elements inserted after the previously removed key would either be fetched wrongly, or panic when using `.get_or_create`. This PR fixes these two related bugs and adds extra test cases. --------- Signed-off-by: g@leirbag.net Co-authored-by: Benjamin Bouvier --- .../src/store/observable_map.rs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/observable_map.rs b/crates/matrix-sdk-base/src/store/observable_map.rs index 3392dd6510f..4c6626979c3 100644 --- a/crates/matrix-sdk-base/src/store/observable_map.rs +++ b/crates/matrix-sdk-base/src/store/observable_map.rs @@ -138,6 +138,13 @@ where L: Hash + Eq + ?Sized, { let position = self.mapping.remove(key)?; + + // Reindex every mapped entry that is after the position we're looking to + // remove. + for mapped_pos in self.mapping.values_mut().filter(|pos| **pos > position) { + *mapped_pos = mapped_pos.saturating_sub(1); + } + Some(self.values.remove(position)) } } @@ -195,6 +202,12 @@ mod tests { assert_eq!(map.get(&'a'), Some(&'E')); assert_eq!(map.get(&'b'), Some(&'f')); assert_eq!(map.get(&'c'), Some(&'G')); + + // remove non-last item + assert_eq!(map.remove(&'b'), Some('f')); + + // get_or_create item after the removed one + assert_eq!(map.get_or_create(&'c', || 'G'), &'G'); } #[test] @@ -208,20 +221,26 @@ mod tests { // new items map.insert('a', 'e'); map.insert('b', 'f'); + map.insert('c', 'g'); assert_eq!(map.get(&'a'), Some(&'e')); assert_eq!(map.get(&'b'), Some(&'f')); - assert!(map.get(&'c').is_none()); + assert_eq!(map.get(&'c'), Some(&'g')); + assert!(map.get(&'d').is_none()); - // remove one item - assert_eq!(map.remove(&'b'), Some('f')); + // remove last item + assert_eq!(map.remove(&'c'), Some('g')); assert_eq!(map.get(&'a'), Some(&'e')); - assert_eq!(map.get(&'b'), None); + assert_eq!(map.get(&'b'), Some(&'f')); assert_eq!(map.get(&'c'), None); // remove a non-existent item assert_eq!(map.remove(&'c'), None); + + // remove a non-last item + assert_eq!(map.remove(&'a'), Some('e')); + assert_eq!(map.get(&'b'), Some(&'f')); } #[test] From d49d12249a804757ecd97061a9f764e1af6c0f4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:00:01 +0000 Subject: [PATCH 661/979] build(deps): bump crate-ci/typos from 1.27.3 to 1.28.2 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.3 to 1.28.2. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.27.3...v1.28.2) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dd0b90930b..74a33a3de7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,7 +304,7 @@ jobs: uses: actions/checkout@v4 - name: Check the spelling of the files in our repo - uses: crate-ci/typos@v1.27.3 + uses: crate-ci/typos@v1.28.2 clippy: name: Run clippy From 552ab81739d7b3d15f33a4ad6b8a0fb6785c7bc4 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 27 Nov 2024 15:50:38 +0000 Subject: [PATCH 662/979] task(crypto_tests): Add comments and clarify tests for determining UTD causes --- .../src/types/events/utd_cause.rs | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index 685dc24d03b..0122caa830a 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -339,10 +339,11 @@ mod tests { } #[test] - fn test_historical_expected_reason_depending_on_origin_ts_for_missing_session() { + fn test_date_of_device_determines_whether_a_missing_key_utd_is_expected_historical() { let message_creation_ts = 10000; let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + // Given the device is older than the event let older_than_event_device = CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch( (message_creation_ts - 1000).try_into().unwrap(), @@ -350,6 +351,8 @@ mod tests { is_backup_configured: true, }; + // If the key was missing + // Then we say the cause is unknown - this is not an historical event assert_eq!( UtdCause::determine( &raw_event(json!({})), @@ -362,6 +365,7 @@ mod tests { UtdCause::Unknown ); + // But if the device is newer than the event let newer_than_event_device = CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch( (message_creation_ts + 1000).try_into().unwrap(), @@ -369,6 +373,8 @@ mod tests { is_backup_configured: true, }; + // If the key was missing + // Then we say this is expected, because the event is historical assert_eq!( UtdCause::determine( &utd_event, @@ -383,10 +389,11 @@ mod tests { } #[test] - fn test_historical_expected_reason_depending_on_origin_ts_for_ratcheted_session() { + fn test_date_of_device_determines_whether_a_message_index_utd_is_expected_historical() { let message_creation_ts = 10000; let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + // Given the device is older than the event let older_than_event_device = CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch( (message_creation_ts - 1000).try_into().unwrap(), @@ -394,6 +401,8 @@ mod tests { is_backup_configured: true, }; + // If the message index was incorrect + // Then we say the cause is unknown - this is not an historical event assert_eq!( UtdCause::determine( &raw_event(json!({})), @@ -406,6 +415,7 @@ mod tests { UtdCause::Unknown ); + // But if the device is newer than the event let newer_than_event_device = CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch( (message_creation_ts + 1000).try_into().unwrap(), @@ -413,6 +423,8 @@ mod tests { is_backup_configured: true, }; + // If the message index was incorrect + // Then we say this is expected, because the event is historical assert_eq!( UtdCause::determine( &utd_event, @@ -427,10 +439,11 @@ mod tests { } #[test] - fn test_historical_expected_reason_depending_on_origin_only_for_correct_reason() { + fn test_when_event_is_old_and_message_index_is_wrong_this_is_expected_historical() { let message_creation_ts = 10000; let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + // Given the device is newer than the event let newer_than_event_device = CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch( (message_creation_ts + 1000).try_into().unwrap(), @@ -438,6 +451,8 @@ mod tests { is_backup_configured: true, }; + // If the message index was incorrect + // Then we say this is an expected UTD (because it's historical) assert_eq!( UtdCause::determine( &utd_event, @@ -450,6 +465,8 @@ mod tests { UtdCause::HistoricalMessage ); + // But if we have some other failure + // Then we say the UTD is unexpected, and we don't know what type it is assert_eq!( UtdCause::determine( &utd_event, @@ -461,7 +478,6 @@ mod tests { ), UtdCause::Unknown ); - assert_eq!( UtdCause::determine( &utd_event, @@ -476,10 +492,13 @@ mod tests { } #[test] - fn test_historical_expected_only_if_backup_configured() { + fn test_when_event_is_old_and_message_index_is_wrong_but_backup_is_disabled_this_is_unexpected() + { let message_creation_ts = 10000; let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + // Given the device is newer than the event + // But backup is disabled let crypto_context_info = CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch( (message_creation_ts + 1000).try_into().unwrap(), @@ -487,6 +506,8 @@ mod tests { is_backup_configured: false, }; + // If the message key was missing + // Then we say this was unexpected (because backup was disabled) assert_eq!( UtdCause::determine( &utd_event, @@ -499,6 +520,8 @@ mod tests { UtdCause::Unknown ); + // And if the message index was incorrect + // Then we still say this was unexpected (because backup was disabled) assert_eq!( UtdCause::determine( &utd_event, From 0b4b4ea791879cb8f07145637d03257f63ed799d Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 28 Nov 2024 10:58:29 +0000 Subject: [PATCH 663/979] task(crypto_tests): Shorten utd_cause tests with utility functions for UnableToDecryptInfos --- .../src/types/events/utd_cause.rs | 181 ++++++++---------- 1 file changed, 75 insertions(+), 106 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index 0122caa830a..e5a310a026e 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -160,10 +160,7 @@ mod tests { UtdCause::determine( &raw_event(json!({})), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession, - } + &missing_megolm_session() ), UtdCause::Unknown ); @@ -176,10 +173,7 @@ mod tests { UtdCause::determine( &raw_event(json!({})), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } + &missing_megolm_session() ), UtdCause::Unknown ); @@ -193,10 +187,7 @@ mod tests { UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": 3 } })), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } + &missing_megolm_session() ), UtdCause::Unknown ); @@ -210,10 +201,7 @@ mod tests { UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "invite" } }),), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } + &missing_megolm_session() ), UtdCause::Unknown ); @@ -227,10 +215,7 @@ mod tests { UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "join" } })), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } + &missing_megolm_session() ), UtdCause::Unknown ); @@ -244,10 +229,7 @@ mod tests { UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "leave" } })), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } + &missing_megolm_session() ), UtdCause::SentBeforeWeJoined ); @@ -262,10 +244,7 @@ mod tests { UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "leave" } })), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MalformedEncryptedEvent - } + &malformed_encrypted_event() ), UtdCause::Unknown ); @@ -278,10 +257,7 @@ mod tests { UtdCause::determine( &raw_event(json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } + &missing_megolm_session() ), UtdCause::SentBeforeWeJoined ); @@ -293,12 +269,7 @@ mod tests { UtdCause::determine( &raw_event(json!({})), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::SenderIdentityNotTrusted( - VerificationLevel::VerificationViolation, - ) - } + &verification_violation() ), UtdCause::VerificationViolation ); @@ -310,12 +281,7 @@ mod tests { UtdCause::determine( &raw_event(json!({})), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::SenderIdentityNotTrusted( - VerificationLevel::UnsignedDevice, - ) - } + &unsigned_device() ), UtdCause::UnsignedDevice ); @@ -327,12 +293,7 @@ mod tests { UtdCause::determine( &raw_event(json!({})), some_crypto_context_info(), - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::SenderIdentityNotTrusted( - VerificationLevel::None(DeviceLinkProblem::MissingDevice) - ) - } + &missing_device() ), UtdCause::UnknownDevice ); @@ -357,10 +318,7 @@ mod tests { UtdCause::determine( &raw_event(json!({})), older_than_event_device, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } + &missing_megolm_session() ), UtdCause::Unknown ); @@ -376,14 +334,7 @@ mod tests { // If the key was missing // Then we say this is expected, because the event is historical assert_eq!( - UtdCause::determine( - &utd_event, - newer_than_event_device, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } - ), + UtdCause::determine(&utd_event, newer_than_event_device, &missing_megolm_session()), UtdCause::HistoricalMessage ); } @@ -407,10 +358,7 @@ mod tests { UtdCause::determine( &raw_event(json!({})), older_than_event_device, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::UnknownMegolmMessageIndex - } + &unknown_megolm_message_index() ), UtdCause::Unknown ); @@ -429,10 +377,7 @@ mod tests { UtdCause::determine( &utd_event, newer_than_event_device, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::UnknownMegolmMessageIndex - } + &unknown_megolm_message_index() ), UtdCause::HistoricalMessage ); @@ -457,10 +402,7 @@ mod tests { UtdCause::determine( &utd_event, newer_than_event_device, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::UnknownMegolmMessageIndex - } + &unknown_megolm_message_index() ), UtdCause::HistoricalMessage ); @@ -468,25 +410,11 @@ mod tests { // But if we have some other failure // Then we say the UTD is unexpected, and we don't know what type it is assert_eq!( - UtdCause::determine( - &utd_event, - newer_than_event_device, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MalformedEncryptedEvent - } - ), + UtdCause::determine(&utd_event, newer_than_event_device, &malformed_encrypted_event()), UtdCause::Unknown ); assert_eq!( - UtdCause::determine( - &utd_event, - newer_than_event_device, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MegolmDecryptionFailure - } - ), + UtdCause::determine(&utd_event, newer_than_event_device, &megolm_decryption_failure()), UtdCause::Unknown ); } @@ -509,28 +437,14 @@ mod tests { // If the message key was missing // Then we say this was unexpected (because backup was disabled) assert_eq!( - UtdCause::determine( - &utd_event, - crypto_context_info, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession - } - ), + UtdCause::determine(&utd_event, crypto_context_info, &missing_megolm_session()), UtdCause::Unknown ); // And if the message index was incorrect // Then we still say this was unexpected (because backup was disabled) assert_eq!( - UtdCause::determine( - &utd_event, - crypto_context_info, - &UnableToDecryptInfo { - session_id: None, - reason: UnableToDecryptReason::UnknownMegolmMessageIndex - } - ), + UtdCause::determine(&utd_event, crypto_context_info, &unknown_megolm_message_index()), UtdCause::Unknown ); } @@ -563,4 +477,59 @@ mod tests { is_backup_configured: false, } } + + fn missing_megolm_session() -> UnableToDecryptInfo { + UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession, + } + } + + fn malformed_encrypted_event() -> UnableToDecryptInfo { + UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MalformedEncryptedEvent, + } + } + + fn unknown_megolm_message_index() -> UnableToDecryptInfo { + UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex, + } + } + + fn megolm_decryption_failure() -> UnableToDecryptInfo { + UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MegolmDecryptionFailure, + } + } + + fn verification_violation() -> UnableToDecryptInfo { + UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::SenderIdentityNotTrusted( + VerificationLevel::VerificationViolation, + ), + } + } + + fn unsigned_device() -> UnableToDecryptInfo { + UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::SenderIdentityNotTrusted( + VerificationLevel::UnsignedDevice, + ), + } + } + + fn missing_device() -> UnableToDecryptInfo { + UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::SenderIdentityNotTrusted(VerificationLevel::None( + DeviceLinkProblem::MissingDevice, + )), + } + } } From b65728d46fd6bd81e1e008d6473647948cbed723 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 28 Nov 2024 11:24:54 +0000 Subject: [PATCH 664/979] task(crypto_tests): Shorten utd_cause tests with helper functions for devices --- .../src/types/events/utd_cause.rs | 140 +++++++----------- 1 file changed, 53 insertions(+), 87 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index e5a310a026e..8204af230e0 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -147,7 +147,7 @@ mod tests { use matrix_sdk_common::deserialized_responses::{ DeviceLinkProblem, UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, }; - use ruma::{events::AnySyncTimelineEvent, serde::Raw, uint, MilliSecondsSinceUnixEpoch}; + use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch}; use serde_json::{json, value::to_raw_value}; use crate::types::events::{utd_cause::CryptoContextInfo, UtdCause}; @@ -159,7 +159,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({})), - some_crypto_context_info(), + device_new_with_backup(), &missing_megolm_session() ), UtdCause::Unknown @@ -172,7 +172,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({})), - some_crypto_context_info(), + device_new_with_backup(), &missing_megolm_session() ), UtdCause::Unknown @@ -186,7 +186,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": 3 } })), - some_crypto_context_info(), + device_new_with_backup(), &missing_megolm_session() ), UtdCause::Unknown @@ -200,7 +200,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "invite" } }),), - some_crypto_context_info(), + device_new_with_backup(), &missing_megolm_session() ), UtdCause::Unknown @@ -214,7 +214,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "join" } })), - some_crypto_context_info(), + device_new_with_backup(), &missing_megolm_session() ), UtdCause::Unknown @@ -228,7 +228,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "leave" } })), - some_crypto_context_info(), + device_new_with_backup(), &missing_megolm_session() ), UtdCause::SentBeforeWeJoined @@ -243,7 +243,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "leave" } })), - some_crypto_context_info(), + device_new_with_backup(), &malformed_encrypted_event() ), UtdCause::Unknown @@ -256,7 +256,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })), - some_crypto_context_info(), + device_new_with_backup(), &missing_megolm_session() ), UtdCause::SentBeforeWeJoined @@ -268,7 +268,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({})), - some_crypto_context_info(), + device_new_with_backup(), &verification_violation() ), UtdCause::VerificationViolation @@ -280,7 +280,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({})), - some_crypto_context_info(), + device_new_with_backup(), &unsigned_device() ), UtdCause::UnsignedDevice @@ -290,93 +290,54 @@ mod tests { #[test] fn test_unknown_device_is_passed_through() { assert_eq!( - UtdCause::determine( - &raw_event(json!({})), - some_crypto_context_info(), - &missing_device() - ), + UtdCause::determine(&raw_event(json!({})), device_new_with_backup(), &missing_device()), UtdCause::UnknownDevice ); } #[test] fn test_date_of_device_determines_whether_a_missing_key_utd_is_expected_historical() { - let message_creation_ts = 10000; - let utd_event = a_utd_event_with_origin_ts(message_creation_ts); - - // Given the device is older than the event - let older_than_event_device = CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch( - (message_creation_ts - 1000).try_into().unwrap(), - ), - is_backup_configured: true, - }; - // If the key was missing // Then we say the cause is unknown - this is not an historical event assert_eq!( UtdCause::determine( &raw_event(json!({})), - older_than_event_device, + device_old_with_backup(), &missing_megolm_session() ), UtdCause::Unknown ); // But if the device is newer than the event - let newer_than_event_device = CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch( - (message_creation_ts + 1000).try_into().unwrap(), - ), - is_backup_configured: true, - }; - // If the key was missing // Then we say this is expected, because the event is historical assert_eq!( - UtdCause::determine(&utd_event, newer_than_event_device, &missing_megolm_session()), + UtdCause::determine(&utd_event(), device_new_with_backup(), &missing_megolm_session()), UtdCause::HistoricalMessage ); } #[test] fn test_date_of_device_determines_whether_a_message_index_utd_is_expected_historical() { - let message_creation_ts = 10000; - let utd_event = a_utd_event_with_origin_ts(message_creation_ts); - - // Given the device is older than the event - let older_than_event_device = CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch( - (message_creation_ts - 1000).try_into().unwrap(), - ), - is_backup_configured: true, - }; - // If the message index was incorrect // Then we say the cause is unknown - this is not an historical event assert_eq!( UtdCause::determine( &raw_event(json!({})), - older_than_event_device, + device_old_with_backup(), &unknown_megolm_message_index() ), UtdCause::Unknown ); // But if the device is newer than the event - let newer_than_event_device = CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch( - (message_creation_ts + 1000).try_into().unwrap(), - ), - is_backup_configured: true, - }; // If the message index was incorrect // Then we say this is expected, because the event is historical assert_eq!( UtdCause::determine( - &utd_event, - newer_than_event_device, + &utd_event(), + device_new_with_backup(), &unknown_megolm_message_index() ), UtdCause::HistoricalMessage @@ -385,23 +346,12 @@ mod tests { #[test] fn test_when_event_is_old_and_message_index_is_wrong_this_is_expected_historical() { - let message_creation_ts = 10000; - let utd_event = a_utd_event_with_origin_ts(message_creation_ts); - - // Given the device is newer than the event - let newer_than_event_device = CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch( - (message_creation_ts + 1000).try_into().unwrap(), - ), - is_backup_configured: true, - }; - // If the message index was incorrect // Then we say this is an expected UTD (because it's historical) assert_eq!( UtdCause::determine( - &utd_event, - newer_than_event_device, + &utd_event(), + device_new_with_backup(), &unknown_megolm_message_index() ), UtdCause::HistoricalMessage @@ -410,11 +360,19 @@ mod tests { // But if we have some other failure // Then we say the UTD is unexpected, and we don't know what type it is assert_eq!( - UtdCause::determine(&utd_event, newer_than_event_device, &malformed_encrypted_event()), + UtdCause::determine( + &utd_event(), + device_new_with_backup(), + &malformed_encrypted_event() + ), UtdCause::Unknown ); assert_eq!( - UtdCause::determine(&utd_event, newer_than_event_device, &megolm_decryption_failure()), + UtdCause::determine( + &utd_event(), + device_new_with_backup(), + &megolm_decryption_failure() + ), UtdCause::Unknown ); } @@ -422,34 +380,28 @@ mod tests { #[test] fn test_when_event_is_old_and_message_index_is_wrong_but_backup_is_disabled_this_is_unexpected() { - let message_creation_ts = 10000; - let utd_event = a_utd_event_with_origin_ts(message_creation_ts); - // Given the device is newer than the event // But backup is disabled - let crypto_context_info = CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch( - (message_creation_ts + 1000).try_into().unwrap(), - ), - is_backup_configured: false, - }; - // If the message key was missing // Then we say this was unexpected (because backup was disabled) assert_eq!( - UtdCause::determine(&utd_event, crypto_context_info, &missing_megolm_session()), + UtdCause::determine(&utd_event(), device_new_no_backup(), &missing_megolm_session()), UtdCause::Unknown ); // And if the message index was incorrect // Then we still say this was unexpected (because backup was disabled) assert_eq!( - UtdCause::determine(&utd_event, crypto_context_info, &unknown_megolm_message_index()), + UtdCause::determine( + &utd_event(), + device_new_no_backup(), + &unknown_megolm_message_index() + ), UtdCause::Unknown ); } - fn a_utd_event_with_origin_ts(origin_server_ts: i32) -> Raw { + fn utd_event() -> Raw { raw_event(json!({ "type": "m.room.encrypted", "event_id": "$0", @@ -462,7 +414,7 @@ mod tests { "session_id": "A0", }, "sender": "@bob:localhost", - "origin_server_ts": origin_server_ts, + "origin_server_ts": 5555, "unsigned": { "membership": "join" } })) } @@ -471,13 +423,27 @@ mod tests { Raw::from_json(to_raw_value(&value).unwrap()) } - fn some_crypto_context_info() -> CryptoContextInfo { + fn device_old_with_backup() -> CryptoContextInfo { CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch(uint!(42)), + device_creation_ts: MilliSecondsSinceUnixEpoch((1111).try_into().unwrap()), + is_backup_configured: true, + } + } + + fn device_new_no_backup() -> CryptoContextInfo { + CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch((9999).try_into().unwrap()), is_backup_configured: false, } } + fn device_new_with_backup() -> CryptoContextInfo { + CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch((9999).try_into().unwrap()), + is_backup_configured: true, + } + } + fn missing_megolm_session() -> UnableToDecryptInfo { UnableToDecryptInfo { session_id: None, From 8de76deb1ba6c1adc3fafe982e44b224254cedec Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 28 Nov 2024 11:34:13 +0000 Subject: [PATCH 665/979] task(crypto_tests): Re-arrange and reword utd_cause tests --- .../src/types/events/utd_cause.rs | 91 +++++++++---------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index 8204af230e0..423abdba1b0 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -152,6 +152,10 @@ mod tests { use crate::types::events::{utd_cause::CryptoContextInfo, UtdCause}; + const EVENT_TIME: usize = 5555; + const BEFORE_EVENT_TIME: usize = 1111; + const AFTER_EVENT_TIME: usize = 9999; + #[test] fn test_a_missing_raw_event_means_we_guess_unknown() { // When we don't provide any JSON to check for membership, then we guess the UTD @@ -296,58 +300,34 @@ mod tests { } #[test] - fn test_date_of_device_determines_whether_a_missing_key_utd_is_expected_historical() { - // If the key was missing - // Then we say the cause is unknown - this is not an historical event + fn test_old_devices_dont_cause_historical_utds() { + // If the device is old, we say this UTD is unexpected (missing megolm session) assert_eq!( - UtdCause::determine( - &raw_event(json!({})), - device_old_with_backup(), - &missing_megolm_session() - ), + UtdCause::determine(&utd_event(), device_old_with_backup(), &missing_megolm_session()), UtdCause::Unknown ); - // But if the device is newer than the event - // If the key was missing - // Then we say this is expected, because the event is historical - assert_eq!( - UtdCause::determine(&utd_event(), device_new_with_backup(), &missing_megolm_session()), - UtdCause::HistoricalMessage - ); - } - - #[test] - fn test_date_of_device_determines_whether_a_message_index_utd_is_expected_historical() { - // If the message index was incorrect - // Then we say the cause is unknown - this is not an historical event + // Same for unknown megolm message index assert_eq!( UtdCause::determine( - &raw_event(json!({})), + &utd_event(), device_old_with_backup(), &unknown_megolm_message_index() ), UtdCause::Unknown ); + } - // But if the device is newer than the event - - // If the message index was incorrect - // Then we say this is expected, because the event is historical + #[test] + fn test_new_devices_cause_historical_utds() { + // If the device is old, we say this UTD is expected historical (missing megolm + // session) assert_eq!( - UtdCause::determine( - &utd_event(), - device_new_with_backup(), - &unknown_megolm_message_index() - ), + UtdCause::determine(&utd_event(), device_new_with_backup(), &missing_megolm_session()), UtdCause::HistoricalMessage ); - } - #[test] - fn test_when_event_is_old_and_message_index_is_wrong_this_is_expected_historical() { - // If the message index was incorrect - // Then we say this is an expected UTD (because it's historical) + // Same for unknown megolm message index assert_eq!( UtdCause::determine( &utd_event(), @@ -356,9 +336,12 @@ mod tests { ), UtdCause::HistoricalMessage ); + } - // But if we have some other failure - // Then we say the UTD is unexpected, and we don't know what type it is + #[test] + fn test_malformed_events_are_never_expected_utds() { + // Even if the device is new, if the reason for the UTD is a malformed event, + // it's an unexpected UTD. assert_eq!( UtdCause::determine( &utd_event(), @@ -367,6 +350,8 @@ mod tests { ), UtdCause::Unknown ); + + // Same for decryption failures assert_eq!( UtdCause::determine( &utd_event(), @@ -378,19 +363,18 @@ mod tests { } #[test] - fn test_when_event_is_old_and_message_index_is_wrong_but_backup_is_disabled_this_is_unexpected() - { - // Given the device is newer than the event - // But backup is disabled - // If the message key was missing - // Then we say this was unexpected (because backup was disabled) + fn test_if_backup_is_disabled_this_utd_is_unexpected() { + // If the backup is disables, even if the device is new and the reason for the + // UTD is missing keys, we still treat this UTD as unexpected. + // + // TODO: I (AJB) think this is wrong, but it will be addressed as part of + // https://github.com/element-hq/element-meta/issues/2638 assert_eq!( UtdCause::determine(&utd_event(), device_new_no_backup(), &missing_megolm_session()), UtdCause::Unknown ); - // And if the message index was incorrect - // Then we still say this was unexpected (because backup was disabled) + // Same for unknown megolm message index assert_eq!( UtdCause::determine( &utd_event(), @@ -414,7 +398,7 @@ mod tests { "session_id": "A0", }, "sender": "@bob:localhost", - "origin_server_ts": 5555, + "origin_server_ts": EVENT_TIME, "unsigned": { "membership": "join" } })) } @@ -423,23 +407,30 @@ mod tests { Raw::from_json(to_raw_value(&value).unwrap()) } + fn device_old_no_backup() -> CryptoContextInfo { + CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch((BEFORE_EVENT_TIME).try_into().unwrap()), + is_backup_configured: false, + } + } + fn device_old_with_backup() -> CryptoContextInfo { CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch((1111).try_into().unwrap()), + device_creation_ts: MilliSecondsSinceUnixEpoch((BEFORE_EVENT_TIME).try_into().unwrap()), is_backup_configured: true, } } fn device_new_no_backup() -> CryptoContextInfo { CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch((9999).try_into().unwrap()), + device_creation_ts: MilliSecondsSinceUnixEpoch((AFTER_EVENT_TIME).try_into().unwrap()), is_backup_configured: false, } } fn device_new_with_backup() -> CryptoContextInfo { CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch((9999).try_into().unwrap()), + device_creation_ts: MilliSecondsSinceUnixEpoch((AFTER_EVENT_TIME).try_into().unwrap()), is_backup_configured: true, } } From 8c73f0c655f85003d85c53478ce1ef8f569cf0f0 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 29 Nov 2024 10:28:16 +0000 Subject: [PATCH 666/979] task(crypto_tests): Remove duplicate test --- .../src/types/events/utd_cause.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index 423abdba1b0..bc21ab5385d 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -156,20 +156,6 @@ mod tests { const BEFORE_EVENT_TIME: usize = 1111; const AFTER_EVENT_TIME: usize = 9999; - #[test] - fn test_a_missing_raw_event_means_we_guess_unknown() { - // When we don't provide any JSON to check for membership, then we guess the UTD - // is unknown. - assert_eq!( - UtdCause::determine( - &raw_event(json!({})), - device_new_with_backup(), - &missing_megolm_session() - ), - UtdCause::Unknown - ); - } - #[test] fn test_if_there_is_no_membership_info_we_guess_unknown() { // If our JSON contains no membership info, then we guess the UTD is unknown. From 50f036d283d90f5352406b14f340acf3367ca851 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 29 Nov 2024 10:30:07 +0000 Subject: [PATCH 667/979] task(crypto_tests): Fix test that will fail when we handle backups correctly This test was checking that a new device which has access to backups returned an unknown UTD when it was given empty JSON for the event. It was only passing because we currently have incorrect behaviour when backups are enabled. The fix is to make the device old and without access to backups, so that we still consider this UTD unexpected. --- crates/matrix-sdk-crypto/src/types/events/utd_cause.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index bc21ab5385d..c7d2a0810fc 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -162,7 +162,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({})), - device_new_with_backup(), + device_old_no_backup(), &missing_megolm_session() ), UtdCause::Unknown From 31bd5c679038044421ebfafbb4cf3843466d3c57 Mon Sep 17 00:00:00 2001 From: Integral Date: Tue, 3 Dec 2024 13:07:49 +0800 Subject: [PATCH 668/979] refactor: replace static with const for global constants Signed-off-by: Integral --- crates/matrix-sdk-crypto/src/lib.rs | 2 +- crates/matrix-sdk/src/oidc/cross_process.rs | 2 +- .../src/widget/settings/url_params.rs | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index e8836195e82..04079868a98 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -110,7 +110,7 @@ pub use verification::{QrVerification, QrVerificationState, ScanError}; pub use vodozemac; /// The version of the matrix-sdk-cypto crate being used -pub static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(test)] matrix_sdk_test::init_tracing_for_tests!(); diff --git a/crates/matrix-sdk/src/oidc/cross_process.rs b/crates/matrix-sdk/src/oidc/cross_process.rs index d51ce907604..87c057d1740 100644 --- a/crates/matrix-sdk/src/oidc/cross_process.rs +++ b/crates/matrix-sdk/src/oidc/cross_process.rs @@ -24,7 +24,7 @@ struct SessionHash(Vec); impl SessionHash { fn to_hex(&self) -> String { - static CHARS: &[char; 16] = + const CHARS: &[char; 16] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; let mut res = String::with_capacity(2 * self.0.len() + 2); if !self.0.is_empty() { diff --git a/crates/matrix-sdk/src/widget/settings/url_params.rs b/crates/matrix-sdk/src/widget/settings/url_params.rs index fe2dd2bd9c8..ba787582bfd 100644 --- a/crates/matrix-sdk/src/widget/settings/url_params.rs +++ b/crates/matrix-sdk/src/widget/settings/url_params.rs @@ -15,16 +15,16 @@ use url::Url; use urlencoding::encode; -pub static USER_ID: &str = "$matrix_user_id"; -pub static ROOM_ID: &str = "$matrix_room_id"; -pub static WIDGET_ID: &str = "$matrix_widget_id"; -pub static AVATAR_URL: &str = "$matrix_avatar_url"; -pub static DISPLAY_NAME: &str = "$matrix_display_name"; -pub static LANGUAGE: &str = "$org.matrix.msc2873.client_language"; -pub static CLIENT_THEME: &str = "$org.matrix.msc2873.client_theme"; -pub static CLIENT_ID: &str = "$org.matrix.msc2873.client_id"; -pub static DEVICE_ID: &str = "$org.matrix.msc2873.matrix_device_id"; -pub static HOMESERVER_URL: &str = "$org.matrix.msc4039.matrix_base_url"; +pub const USER_ID: &str = "$matrix_user_id"; +pub const ROOM_ID: &str = "$matrix_room_id"; +pub const WIDGET_ID: &str = "$matrix_widget_id"; +pub const AVATAR_URL: &str = "$matrix_avatar_url"; +pub const DISPLAY_NAME: &str = "$matrix_display_name"; +pub const LANGUAGE: &str = "$org.matrix.msc2873.client_language"; +pub const CLIENT_THEME: &str = "$org.matrix.msc2873.client_theme"; +pub const CLIENT_ID: &str = "$org.matrix.msc2873.client_id"; +pub const DEVICE_ID: &str = "$org.matrix.msc2873.matrix_device_id"; +pub const HOMESERVER_URL: &str = "$org.matrix.msc4039.matrix_base_url"; pub struct QueryProperties { pub(crate) widget_id: String, From e76b8f7e1591205e29165dd93dde9b77798bc971 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:36:24 +0100 Subject: [PATCH 669/979] tests: Refactor the widget tests to use `MockMatrixServer` (#4236) This is a follow up PR on https://github.com/matrix-org/matrix-rust-sdk/pull/3987. And tries to use the `MockMatrixServer` wherever reasonable in the widget integration tests. --------- Co-authored-by: Benjamin Bouvier --- crates/matrix-sdk/src/test_utils/mocks.rs | 392 +++++++++++++++++- .../tests/integration/event_cache.rs | 1 + crates/matrix-sdk/tests/integration/widget.rs | 106 ++--- 3 files changed, 427 insertions(+), 72 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 121d57b9956..6774102f227 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -27,11 +27,16 @@ use matrix_sdk_test::{ test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, }; -use ruma::{directory::PublicRoomsChunk, MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName}; +use ruma::{ + directory::PublicRoomsChunk, + events::{AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType}, + serde::Raw, + MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName, +}; use serde::Deserialize; -use serde_json::json; +use serde_json::{json, Value}; use wiremock::{ - matchers::{body_partial_json, header, method, path, path_regex}, + matchers::{body_partial_json, header, method, path, path_regex, query_param}, Mock, MockBuilder, MockGuard, MockServer, Request, Respond, ResponseTemplate, Times, }; @@ -312,11 +317,61 @@ impl MatrixMockServer { /// ``` pub fn mock_room_send(&self) -> MockEndpoint<'_, RoomSendEndpoint> { let mock = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")); + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/.*".to_owned())); MockEndpoint { mock, server: &self.server, endpoint: RoomSendEndpoint } } + /// Creates a prebuilt mock for sending a state event in a room. + /// + /// Similar to: [`MatrixMockServer::mock_room_send`] + /// + /// Note: works with *any* room. + /// Note: works with *any* event type. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send_state() + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room.send_raw("m.room.create", json!({ "body": "Hello world" })).await; + /// // The `/send` endpoint should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// + /// let response = room.send_state_event_raw("m.room.message", "my_key", json!({ "body": "Hello world" })).await?; + /// // The `/state` endpoint should be mocked by the server. + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_room_send_state(&self) -> MockEndpoint<'_, RoomSendStateEndpoint> { + let mock = Mock::given(method("PUT")) + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/.*/.*")); + MockEndpoint { mock, server: &self.server, endpoint: RoomSendStateEndpoint::default() } + } + /// Creates a prebuilt mock for asking whether *a* room is encrypted or not. /// /// Note: Applies to all rooms. @@ -432,6 +487,15 @@ impl MatrixMockServer { } } + /// Create a prebuild mock for paginating room message with the `/messages` + /// endpoint. + pub fn mock_room_messages(&self) -> MockEndpoint<'_, RoomMessagesEndpoint> { + let mock = Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")); + MockEndpoint { mock, server: &self.server, endpoint: RoomMessagesEndpoint } + } + /// Create a prebuilt mock for uploading media. pub fn mock_upload(&self) -> MockEndpoint<'_, UploadEndpoint> { let mock = Mock::given(method("POST")) @@ -755,7 +819,7 @@ impl<'a, T> MockEndpoint<'a, T> { } } -/// A prebuilt mock for sending an event in a room. +/// A prebuilt mock for sending a message like event in a room. pub struct RoomSendEndpoint; impl<'a> MockEndpoint<'a, RoomSendEndpoint> { @@ -781,14 +845,14 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// .await; /// /// let event_id = event_id!("$some_id"); - /// let send_guard = mock_server + /// mock_server /// .mock_room_send() /// .body_matches_partial_json(json!({ /// "body": "Hello world", /// })) /// .ok(event_id) /// .expect(1) - /// .mount_as_scoped() + /// .mount() /// .await; /// /// let content = RoomMessageEventContent::text_plain("Hello world"); @@ -801,10 +865,63 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// ); /// # anyhow::Ok(()) }); /// ``` - pub fn body_matches_partial_json(self, body: serde_json::Value) -> Self { + pub fn body_matches_partial_json(self, body: Value) -> Self { Self { mock: self.mock.and(body_partial_json(body)), ..self } } + /// Ensures that the send endpoint request uses a specific event type. + /// + /// # Examples + /// + /// see also [`MatrixMockServer::mock_room_send`] for more context. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send() + /// .for_type("m.room.message".into()) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room.send_raw("m.room.reaction", json!({ "body": "Hello world" })).await; + /// // The `m.room.reaction` event type should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let response = room.send_raw("m.room.message", json!({ "body": "Hello world" })).await?; + /// // The `m.room.message` event type should be mocked by the server. + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn for_type(self, event_type: MessageLikeEventType) -> Self { + Self { + // Note: we already defined a path when constructing the mock builder, but this one + // ought to be more specialized. + mock: self + .mock + .and(path_regex(format!(r"^/_matrix/client/v3/rooms/.*/send/{event_type}",))), + ..self + } + } + /// Returns a send endpoint that emulates success, i.e. the event has been /// sent with the given event id. /// @@ -845,6 +962,231 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { } } +/// A prebuilt mock for sending a state event in a room. +#[derive(Default)] +pub struct RoomSendStateEndpoint { + state_key: Option, + event_type: Option, +} + +impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { + fn generate_path_regexp(endpoint: &RoomSendStateEndpoint) -> String { + format!( + r"^/_matrix/client/v3/rooms/.*/state/{}/{}", + endpoint.event_type.as_ref().map_or_else(|| ".*".to_owned(), |t| t.to_string()), + endpoint.state_key.as_ref().map_or_else(|| ".*".to_owned(), |k| k.to_string()) + ) + } + + /// Ensures that the body of the request is a superset of the provided + /// `body` parameter. + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{room_id, event_id, events::room::power_levels::RoomPowerLevelsEventContent}, + /// test_utils::mocks::MatrixMockServer + /// }; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send_state() + /// .body_matches_partial_json(json!({ + /// "redact": 51, + /// })) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let mut content = RoomPowerLevelsEventContent::new(); + /// // Update the power level to a non default value. + /// // Otherwise it will be skipped from serialization. + /// content.redact = 51.into(); + /// + /// let response = room.send_state_event(content).await?; + /// + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn body_matches_partial_json(self, body: Value) -> Self { + Self { mock: self.mock.and(body_partial_json(body)), ..self } + } + + /// Ensures that the send endpoint request uses a specific event type. + /// + /// Note: works with *any* room. + /// + /// # Examples + /// + /// see also [`MatrixMockServer::mock_room_send`] for more context. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{ + /// event_id, + /// events::room::{ + /// create::RoomCreateEventContent, power_levels::RoomPowerLevelsEventContent, + /// }, + /// events::StateEventType, + /// room_id, + /// }, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await; + /// + /// let event_id = event_id!("$some_id"); + /// + /// mock_server + /// .mock_room_send_state() + /// .for_type(StateEventType::RoomPowerLevels) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room.send_state_event(RoomCreateEventContent::new_v11()).await; + /// // The `m.room.reaction` event type should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let response = room.send_state_event(RoomPowerLevelsEventContent::new()).await?; + /// // The `m.room.message` event type should be mocked by the server. + /// assert_eq!( + /// event_id, response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// + /// # anyhow::Ok(()) }); + /// ``` + pub fn for_type(mut self, event_type: StateEventType) -> Self { + self.endpoint.event_type = Some(event_type); + // Note: we may have already defined a path, but this one ought to be more + // specialized (unless for_key/for_type were called multiple times). + Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self } + } + + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{ + /// event_id, + /// events::{call::member::CallMemberEventContent, AnyStateEventContent}, + /// room_id, + /// }, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await; + /// + /// let event_id = event_id!("$some_id"); + /// + /// mock_server + /// .mock_room_send_state() + /// .for_key("my_key".to_owned()) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room + /// .send_state_event_for_key( + /// "", + /// AnyStateEventContent::CallMember(CallMemberEventContent::new_empty(None)), + /// ) + /// .await; + /// // The `m.room.reaction` event type should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let response = room + /// .send_state_event_for_key( + /// "my_key", + /// AnyStateEventContent::CallMember(CallMemberEventContent::new_empty(None)), + /// ) + /// .await + /// .unwrap(); + /// + /// // The `m.room.message` event type should be mocked by the server. + /// assert_eq!( + /// event_id, response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn for_key(mut self, state_key: String) -> Self { + self.endpoint.state_key = Some(state_key); + // Note: we may have already defined a path, but this one ought to be more + // specialized (unless for_key/for_type were called multiple times). + Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self } + } + + /// Returns a send endpoint that emulates success, i.e. the event has been + /// sent with the given event id. + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// let send_guard = mock_server + /// .mock_room_send_state() + /// .ok(event_id) + /// .expect(1) + /// .mount_as_scoped() + /// .await; + /// + /// let response = room.send_state_event_raw("m.room.message", "my_key", json!({ "body": "Hello world" })).await?; + /// + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + self.ok_with_event_id(returned_event_id.into()) + } +} + /// A prebuilt mock for running sync v2. pub struct SyncEndpoint { sync_response_builder: Arc>, @@ -1024,6 +1366,38 @@ impl<'a> MockEndpoint<'a, RoomEventEndpoint> { } } +/// A prebuilt mock for the `/messages` endpoint. +pub struct RoomMessagesEndpoint; + +/// A prebuilt mock for getting a room messages in a room. +impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { + /// Expects an optional limit to be set on the request. + pub fn limit(self, limit: u32) -> Self { + Self { mock: self.mock.and(query_param("limit", limit.to_string())), ..self } + } + + /// Returns a messages endpoint that emulates success, i.e. the messages + /// provided as `response` could be retrieved. + /// + /// Note: pass `chunk` in the correct order: topological for forward + /// pagination, reverse topological for backwards pagination. + pub fn ok( + self, + start: String, + end: Option, + chunk: Vec>>, + state: Vec>>, + ) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "start": start, + "end": end, + "chunk": chunk.into_iter().map(|ev| ev.into()).collect::>(), + "state": state.into_iter().map(|ev| ev.into()).collect::>(), + }))); + MatrixMock { server: self.server, mock } + } +} + /// A prebuilt mock for uploading media. pub struct UploadEndpoint; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index f6fc42dd2ad..17e50d04f37 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -257,6 +257,7 @@ async fn test_ignored_unignored() { /// Puts a mounting point for /messages for a pagination request, matching /// against a precise `from` token given as `expected_from`, and returning the /// chunk of events and the next token as `end` (if available). +// TODO: replace this with the `mock_room_messages` from mocks.rs async fn mock_messages( server: &MockServer, expected_from: &str, diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index ecd00353937..d5ed2702d3f 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -25,18 +25,23 @@ use matrix_sdk::{ Client, }; use matrix_sdk_common::{executor::spawn, timeout::timeout}; -use matrix_sdk_test::{async_test, EventBuilder, JoinedRoomBuilder, ALICE, BOB}; +use matrix_sdk_test::{ + async_test, event_factory::EventFactory, EventBuilder, JoinedRoomBuilder, ALICE, BOB, +}; use once_cell::sync::Lazy; use ruma::{ event_id, - events::room::{ - member::{MembershipState, RoomMemberEventContent}, - message::RoomMessageEventContent, - name::RoomNameEventContent, - topic::RoomTopicEventContent, + events::{ + room::{ + member::{MembershipState, RoomMemberEventContent}, + message::RoomMessageEventContent, + name::RoomNameEventContent, + topic::RoomTopicEventContent, + }, + AnyStateEvent, StateEventType, }, owned_room_id, - serde::JsonObject, + serde::{JsonObject, Raw}, user_id, OwnedRoomId, }; use serde::Serialize; @@ -297,51 +302,22 @@ async fn test_read_messages_with_msgtype_capabilities() { // No messages from the driver assert_matches!(recv_message(&driver_handle).now_or_never(), None); + let f = EventFactory::new().room(&ROOM_ID).sender(user_id!("@example:localhost")); + { - let response_json = json!({ - "chunk": [ - { - "content": { - "body": "custom content", - "msgtype": "m.custom.element", - }, - "event_id": "$msda7m0df9E9op3", - "origin_server_ts": 152037220, - "sender": "@example:localhost", - "type": "m.room.message", - "room_id": &*ROOM_ID, - }, - { - "content": { - "body": "hello", - "msgtype": "m.text", - }, - "event_id": "$msda7m0df9E9op5", - "origin_server_ts": 152037280, - "sender": "@example:localhost", - "type": "m.room.message", - "room_id": &*ROOM_ID, - }, - { - "content": { - }, - "event_id": "$msda7m0df9E9op7", - "origin_server_ts": 152037290, - "sender": "@example:localhost", - "type": "m.reaction", - "room_id": &*ROOM_ID, - }, - ], - "end": "t47409-4357353_219380_26003_2269", - "start": "t392-516_47314_0_7_1_1_1_11444_1" - }); - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .and(query_param("limit", "3")) - .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) - .expect(1) - .mount(mock_server.server()) + let start = "t392-516_47314_0_7_1_1_1_11444_1".to_owned(); + let end = Some("t47409-4357353_219380_26003_2269".to_owned()); + let chun2 = vec![ + f.notice("custom content").event_id(event_id!("$msda7m0df9E9op3")).into_raw_timeline(), + f.text_msg("hello").event_id(event_id!("$msda7m0df9E9op5")).into_raw_timeline(), + f.reaction(event_id!("$event_id"), "annotation".to_owned()).into_raw_timeline(), + ]; + mock_server + .mock_room_messages() + .limit(3) + .ok(start, end, chun2, Vec::>::new()) + .mock_once() + .mount() .await; // Ask the driver to read messages @@ -508,11 +484,12 @@ async fn test_send_room_message() { negotiate_capabilities(&driver_handle, json!(["org.matrix.msc2762.send.event:m.room.message"])) .await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*$")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) - .expect(1) - .mount(mock_server.server()) + mock_server + .mock_room_send() + .for_type("m.room.message".into()) + .ok(event_id!("$foobar")) + .mock_once() + .mount() .await; send_request( @@ -549,11 +526,12 @@ async fn test_send_room_name() { ) .await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/?$")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) - .expect(1) - .mount(mock_server.server()) + mock_server + .mock_room_send_state() + .for_type(StateEventType::RoomName) + .ok(event_id!("$foobar")) + .mock_once() + .mount() .await; send_request( @@ -594,7 +572,8 @@ async fn test_send_delayed_message_event() { .await; Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*")) + .and(query_param("org.matrix.msc4140.delay", "1000")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "delay_id": "1234", }))) @@ -641,7 +620,8 @@ async fn test_send_delayed_state_event() { .await; Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/?$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/.*")) + .and(query_param("org.matrix.msc4140.delay", "1000")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "delay_id": "1234", }))) From 74119e8861d83275d3d0c8647a9350b9d28eb9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 3 Dec 2024 12:39:43 +0100 Subject: [PATCH 670/979] Revert "chore: Remove Ruma from the cargo-deny git dep allow list" This reverts commit f256fe4b24d9d180b6dcf3dc7ead4b06d2ef87ee. As discussed, we want to prioritize the testing of new Ruma features over stability. --- .deny.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.deny.toml b/.deny.toml index ee7d34d1e28..14353bb8f08 100644 --- a/.deny.toml +++ b/.deny.toml @@ -55,6 +55,8 @@ allow-git = [ "https://github.com/element-hq/tracing.git", # Sam as for the tracing dependency. "https://github.com/element-hq/paranoid-android.git", + # Well, it's Ruma. + "https://github.com/ruma/ruma", # A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10 "https://github.com/jplatte/const_panic", # A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22 From 4f28dd85bf94cb4117702ca1d8b51a597218fcf3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 3 Dec 2024 09:49:04 +0100 Subject: [PATCH 671/979] refactor: `TimelineStateTransaction::add_or_update_remote_event` always return `true`. The `add_or_update_remote_event` method always returns `true`. This patch updates the method to return nothing, and cleans up the call sites accordingly. This patch also adds comments to clarify the code flow. The bool value returned by `add_or_update_remote_event` was supposed to be `false` if the event was duplicated. First off, as soon as the `Timeline` receives its events from the `EventCache` via `VectorDiff`, the `event_cache::Deduplicator` will take care of deduplication, so the `Timeline` won't have to handle that itself. Second, `add_or_update_remote_event` was sometimes removing an event, but it was re-inserting a new one immediately without returning `false`: it was never returning `false` because a new event was always added. --- .../src/timeline/controller/state.rs | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 863848b63d9..d2030d8cfbf 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -454,6 +454,7 @@ impl TimelineStateTransaction<'_> { let (event_id, sender, timestamp, txn_id, event_kind, should_add) = match raw.deserialize() { + // Classical path: the event is valid, can be deserialized, everything is alright. Ok(event) => { let event_id = event.event_id().to_owned(); let room_version = room_data_provider.room_version(); @@ -513,7 +514,10 @@ impl TimelineStateTransaction<'_> { ) } + // The event seems invalid… Err(e) => match raw.deserialize_as::() { + // The event can be partially deserialized, and it is allowed to be added to the + // timeline. Ok(event) if settings.add_failed_to_parse => ( event.event_id().to_owned(), event.sender().to_owned(), @@ -523,6 +527,8 @@ impl TimelineStateTransaction<'_> { true, ), + // The event can be partially deserialized, but it is NOT allowed to be added to + // the timeline. Ok(event) => { let event_type = event.event_type(); let event_id = event.event_id(); @@ -536,24 +542,30 @@ impl TimelineStateTransaction<'_> { timestamp: Some(event.origin_server_ts()), visible: false, }; - let _event_added_or_updated = self - .add_or_update_remote_event( - event_meta, - position, - room_data_provider, - settings, - ) - .await; + + // Remember the event before returning prematurely. + self.add_or_update_remote_event( + event_meta, + position, + room_data_provider, + settings, + ) + .await; return HandleEventResult::default(); } + // The event can NOT be partially deserialized, it seems really broken. Err(e) => { let event_type: Option = raw.get_field("type").ok().flatten(); let event_id: Option = raw.get_field("event_id").ok().flatten(); - warn!(event_type, event_id, "Failed to deserialize timeline event: {e}"); + warn!( + event_type, + event_id, "Failed to deserialize timeline event even without content: {e}" + ); let event_id = event_id.and_then(|s| EventId::parse(s).ok()); + if let Some(event_id) = &event_id { let sender: Option = raw.get_field("sender").ok().flatten(); let is_own_event = @@ -568,14 +580,15 @@ impl TimelineStateTransaction<'_> { timestamp, visible: false, }; - let _event_added_or_updated = self - .add_or_update_remote_event( - event_meta, - position, - room_data_provider, - settings, - ) - .await; + + // Remember the event before returning prematurely. + self.add_or_update_remote_event( + event_meta, + position, + room_data_provider, + settings, + ) + .await; } return HandleEventResult::default(); @@ -593,15 +606,8 @@ impl TimelineStateTransaction<'_> { visible: should_add, }; - let event_added_or_updated = self - .add_or_update_remote_event(event_meta, position, room_data_provider, settings) - .await; - - // If the event has not been added or updated, it's because it's a duplicated - // event. Let's return early. - if !event_added_or_updated { - return HandleEventResult::default(); - } + // Remember the event. + self.add_or_update_remote_event(event_meta, position, room_data_provider, settings).await; let sender_profile = room_data_provider.profile_from_user_id(&sender).await; let ctx = TimelineEventContext { @@ -629,6 +635,7 @@ impl TimelineStateTransaction<'_> { should_add_new_items: should_add, }; + // Handle the event to create or update a timeline item. TimelineEventHandler::new(self, ctx).handle_event(day_divider_adjuster, event_kind).await } @@ -693,17 +700,13 @@ impl TimelineStateTransaction<'_> { /// [`TimelineMetadata::all_remote_events`] collection. /// /// This method also adjusts read receipt if needed. - /// - /// It returns `true` if the event has been added or updated, `false` - /// otherwise. The latter happens if the event already exists, i.e. if - /// an existing event is requested to be added. async fn add_or_update_remote_event( &mut self, event_meta: FullEventMeta<'_>, position: TimelineItemPosition, room_data_provider: &P, settings: &TimelineSettings, - ) -> bool { + ) { // Detect if an event already exists in [`TimelineMetadata::all_remote_events`]. // // Returns its position, in this case. @@ -765,8 +768,6 @@ impl TimelineStateTransaction<'_> { self.maybe_add_implicit_read_receipt(event_meta); } - - true } fn adjust_day_dividers(&mut self, mut adjuster: DayDividerAdjuster) { From 9be8578aff4a6facb358e8148cc2ac65408d77e9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 3 Dec 2024 13:07:49 +0100 Subject: [PATCH 672/979] doc(ui): Explain why `all_remote_events` is necessary. --- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index d2030d8cfbf..feff572776e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -544,6 +544,7 @@ impl TimelineStateTransaction<'_> { }; // Remember the event before returning prematurely. + // See [`TimelineMetadata::all_remote_events`]. self.add_or_update_remote_event( event_meta, position, @@ -582,6 +583,7 @@ impl TimelineStateTransaction<'_> { }; // Remember the event before returning prematurely. + // See [`TimelineMetadata::all_remote_events`]. self.add_or_update_remote_event( event_meta, position, @@ -607,6 +609,7 @@ impl TimelineStateTransaction<'_> { }; // Remember the event. + // See [`TimelineMetadata::all_remote_events`]. self.add_or_update_remote_event(event_meta, position, room_data_provider, settings).await; let sender_profile = room_data_provider.profile_from_user_id(&sender).await; @@ -918,6 +921,9 @@ pub(in crate::timeline) struct TimelineMetadata { /// List of all the remote events as received in the timeline, even the ones /// that are discarded in the timeline items. + /// + /// This is useful to get this for the moment as it helps the `Timeline` to + /// compute read receipts and read markers. pub all_remote_events: VecDeque, /// State helping matching reactions to their associated events, and From b02fd92ad0f018e79cd628501b438ad41dfc2b53 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 3 Dec 2024 10:31:20 +0100 Subject: [PATCH 673/979] refactor(ui): Introduce the `AllRemoteEvents` type. This patch replaces `VecDeque` by `AllRemoteEvents` which is a wrapper type around `VecDeque`, but this new type aims at adding semantics API rather than a generic API. It also helps to isolate the use of these values and to know precisely when and how they are used. As a first step, `AllRemoteEvents` implements a generic API to not break the existing code. Next patches will revisit that a little bit step by step. --- .../src/timeline/controller/mod.rs | 2 +- .../src/timeline/controller/state.rs | 55 ++++++++++++++++++- .../src/timeline/read_receipts.rs | 12 ++-- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 29cd9fdd380..e6ca9b54bf6 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -52,7 +52,7 @@ use tracing::{ }; pub(super) use self::state::{ - EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, + AllRemoteEvents, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, TimelineNewItemPosition, TimelineState, TimelineStateTransaction, }; use super::{ diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index feff572776e..b040f88d654 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -13,7 +13,10 @@ // limitations under the License. use std::{ - collections::{HashMap, VecDeque}, + collections::{ + vec_deque::{Iter, IterMut}, + HashMap, VecDeque, + }, future::Future, num::NonZeroUsize, sync::{Arc, RwLock}, @@ -715,7 +718,7 @@ impl TimelineStateTransaction<'_> { // Returns its position, in this case. fn event_already_exists( new_event_id: &EventId, - all_remote_events: &VecDeque, + all_remote_events: &AllRemoteEvents, ) -> Option { all_remote_events.iter().position(|EventMeta { event_id, .. }| event_id == new_event_id) } @@ -924,7 +927,7 @@ pub(in crate::timeline) struct TimelineMetadata { /// /// This is useful to get this for the moment as it helps the `Timeline` to /// compute read receipts and read markers. - pub all_remote_events: VecDeque, + pub all_remote_events: AllRemoteEvents, /// State helping matching reactions to their associated events, and /// stashing pending reactions. @@ -1139,6 +1142,52 @@ impl TimelineMetadata { } } +/// A type for all remote events. +/// +/// Having this type helps to know exactly which parts of the code and how they +/// use all remote events. It also helps to give a bit of semantics on top of +/// them. +#[derive(Clone, Debug, Default)] +pub(crate) struct AllRemoteEvents(VecDeque); + +impl AllRemoteEvents { + /// Return a front-to-back iterator over all remote events. + pub fn iter(&self) -> Iter<'_, EventMeta> { + self.0.iter() + } + + /// Return a front-to-back iterator over all remote events as mutable + /// references. + pub fn iter_mut(&mut self) -> IterMut<'_, EventMeta> { + self.0.iter_mut() + } + + /// Remove all remote events. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Insert a new remote event at the front of all the others. + pub fn push_front(&mut self, event_meta: EventMeta) { + self.0.push_front(event_meta) + } + + /// Insert a new remote event at the back of all the others. + pub fn push_back(&mut self, event_meta: EventMeta) { + self.0.push_back(event_meta) + } + + /// Remove one remote event at a specific index, and return it if it exists. + pub fn remove(&mut self, event_index: usize) -> Option { + self.0.remove(event_index) + } + + /// Return a reference to the last remote event if it exists. + pub fn back(&self) -> Option<&EventMeta> { + self.0.back() + } +} + /// Full metadata about an event. /// /// Only used to group function parameters. diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs index a97cbdc9340..a12ebbb0967 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs @@ -12,11 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - cmp::Ordering, - collections::{HashMap, VecDeque}, - sync::Arc, -}; +use std::{cmp::Ordering, collections::HashMap, sync::Arc}; use eyeball_im::ObservableVectorTransaction; use futures_core::Stream; @@ -31,7 +27,7 @@ use tracing::{debug, error, warn}; use super::{ controller::{ - EventMeta, FullEventMeta, TimelineMetadata, TimelineState, TimelineStateTransaction, + AllRemoteEvents, FullEventMeta, TimelineMetadata, TimelineState, TimelineStateTransaction, }, traits::RoomDataProvider, util::{rfind_event_by_id, RelativePosition}, @@ -103,7 +99,7 @@ impl ReadReceipts { &mut self, new_receipt: FullReceipt<'_>, is_own_user_id: bool, - all_events: &VecDeque, + all_events: &AllRemoteEvents, timeline_items: &mut ObservableVectorTransaction<'_, Arc>, ) { // Get old receipt. @@ -243,7 +239,7 @@ impl ReadReceipts { pub(super) fn compute_event_receipts( &self, event_id: &EventId, - all_events: &VecDeque, + all_events: &AllRemoteEvents, at_end: bool, ) -> IndexMap { let mut all_receipts = self.get_event_receipts(event_id).cloned().unwrap_or_default(); From cabde8ed11cd524eaf3e7f79ba52aab1e1fad59e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 3 Dec 2024 10:34:52 +0100 Subject: [PATCH 674/979] refactor(ui): `AllRemoteEvents::back` becomes `last` to add semantics. This patch renames `AllRemoteEvents::back` to `last` so that it now gets a specific semantics instead of being generic. --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 2 +- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index e6ca9b54bf6..13638ed6230 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1529,7 +1529,7 @@ impl TimelineController { /// it's folded into another timeline item. pub(crate) async fn latest_event_id(&self) -> Option { let state = self.state.read().await; - state.meta.all_remote_events.back().map(|event_meta| &event_meta.event_id).cloned() + state.meta.all_remote_events.last().map(|event_meta| &event_meta.event_id).cloned() } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index b040f88d654..4587b664357 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -1183,7 +1183,7 @@ impl AllRemoteEvents { } /// Return a reference to the last remote event if it exists. - pub fn back(&self) -> Option<&EventMeta> { + pub fn last(&self) -> Option<&EventMeta> { self.0.back() } } From d7dff5b026bd68945c5853175f1a52e186543297 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 3 Dec 2024 10:39:26 +0100 Subject: [PATCH 675/979] refactor(ui): Add `AllRemoteEvents::get_by_event_id_mut`. Having a mutable iterator can be dangerous and is probably too generic regarding the safety we want to add around the `AllRemoteEvents` type. This patch removes `iter_mut` and replaces it by its only use case: `get_by_event_id_mut`. --- .../src/timeline/controller/state.rs | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4587b664357..1800bebdab7 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -13,10 +13,7 @@ // limitations under the License. use std::{ - collections::{ - vec_deque::{Iter, IterMut}, - HashMap, VecDeque, - }, + collections::{vec_deque::Iter, HashMap, VecDeque}, future::Future, num::NonZeroUsize, sync::{Arc, RwLock}, @@ -745,11 +742,8 @@ impl TimelineStateTransaction<'_> { } TimelineItemPosition::UpdateDecrypted { .. } => { - if let Some(event) = self - .meta - .all_remote_events - .iter_mut() - .find(|e| e.event_id == event_meta.event_id) + if let Some(event) = + self.meta.all_remote_events.get_by_event_id_mut(event_meta.event_id) { if event.visible != event_meta.visible { event.visible = event_meta.visible; @@ -1156,12 +1150,6 @@ impl AllRemoteEvents { self.0.iter() } - /// Return a front-to-back iterator over all remote events as mutable - /// references. - pub fn iter_mut(&mut self) -> IterMut<'_, EventMeta> { - self.0.iter_mut() - } - /// Remove all remote events. pub fn clear(&mut self) { self.0.clear(); @@ -1186,6 +1174,11 @@ impl AllRemoteEvents { pub fn last(&self) -> Option<&EventMeta> { self.0.back() } + + /// Get a mutable reference to a specific remote event by its ID. + pub fn get_by_event_id_mut(&mut self, event_id: &EventId) -> Option<&mut EventMeta> { + self.0.iter_mut().rev().find(|event_meta| event_meta.event_id == event_id) + } } /// Full metadata about an event. From 5907104e0ea694e74529a64539e313922707bff3 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 29 Nov 2024 13:39:35 +0000 Subject: [PATCH 676/979] task(backup_tests): Use helper functions to shorten exists_on_server tests --- .../matrix-sdk/src/encryption/backups/mod.rs | 110 ++++++++++-------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index 437c6d8746c..4d017aa46a3 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -1010,7 +1010,7 @@ mod test { use serde_json::json; use wiremock::{ matchers::{header, method, path}, - Mock, MockServer, ResponseTemplate, + Mock, MockGuard, MockServer, ResponseTemplate, }; use super::*; @@ -1124,22 +1124,7 @@ mod test { let client = logged_in_client(Some(server.uri())).await; { - let _scope = Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", - "auth_data": { - "public_key": "abcdefg", - "signatures": {}, - }, - "count": 42, - "etag": "anopaquestring", - "version": "1", - }))) - .expect(1) - .mount_as_scoped(&server) - .await; + let _scope = mock_backup_exists(&server).await; let exists = client .encryption() @@ -1148,20 +1133,11 @@ mod test { .await .expect("We should be able to check if backups exist on the server"); - assert!(exists, "We should deduce that a backup exist on the server"); + assert!(exists, "We should deduce that a backup exists on the server"); } { - let _scope = Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(404).set_body_json(json!({ - "errcode": "M_NOT_FOUND", - "error": "No current backup version" - }))) - .expect(1) - .mount_as_scoped(&server) - .await; + let _scope = mock_backup_none(&server).await; let exists = client .encryption() @@ -1170,21 +1146,11 @@ mod test { .await .expect("We should be able to check if backups exist on the server"); - assert!(!exists, "We should deduce that no backup exist on the server"); + assert!(!exists, "We should deduce that no backup exists on the server"); } { - let _scope = Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(429).set_body_json(json!({ - "errcode": "M_LIMIT_EXCEEDED", - "error": "Too many requests", - "retry_after_ms": 2000 - }))) - .expect(1) - .mount_as_scoped(&server) - .await; + let _scope = mock_backup_too_many_requests(&server).await; client.encryption().backups().exists_on_server().await.expect_err( "If the /version endpoint returns a non 404 error we should throw an error", @@ -1192,13 +1158,7 @@ mod test { } { - let _scope = Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(404)) - .expect(1) - .mount_as_scoped(&server) - .await; + let _scope = mock_backup_404(&server); client.encryption().backups().exists_on_server().await.expect_err( "If the /version endpoint returns a non-Matrix 404 error we should throw an error", @@ -1282,4 +1242,60 @@ mod test { server.verify().await; } + + async fn mock_backup_exists(server: &MockServer) -> MockGuard { + Mock::given(method("GET")) + .and(path("_matrix/client/r0/room_keys/version")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "auth_data": { + "public_key": "abcdefg", + "signatures": {}, + }, + "count": 42, + "etag": "anopaquestring", + "version": "1", + }))) + .expect(1) + .mount_as_scoped(server) + .await + } + + async fn mock_backup_none(server: &MockServer) -> MockGuard { + Mock::given(method("GET")) + .and(path("_matrix/client/r0/room_keys/version")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "No current backup version" + }))) + .expect(1) + .mount_as_scoped(server) + .await + } + + async fn mock_backup_too_many_requests(server: &MockServer) -> MockGuard { + Mock::given(method("GET")) + .and(path("_matrix/client/r0/room_keys/version")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(429).set_body_json(json!({ + "errcode": "M_LIMIT_EXCEEDED", + "error": "Too many requests", + "retry_after_ms": 2000 + }))) + .expect(1) + .mount_as_scoped(server) + .await + } + + async fn mock_backup_404(server: &MockServer) -> MockGuard { + Mock::given(method("GET")) + .and(path("_matrix/client/r0/room_keys/version")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(404)) + .expect(1) + .mount_as_scoped(server) + .await + } } From 5f7fb4699ae6e615e3ddce6b5bfeafa7d6115f21 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 29 Nov 2024 13:45:59 +0000 Subject: [PATCH 677/979] task(backup_tests): Split exists_on_server test into separate tests --- .../matrix-sdk/src/encryption/backups/mod.rs | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index 4d017aa46a3..bf64e580b42 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -1119,35 +1119,47 @@ mod test { } #[async_test] - async fn test_exists_on_server() { + async fn test_when_a_backup_exists_then_exists_on_server_returns_true() { let server = MockServer::start().await; let client = logged_in_client(Some(server.uri())).await; - { - let _scope = mock_backup_exists(&server).await; + let _scope = mock_backup_exists(&server).await; - let exists = client - .encryption() - .backups() - .exists_on_server() - .await - .expect("We should be able to check if backups exist on the server"); + let exists = client + .encryption() + .backups() + .exists_on_server() + .await + .expect("We should be able to check if backups exist on the server"); - assert!(exists, "We should deduce that a backup exists on the server"); - } + assert!(exists, "We should deduce that a backup exists on the server"); - { - let _scope = mock_backup_none(&server).await; + server.verify().await; + } - let exists = client - .encryption() - .backups() - .exists_on_server() - .await - .expect("We should be able to check if backups exist on the server"); + #[async_test] + async fn test_when_no_backup_exists_then_exists_on_server_returns_false() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; - assert!(!exists, "We should deduce that no backup exists on the server"); - } + let _scope = mock_backup_none(&server).await; + + let exists = client + .encryption() + .backups() + .exists_on_server() + .await + .expect("We should be able to check if backups exist on the server"); + + assert!(!exists, "We should deduce that no backup exists on the server"); + + server.verify().await; + } + + #[async_test] + async fn test_when_server_returns_an_error_then_exists_on_server_returns_an_error() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; { let _scope = mock_backup_too_many_requests(&server).await; From 9002f826590560242360a52da72c9ac4812b6a45 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 3 Dec 2024 13:33:32 +0000 Subject: [PATCH 678/979] task(backup_tests): Move mock helpers into MatrixMockServer --- .../matrix-sdk/src/encryption/backups/mod.rs | 88 +++---------------- crates/matrix-sdk/src/test_utils/mocks.rs | 75 ++++++++++++++++ 2 files changed, 89 insertions(+), 74 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index bf64e580b42..d0d0ad43e59 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -1010,11 +1010,11 @@ mod test { use serde_json::json; use wiremock::{ matchers::{header, method, path}, - Mock, MockGuard, MockServer, ResponseTemplate, + Mock, MockServer, ResponseTemplate, }; use super::*; - use crate::test_utils::logged_in_client; + use crate::test_utils::{logged_in_client, mocks::MatrixMockServer}; fn room_key() -> ExportedRoomKey { let json = json!({ @@ -1120,10 +1120,10 @@ mod test { #[async_test] async fn test_when_a_backup_exists_then_exists_on_server_returns_true() { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - let _scope = mock_backup_exists(&server).await; + server.mock_room_keys_version().exists().expect(1).mount().await; let exists = client .encryption() @@ -1133,16 +1133,14 @@ mod test { .expect("We should be able to check if backups exist on the server"); assert!(exists, "We should deduce that a backup exists on the server"); - - server.verify().await; } #[async_test] async fn test_when_no_backup_exists_then_exists_on_server_returns_false() { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - let _scope = mock_backup_none(&server).await; + server.mock_room_keys_version().none().expect(1).mount().await; let exists = client .encryption() @@ -1152,17 +1150,16 @@ mod test { .expect("We should be able to check if backups exist on the server"); assert!(!exists, "We should deduce that no backup exists on the server"); - - server.verify().await; } #[async_test] async fn test_when_server_returns_an_error_then_exists_on_server_returns_an_error() { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; { - let _scope = mock_backup_too_many_requests(&server).await; + let _scope = + server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await; client.encryption().backups().exists_on_server().await.expect_err( "If the /version endpoint returns a non 404 error we should throw an error", @@ -1170,14 +1167,13 @@ mod test { } { - let _scope = mock_backup_404(&server); + let _scope = + server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await; client.encryption().backups().exists_on_server().await.expect_err( "If the /version endpoint returns a non-Matrix 404 error we should throw an error", ); } - - server.verify().await; } #[async_test] @@ -1254,60 +1250,4 @@ mod test { server.verify().await; } - - async fn mock_backup_exists(server: &MockServer) -> MockGuard { - Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", - "auth_data": { - "public_key": "abcdefg", - "signatures": {}, - }, - "count": 42, - "etag": "anopaquestring", - "version": "1", - }))) - .expect(1) - .mount_as_scoped(server) - .await - } - - async fn mock_backup_none(server: &MockServer) -> MockGuard { - Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(404).set_body_json(json!({ - "errcode": "M_NOT_FOUND", - "error": "No current backup version" - }))) - .expect(1) - .mount_as_scoped(server) - .await - } - - async fn mock_backup_too_many_requests(server: &MockServer) -> MockGuard { - Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(429).set_body_json(json!({ - "errcode": "M_LIMIT_EXCEEDED", - "error": "Too many requests", - "retry_after_ms": 2000 - }))) - .expect(1) - .mount_as_scoped(server) - .await - } - - async fn mock_backup_404(server: &MockServer) -> MockGuard { - Mock::given(method("GET")) - .and(path("_matrix/client/r0/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(404)) - .expect(1) - .mount_as_scoped(server) - .await - } } diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 6774102f227..46981c4b6c5 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -560,6 +560,36 @@ impl MatrixMockServer { let mock = Mock::given(method("POST")).and(path_regex(r"/_matrix/client/v3/publicRooms")); MockEndpoint { mock, server: &self.server, endpoint: PublicRoomsEndpoint } } + + /// Create a prebuilt mock for fetching information about key storage + /// backups. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "e2e-encryption")] + /// # { + /// # tokio_test::block_on(async { + /// use matrix_sdk::test_utils::mocks::MatrixMockServer; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_keys_version().exists().expect(1).mount().await; + /// + /// let exists = + /// client.encryption().backups().exists_on_server().await.unwrap(); + /// + /// assert!(exists); + /// # }); + /// # } + /// ``` + pub fn mock_room_keys_version(&self) -> MockEndpoint<'_, RoomKeysVersionEndpoint> { + let mock = Mock::given(method("GET")) + .and(path_regex(r"_matrix/client/v3/room_keys/version")) + .and(header("authorization", "Bearer 1234")); + MockEndpoint { mock, server: &self.server, endpoint: RoomKeysVersionEndpoint } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -1503,3 +1533,48 @@ impl<'a> MockEndpoint<'a, PublicRoomsEndpoint> { MatrixMock { server: self.server, mock } } } + +/// A prebuilt mock for `room_keys/version`: storage ("backup") of room keys. +pub struct RoomKeysVersionEndpoint; + +impl<'a> MockEndpoint<'a, RoomKeysVersionEndpoint> { + /// Returns an endpoint that says there is a single room keys backup + pub fn exists(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "auth_data": { + "public_key": "abcdefg", + "signatures": {}, + }, + "count": 42, + "etag": "anopaquestring", + "version": "1", + }))); + MatrixMock { server: self.server, mock } + } + + /// Returns an endpoint that says there is no room keys backup + pub fn none(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "No current backup version" + }))); + MatrixMock { server: self.server, mock } + } + + /// Returns an endpoint that 429 errors when we get it + pub fn error429(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(429).set_body_json(json!({ + "errcode": "M_LIMIT_EXCEEDED", + "error": "Too many requests", + "retry_after_ms": 2000 + }))); + MatrixMock { server: self.server, mock } + } + + /// Returns an endpoint that 404 errors when we get it + pub fn error404(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(404)); + MatrixMock { server: self.server, mock } + } +} From 9c381c102256b813d9d138282c58aaa9458bb7c9 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 28 Nov 2024 14:52:04 +0200 Subject: [PATCH 679/979] feat(ffi): expose a generic `message_filtered_timeline` that can be configured to only include RoomMessage type events and filter those further based on their message type. Virtual timeline items will still be provided and the `default_event_filter` will be applied before everything else. Instances of these timelines will be used to power the 2 different tabs shown on the new media browser. The client will be responsible for interacting with it similar to a normal timeline and transforming its data into something renderable e.g. section by date separators (which will be made configurable in a follow up PR) --- bindings/matrix-sdk-ffi/src/event.rs | 38 +++++++++++++++++++++- bindings/matrix-sdk-ffi/src/room.rs | 48 ++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index 1fbc423d54f..2be9989959e 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -3,7 +3,10 @@ use matrix_sdk::IdParseError; use matrix_sdk_ui::timeline::TimelineEventItemId; use ruma::{ events::{ - room::{message::Relation, redaction::SyncRoomRedactionEvent}, + room::{ + message::{MessageType as RumaMessageType, Relation}, + redaction::SyncRoomRedactionEvent, + }, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent, RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent, @@ -356,6 +359,39 @@ impl From for ruma::events::MessageLikeEventType { } } +#[derive(Debug, PartialEq, Clone, uniffi::Enum)] +pub enum RoomMessageEventMessageType { + Audio, + Emote, + File, + Image, + Location, + Notice, + ServerNotice, + Text, + Video, + VerificationRequest, + Other, +} + +impl From for RoomMessageEventMessageType { + fn from(val: ruma::events::room::message::MessageType) -> Self { + match val { + RumaMessageType::Audio { .. } => Self::Audio, + RumaMessageType::Emote { .. } => Self::Emote, + RumaMessageType::File { .. } => Self::File, + RumaMessageType::Image { .. } => Self::Image, + RumaMessageType::Location { .. } => Self::Location, + RumaMessageType::Notice { .. } => Self::Notice, + RumaMessageType::ServerNotice { .. } => Self::ServerNotice, + RumaMessageType::Text { .. } => Self::Text, + RumaMessageType::Video { .. } => Self::Video, + RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest, + _ => Self::Other, + } + } +} + /// Contains the 2 possible identifiers of an event, either it has a remote /// event id or a local transaction id, never both or none. #[derive(Clone, uniffi::Enum)] diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index dcaf6f2efc4..eeb6eb4af17 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -11,7 +11,7 @@ use matrix_sdk::{ ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, RoomHero as SdkRoomHero, RoomMemberships, RoomState, }; -use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus}; +use matrix_sdk_ui::timeline::{default_event_filter, PaginationError, RoomExt, TimelineFocus}; use mime::Mime; use ruma::{ api::client::room::report_content, @@ -23,7 +23,7 @@ use ruma::{ message::RoomMessageEventContentWithoutRelation, power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource, }, - TimelineEventType, + AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType, }, EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId, }; @@ -34,7 +34,7 @@ use super::RUNTIME; use crate::{ chunk_iterator::ChunkIterator, error::{ClientError, MediaInfoError, RoomError}, - event::{MessageLikeEventType, StateEventType}, + event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType}, identity_status_change::IdentityStatusChange, room_info::RoomInfo, room_member::RoomMember, @@ -260,6 +260,48 @@ impl Room { Ok(Timeline::new(timeline)) } + /// A timeline instance that can be configured to only include RoomMessage + /// type events and filter those further based on their message type. + /// + /// Virtual timeline items will still be provided and the + /// `default_event_filter` will be applied before everything else. + /// + /// # Arguments + /// + /// * `internal_id_prefix` - An optional String that will be prepended to + /// all the timeline item's internal IDs, making it possible to + /// distinguish different timeline instances from each other. + /// + /// * `allowed_message_types` - A list of `RoomMessageEventMessageType` that + /// will be allowed to appear in the timeline + pub async fn message_filtered_timeline( + &self, + internal_id_prefix: Option, + allowed_message_types: Vec, + ) -> Result, ClientError> { + let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner); + + if let Some(internal_id_prefix) = internal_id_prefix { + builder = builder.with_internal_id_prefix(internal_id_prefix); + } + + builder = builder.event_filter(move |event, room_version_id| { + default_event_filter(event, room_version_id) + && match event { + AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() { + Some(AnyMessageLikeEventContent::RoomMessage(content)) => { + allowed_message_types.contains(&content.msgtype.into()) + } + _ => false, + }, + _ => false, + } + }); + + let timeline = builder.build().await?; + Ok(Timeline::new(timeline)) + } + pub fn is_encrypted(&self) -> Result { Ok(RUNTIME.block_on(self.inner.is_encrypted())?) } From a948be9c855ea4e66ef50525713a9e463fb6a340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 4 Dec 2024 10:28:30 +0100 Subject: [PATCH 680/979] chore: Downgrade xshell due to broken stdin interactions Since xshell 0.2.3 the behavior of the run() function has changed in a incompatible manner. Namely the stdin for the run() function no longer inherits stdin from the shell. This makes it impossible for commands that are executed by the run() function to accept input from the shell. We don't use this functionality in many places but the `xtask release prepare` command is now broken. Let's just pin xshell to a working version while we wait for this to be resolved upstream. Upstream-issue: https://github.com/matklad/xshell/issues/63 --- Cargo.lock | 8 ++++---- xtask/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5c88e89afc..be7d083fb1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6814,18 +6814,18 @@ dependencies = [ [[package]] name = "xshell" -version = "0.2.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" +checksum = "6d47097dc5c85234b1e41851b3422dd6d19b3befdd35b4ae5ce386724aeca981" dependencies = [ "xshell-macros", ] [[package]] name = "xshell-macros" -version = "0.2.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" +checksum = "88301b56c26dd9bf5c43d858538f82d6f3f7764767defbc5d34e59459901c41a" [[package]] name = "xtask" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index e21fcd51f1b..32df85c6870 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -16,7 +16,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } fs_extra = "1" uniffi_bindgen = { workspace = true } -xshell = "0.2.7" +xshell = "0.2.2" [package.metadata.release] release = false From 42778dc79ddc7b922ae9b882198ff41630f1ebc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 3 Dec 2024 15:32:30 +0100 Subject: [PATCH 681/979] chore: Replace git-cliff in the weekly-report command --- cliff-weekly-report.toml | 47 ---------------------------------------- xtask/src/release.rs | 27 ++++++++++++++++------- 2 files changed, 19 insertions(+), 55 deletions(-) delete mode 100644 cliff-weekly-report.toml diff --git a/cliff-weekly-report.toml b/cliff-weekly-report.toml deleted file mode 100644 index bfd0732c68f..00000000000 --- a/cliff-weekly-report.toml +++ /dev/null @@ -1,47 +0,0 @@ -# This git-cliff configuration file is used to generate weekly reports for This -# Week in Matrix amongst others. - -[changelog] -header = """ -# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }}) -""" -body = """ -{% for commit in commits %} - {% set_global commit_message = commit.message -%} - {% for footer in commit.footers -%} - {% if footer.token | lower == "changelog" -%} - {% set_global commit_message = footer.value -%} - {% elif footer.token | lower == "breaking-change" -%} - {% set_global commit_message = footer.value -%} - {% endif -%} - {% endfor -%} - - {{ commit_message | upper_first }} -{% endfor %} -""" -trim = true -footer = "" - -[git] -conventional_commits = true -filter_unconventional = true -commit_preprocessors = [ - { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"}, -] -commit_parsers = [ - { message = "^feat", group = "Features" }, - { message = "^fix", group = "Bug Fixes" }, - { message = "^doc", group = "Documentation" }, - { message = "^perf", group = "Performance" }, - { message = "^refactor", group = "Refactor", skip = true }, - { message = "^chore\\(release\\): prepare for", skip = true }, - { message = "^chore", skip = true }, - { message = "^style", group = "Styling", skip = true }, - { message = "^test", skip = true }, - { message = "^ci", skip = true }, -] -filter_commits = true -tag_pattern = "[0-9]*" -skip_tags = "" -ignore_tags = "" -date_order = false -sort_commits = "newest" diff --git a/xtask/src/release.rs b/xtask/src/release.rs index 2b33a07276f..ff75c0a1c1c 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -98,9 +98,9 @@ fn check_prerequisites() { std::process::exit(1); } - if cmd!(sh, "git cliff --version").quiet().ignore_stdout().run().is_err() { - eprintln!("This command requires git-cliff, please install it."); - eprintln!("More info can be found at: https://git-cliff.org/docs/installation/"); + if cmd!(sh, "gh version").quiet().ignore_stdout().run().is_err() { + eprintln!("This command requires GitHub CLI, please install it."); + eprintln!("More info can be found at: https://cli.github.com/"); std::process::exit(1); } @@ -145,14 +145,25 @@ fn publish(execute: bool) -> Result<()> { } fn weekly_report() -> Result<()> { + const JSON_FIELDS: &'static str = "title,number,url,author"; + let sh = sh(); - let lines = cmd!(sh, "git log --pretty=format:%H --since='1 week ago'").read()?; - let Some(start) = lines.split_whitespace().last() else { - panic!("Could not find a start range for the git commit range.") - }; + let one_week_ago = cmd!(sh, "date -d '1 week ago' +%Y-%m-%d").read()?; + let today = cmd!(sh, "date +%Y-%m-%d").read()?; + + let _env_pager = sh.push_env("GH_PAGER", ""); + + let header = format!("# This Week in the Matrix Rust SDK ({today})\n\n"); + let template = "{{range .}}- {{.title}} by @{{.author.login}}{{\"\\n\\n\"}}{{end}}"; + let template = format!("{header}{template}"); - cmd!(sh, "git cliff --config cliff-weekly-report.toml {start}..HEAD").run()?; + cmd!( + sh, + "gh pr list --search created:>{one_week_ago} --json {JSON_FIELDS} --template {template}" + ) + .quiet() + .run()?; Ok(()) } From bab979aaf47e146887e77a3941b63f4154d7c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 3 Dec 2024 17:36:49 +0100 Subject: [PATCH 682/979] chore: Update our changelogs --- crates/matrix-sdk-base/CHANGELOG.md | 11 ++++++++ crates/matrix-sdk-common/CHANGELOG.md | 24 +++++++++++++++++ crates/matrix-sdk-crypto/CHANGELOG.md | 4 +++ crates/matrix-sdk-indexeddb/CHANGELOG.md | 4 +++ crates/matrix-sdk-qrcode/CHANGELOG.md | 4 +++ crates/matrix-sdk-sqlite/CHANGELOG.md | 11 ++++++++ .../matrix-sdk-store-encryption/CHANGELOG.md | 4 +++ crates/matrix-sdk-ui/CHANGELOG.md | 18 +++++++++++++ crates/matrix-sdk/CHANGELOG.md | 26 +++++++++++++++++++ 9 files changed, 106 insertions(+) diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index a1a8fc2753d..2e596658ffc 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + +### Bug Fixes + +- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is + called. Previously, items following the removed item were not shifted left by + one position, leaving them at incorrect indices. + ([#4346](https://github.com/matrix-org/matrix-rust-sdk/pull/4346)) + ## [0.8.0] - 2024-11-19 ### Bug Fixes diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md index 3fec5ae4492..683eed16852 100644 --- a/crates/matrix-sdk-common/CHANGELOG.md +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + +### Bug Fixes + +- Change the behavior of `LinkedChunk::new_with_update_history()` to emit an + `Update::NewItemsChunk` when a new, initial empty, chunk is created. + ([#4327](https://github.com/matrix-org/matrix-rust-sdk/pull/4321)) + +- [**breaking**] Make `Room::history_visibility()` return an Option, and + introduce `Room::history_visibility_or_default()` to return a better + sensible default, according to the spec. + ([#4325](https://github.com/matrix-org/matrix-rust-sdk/pull/4325)) + +- Clear the internal state of the `AsVector` struct if an `Update::Clear` + state has been received. + ([#4321](https://github.com/matrix-org/matrix-rust-sdk/pull/4321)) + +### Documentation + +- Document that a decrypted raw event always has a room id. + ([#728e1fd](https://github.com/matrix-org/matrix-rust-sdk/commit/728e1fda2ae9f1bfa87df162aa553040be705223)) + ## [0.8.0] - 2024-11-19 ### Refactor diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 9809ea75244..e069c95dd4e 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + ## [0.8.0] - 2024-11-19 ### Features diff --git a/crates/matrix-sdk-indexeddb/CHANGELOG.md b/crates/matrix-sdk-indexeddb/CHANGELOG.md index f66e695a48c..4372794f5a1 100644 --- a/crates/matrix-sdk-indexeddb/CHANGELOG.md +++ b/crates/matrix-sdk-indexeddb/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + ## [0.8.0] - 2024-11-19 ### Features diff --git a/crates/matrix-sdk-qrcode/CHANGELOG.md b/crates/matrix-sdk-qrcode/CHANGELOG.md index dff6a57ebbf..9235a4b9ff2 100644 --- a/crates/matrix-sdk-qrcode/CHANGELOG.md +++ b/crates/matrix-sdk-qrcode/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + ## [0.8.0] - 2024-11-19 No notable changes in this release. diff --git a/crates/matrix-sdk-sqlite/CHANGELOG.md b/crates/matrix-sdk-sqlite/CHANGELOG.md index d525613a28c..dcf6ac6d8ad 100644 --- a/crates/matrix-sdk-sqlite/CHANGELOG.md +++ b/crates/matrix-sdk-sqlite/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + +### Features + +- Add support for persisting LinkedChunks in the SQLite store. This is a step + towards implementing event cache support, enabling a persisted cache of + events. + ([#4340](https://github.com/matrix-org/matrix-rust-sdk/pull/4340)) ([#4362](https://github.com/matrix-org/matrix-rust-sdk/pull/4362)) + ## [0.8.0] - 2024-11-19 ### Bug Fixes diff --git a/crates/matrix-sdk-store-encryption/CHANGELOG.md b/crates/matrix-sdk-store-encryption/CHANGELOG.md index dff6a57ebbf..9235a4b9ff2 100644 --- a/crates/matrix-sdk-store-encryption/CHANGELOG.md +++ b/crates/matrix-sdk-store-encryption/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + ## [0.8.0] - 2024-11-19 No notable changes in this release. diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index e069adf9f10..0b0d578ba9a 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + +### Bug Fixes + +- Add the `m.room.create` and the `m.room.history_visibility` state events to + the required state for the sync. These two state events are required to + properly compute the room preview of a joined room. + ([#4325](https://github.com/matrix-org/matrix-rust-sdk/pull/4325)) + +### Features + +- Introduce a new variant to the `UtdCause` enum tailored for device-historical + messages. These messages cannot be decrypted unless the client regains access + to message history through key storage (e.g., room key backups). + ([#4375](https://github.com/matrix-org/matrix-rust-sdk/pull/4375)) + ## [0.8.0] - 2024-11-19 ### Bug Fixes diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 152518be3aa..d2922bc3d82 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. + + +## [Unreleased] - ReleaseDate + +### Bug Fixes + +- Use the inviter's server name and the server name from the room alias as + fallback values for the via parameter when requesting the room summary from + the homeserver. This ensures requests succeed even when the room being + previewed is hosted on a federated server. + ([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357)) + +- Do not use the encrypted original file's content type as the encrypted + thumbnail's content type. + ([#ecf4434](https://github.com/matrix-org/matrix-rust-sdk/commit/ecf44348cf6a872b843fb7d7af1a88f724c58c3e)) +### Features + +- Enable persistent storage for the `EventCache`. This allows events received + through the `/sync` endpoint or backpagination to be stored persistently, + enabling client applications to restore a room's view, including events, + without requiring server communication. + ([#4347](https://github.com/matrix-org/matrix-rust-sdk/pull/4347)) + +- [**breaking**] Make all fields of Thumbnail required + ([#4324](https://github.com/matrix-org/matrix-rust-sdk/pull/4324)) + ## [0.8.0] - 2024-11-19 ### Bug Fixes From 17812b69490fd19fece62fe281769910f326c474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 3 Dec 2024 17:41:02 +0100 Subject: [PATCH 683/979] chore: Remove our cliff config and don't use cliff to generate changelogs --- cliff.toml | 91 -------------------------------------------- xtask/src/release.rs | 58 +--------------------------- 2 files changed, 1 insertion(+), 148 deletions(-) delete mode 100644 cliff.toml diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index a3589a536c4..00000000000 --- a/cliff.toml +++ /dev/null @@ -1,91 +0,0 @@ -# This git-cliff configuration file is used to generate release reports. - -[changelog] -# changelog header -header = """ -# Changelog\n -All notable changes to this project will be documented in this file.\n -""" -# template for the changelog body -# https://keats.github.io/tera/docs/ -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ - ## [unreleased] -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - {% set_global commit_message = commit.message -%} - {% set_global breaking = commit.breaking -%} - {% for footer in commit.footers -%} - {% if footer.token | lower == "changelog" -%} - {% set_global commit_message = footer.value -%} - {% elif footer.token | lower == "breaking-change" -%} - {% set_global commit_message = footer.value -%} - {% elif footer.token | lower == "security-impact" -%} - {% set_global security_impact = footer.value -%} - {% elif footer.token | lower == "cve" -%} - {% set_global cve = footer.value -%} - {% elif footer.token | lower == "github-advisory" -%} - {% set_global github_advisory = footer.value -%} - {% endif -%} - {% endfor -%} - - {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }} - {% if security_impact -%} - (\ - *{{ security_impact | upper_first }}*\ - {% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\ - {% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%} - ) - {% endif -%} - {% endfor %} -{% endfor %}\n -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = """ - -""" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = true -# regex for preprocessing the commit messages -commit_preprocessors = [ - { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"}, -] -# regex for parsing and grouping commits -commit_parsers = [ - { footer = "Security-Impact:", group = "Security" }, - { footer = "CVE:", group = "Security" }, - { footer = "GitHub-Advisory:", group = "Security" }, - { message = "^feat", group = "Features" }, - { message = "^fix", group = "Bug Fixes" }, - { message = "^doc", group = "Documentation" }, - { message = "^perf", group = "Performance" }, - { message = "^refactor", group = "Refactor" }, - { message = "^chore\\(release\\): prepare for", skip = true }, - { message = "^chore", skip = true }, - { message = "^style", group = "Styling", skip = true }, - { message = "^test", skip = true }, - { message = "^ci", skip = true }, -] -# forbid parsers from skipping breaking changes -protect_breaking_commits = true -# filter out the commits that are not matched by commit parsers -filter_commits = true -# glob pattern for matching git tags -tag_pattern = "[0-9]*" -# regex for skipping tags -skip_tags = "" -# regex for ignoring tags -ignore_tags = "" -# sort the tags chronologically -date_order = false -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" diff --git a/xtask/src/release.rs b/xtask/src/release.rs index ff75c0a1c1c..e54726c7897 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -1,9 +1,7 @@ -use std::env; - use clap::{Args, Subcommand, ValueEnum}; use xshell::cmd; -use crate::{sh, workspace, Result}; +use crate::{sh, Result}; #[derive(Args)] pub struct ReleaseArgs { @@ -36,10 +34,6 @@ enum ReleaseCommand { }, /// Get a list of interesting changes that happened in the last week. WeeklyReport, - /// Generate the changelog for a specific crate, this shouldn't be run - /// manually, cargo-release will call this. - #[clap(hide = true)] - Changelog, } #[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)] @@ -68,22 +62,10 @@ impl ReleaseArgs { pub fn run(self) -> Result<()> { check_prerequisites(); - // The changelog needs to be generated from the directory of the crate, - // `cargo-release` changes the directory for us but we need to - // make sure to not switch back to the workspace dir. - // - // More info: https://git-cliff.org/docs/usage/monorepos - let sh = sh(); - let _p; - if self.cmd != ReleaseCommand::Changelog { - _p = sh.push_dir(workspace::root_path()?); - } - match self.cmd { ReleaseCommand::Prepare { version, execute } => prepare(version, execute), ReleaseCommand::Publish { execute } => publish(execute), ReleaseCommand::WeeklyReport => weekly_report(), - ReleaseCommand::Changelog => changelog(), } } } @@ -167,41 +149,3 @@ fn weekly_report() -> Result<()> { Ok(()) } - -/// Generate the changelog for a given crate. -/// -/// This will be called by `cargo-release` and it will set the correct -/// environment and call it from within the correct directory. -fn changelog() -> Result<()> { - let dry_run = env::var("DRY_RUN").map(|dry| str::parse::(&dry)).unwrap_or(Ok(true))?; - let crate_name = env::var("CRATE_NAME").expect("CRATE_NAME must be set"); - let new_version = env::var("NEW_VERSION").expect("NEW_VERSION must be set"); - - if dry_run { - println!( - "\nGenerating a changelog for {} (dry run), the following output will be prepended to the CHANGELOG.md file:\n", - crate_name - ); - } else { - println!("Generating a changelog for {}.", crate_name); - } - - let sh = sh(); - let command = cmd!(sh, "git cliff") - .arg("cliff") - .arg("--config") - .arg("../../cliff.toml") - .arg("--include-path") - .arg(format!("crates/{}/**/*", crate_name)) - .arg("--repository") - .arg("../../") - .arg("--unreleased") - .arg("--tag") - .arg(&new_version); - - let command = if dry_run { command } else { command.arg("--prepend").arg("CHANGELOG.md") }; - - command.run()?; - - Ok(()) -} From b8bf847fc135c8c9a03042661d62e04cf974f4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 3 Dec 2024 17:41:26 +0100 Subject: [PATCH 684/979] chore: Set up cargo-release to update the versions in the changelog --- release.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/release.toml b/release.toml index 9de2b7e4cfc..1e24f872c59 100644 --- a/release.toml +++ b/release.toml @@ -1,8 +1,11 @@ owners = ["poljar", "github:matrix-org:rust"] pre-release-commit-message = "chore: Release matrix-sdk version {{version}}" -pre-release-replacements = [] -pre-release-hook = ["cargo", "xtask", "release", "changelog"] +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}"}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"}, + {file="CHANGELOG.md", search="", replace="\n\n## [Unreleased] - ReleaseDate", exactly=1}, +] sign-tag = true tag-message = "Release {{crate_name}} version {{version}}" From 48bb3dbbe7aa49f6930fe00151166ed0cf2d26c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 3 Dec 2024 18:46:11 +0100 Subject: [PATCH 685/979] chore: Update our contributing guide for the manual changelog entries --- CONTRIBUTING.md | 81 +++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e00b7eafad..82a802cffe0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,9 +45,46 @@ that is, just the branch name.) # Writing changelog entries -We aim to maintain clear and informative changelogs that accurately reflect the -changes in our project. This guide will help you write useful changelog entries -using git-cliff, which fetches changelog entries from commit messages. +Our goal is to maintain clear, concise, and informative changelogs that +accurately document changes in the project. Changelog entries should be written +manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file. + +Be sure to include a link to the pull request for additional context. A +well-written changelog entry should be understandable even to those who may not +be deeply familiar with the project. Provide enough context to ensure clarity +and ease of understanding. + +A couple of examples of bad changelog entry would look like: + +```markdown +- Fixed a panic. +``` + +```markdown +- Added the Bar function to Foo. +``` + +A good example of a changelog entry could look like the following: + +```markdown +- Use the inviter's server name and the server name from the room alias as + fallback values for the via parameter when requesting the room summary from + the homeserver. This ensures requests succeed even when the room being + previewed is hosted on a federated server. + ([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357)) +``` + +For security-related changelog entries, please include the following additional +details alongside the pull request number: + +* Impact: Clearly describe the issue's potential impact on users or systems. +* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier. +* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context. + +```markdown +- Use a constant-time Base64 encoder for secret key material to mitigate + side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)). +``` ## Commit message format @@ -74,45 +111,20 @@ The type of changes which will be included in changelogs is one of the following The scope is optional and can specify the area of the codebase affected (e.g., olm, cipher). -### Changelog trailer - -In addition to the Conventional Commit format, you can use the `Changelog` git -trailer to specify the changelog message explicitly. When that trailer is -present, its value will be used as the changelog entry instead of the commit's -leading line. The `Breaking-Change` git trailer can be used in a similar manner -if the changelog entry should be marked as a breaking change. - - -#### Example commit message - -``` -feat: Add a method to encode Ed25519 public keys to Base64 - -This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to -stringify Ed25519 and thus present them to users. It's also commonly used when -Ed25519 keys need to be inserted into JSON. - -Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to - stringify the Ed25519 public key. -``` - -In this commit message, the content specified in the `Changelog` trailer will be -used for the changelog entry. - -Be careful to add at least one whitespace after new lines to create a paragraph. - ### Security fixes Commits addressing security vulnerabilities must include specific trailers for -vulnerability metadata. These commits are required to include at least the -`Security-Impact` trailer to indicate that the commit is a security fix. +vulnerability metadata, which should also be reflected in the corresponding +changelog entry. -Security issues have some additional git-trailers: +The metadata must be included in the following git-trailers: * `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical. * `CVE`: The CVE that was assigned to this issue. * `GitHub-Advisory`: The GitHub advisory identifier. +Please include all of the fields that are available. + Example: ``` @@ -131,9 +143,6 @@ material. Security-Impact: Low CVE: CVE-2024-40640 GitHub-Advisory: GHSA-j8cm-g7r6-hfpq - -Changelog: Use a constant-time Base64 encoder for secret key material -to mitigate side-channel attacks leaking secret key material. ``` ## Review process From 9bdd9fa831cace40f0c0a4c96bac8233bf9b2499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 3 Dec 2024 19:03:06 +0100 Subject: [PATCH 686/979] chore: Update the RELEASING file so it doesn't mention git-cliff anymore --- RELEASING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index fadd5b76aae..090b0feb4ea 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -17,8 +17,8 @@ The procedure is as follows: git switch -c release-x.y.z   ``` -2. Prepare the release. This will update the `README.md`, prepend the `CHANGELOG.md` - file using `git cliff`, and bump the version in the `Cargo.toml` file. +2. Prepare the release. This will update the `README.md`, set the versions in + the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file. ```bash cargo xtask release prepare --execute minor|patch|rc From de5511f009775e4b00b4d90ad7724a3d41a9ecb1 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 13:30:29 +0100 Subject: [PATCH 687/979] test: rewrite `test_ignored_unignored` so it makes use of the `MatrixMockServer` --- .../tests/integration/event_cache.rs | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 17e50d04f37..ffe35c0de06 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -6,7 +6,7 @@ use matrix_sdk::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, - test_utils::{assert_event_matches_msg, logged_in_client_with_server}, + test_utils::{assert_event_matches_msg, logged_in_client_with_server, mocks::MatrixMockServer}, }; use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, @@ -145,7 +145,8 @@ async fn test_add_initial_events() { #[async_test] async fn test_ignored_unignored() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; // Immediately subscribe the event cache to sync updates. client.event_cache().subscribe().unwrap(); @@ -154,18 +155,12 @@ async fn test_ignored_unignored() { let room_id = room_id!("!omelette:fromage.fr"); let other_room_id = room_id!("!galette:saucisse.bzh"); - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder - .add_joined_room(JoinedRoomBuilder::new(room_id)) - .add_joined_room(JoinedRoomBuilder::new(other_room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; + server.sync_joined_room(&client, room_id).await; + server.sync_joined_room(&client, other_room_id).await; let dexter = user_id!("@dexter:lab.org"); let ivan = user_id!("@ivan:lab.ch"); - let ev_factory = EventFactory::new(); + let f = EventFactory::new(); // If I add initial events to a few rooms, client @@ -173,8 +168,8 @@ async fn test_ignored_unignored() { .add_initial_events( room_id, vec![ - ev_factory.text_msg("hey there").sender(dexter).into_sync(), - ev_factory.text_msg("hoy!").sender(ivan).into_sync(), + f.text_msg("hey there").sender(dexter).into_sync(), + f.text_msg("hoy!").sender(ivan).into_sync(), ], None, ) @@ -185,7 +180,7 @@ async fn test_ignored_unignored() { .event_cache() .add_initial_events( other_room_id, - vec![ev_factory.text_msg("demat!").sender(ivan).into_sync()], + vec![f.text_msg("demat!").sender(ivan).into_sync()], None, ) .await @@ -202,17 +197,19 @@ async fn test_ignored_unignored() { assert_event_matches_msg(&events[1], "hoy!"); // And after receiving a new ignored list, - sync_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Custom(json!({ - "content": { - "ignored_users": { - dexter: {} - } - }, - "type": "m.ignored_user_list", - }))); - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; + server + .mock_sync() + .ok_and_run(&client, |sync_builder| { + sync_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Custom(json!({ + "content": { + "ignored_users": { + dexter: {} + } + }, + "type": "m.ignored_user_list", + }))); + }) + .await; // It does receive one update, let update = timeout(Duration::from_secs(2), subscriber.recv()) @@ -224,13 +221,15 @@ async fn test_ignored_unignored() { assert_matches!(update, RoomEventCacheUpdate::Clear); // Receiving new events still works. - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id) - .add_timeline_event(ev_factory.text_msg("i don't like this dexter").sender(ivan)), - ); - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; + server + .mock_sync() + .ok_and_run(&client, |sync_builder| { + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("i don't like this dexter").sender(ivan)), + ); + }) + .await; // We do receive one update, let update = timeout(Duration::from_secs(2), subscriber.recv()) From 5cde4a6630dce90dcea57184ce543dada18de700 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 13:36:36 +0100 Subject: [PATCH 688/979] test: remove use of `add_initial_events` in `test_ignored_unignored` --- .../tests/integration/event_cache.rs | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index ffe35c0de06..c9c4f740d9b 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1,13 +1,16 @@ -use std::{future::ready, ops::ControlFlow, time::Duration}; +use std::{future::ready, ops::ControlFlow, sync::Arc, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ + config::StoreConfig, event_cache::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, + linked_chunk::{ChunkIdentifier, Position, Update}, test_utils::{assert_event_matches_msg, logged_in_client_with_server, mocks::MatrixMockServer}, }; +use matrix_sdk_base::event_cache::store::{EventCacheStore, MemoryStore}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, @@ -146,46 +149,63 @@ async fn test_add_initial_events() { #[async_test] async fn test_ignored_unignored() { let server = MatrixMockServer::new().await; - let client = server.client_builder().build().await; + + let event_cache_store = Arc::new(MemoryStore::new()); + let client = server + .client_builder() + .store_config( + StoreConfig::new("hodor".to_owned()).event_cache_store(event_cache_store.clone()), + ) + .build() + .await; // Immediately subscribe the event cache to sync updates. client.event_cache().subscribe().unwrap(); + client.event_cache().enable_storage().unwrap(); - // If I sync and get informed I've joined The Room, but with no events, let room_id = room_id!("!omelette:fromage.fr"); let other_room_id = room_id!("!galette:saucisse.bzh"); - server.sync_joined_room(&client, room_id).await; - server.sync_joined_room(&client, other_room_id).await; - let dexter = user_id!("@dexter:lab.org"); let ivan = user_id!("@ivan:lab.ch"); let f = EventFactory::new(); - // If I add initial events to a few rooms, - client - .event_cache() - .add_initial_events( + // Given two rooms which add initial items, + event_cache_store + .handle_linked_chunk_updates( room_id, vec![ - f.text_msg("hey there").sender(dexter).into_sync(), - f.text_msg("hoy!").sender(ivan).into_sync(), + Update::NewItemsChunk { previous: None, new: ChunkIdentifier::new(0), next: None }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(0), 0), + items: vec![ + f.text_msg("hey there").sender(dexter).into_sync(), + f.text_msg("hoy!").sender(ivan).into_sync(), + ], + }, ], - None, ) .await .unwrap(); - client - .event_cache() - .add_initial_events( + event_cache_store + .handle_linked_chunk_updates( other_room_id, - vec![f.text_msg("demat!").sender(ivan).into_sync()], - None, + vec![ + Update::NewItemsChunk { previous: None, new: ChunkIdentifier::new(0), next: None }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(0), 0), + items: vec![f.text_msg("demat!").sender(ivan).into_sync()], + }, + ], ) .await .unwrap(); + // If I get informed about these two rooms during sync, + server.sync_joined_room(&client, room_id).await; + server.sync_joined_room(&client, other_room_id).await; + // And subscribe to the room, let room = client.get_room(room_id).unwrap(); let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); From bd93a9a40e250f8942651c748d233b1a2692f70a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 13:39:31 +0100 Subject: [PATCH 689/979] test: remvoe use of `add_initial_events` in `test_add_initial_events` And make it a smoke test that the event cache correctly gets events it retrieves from sync. --- .../tests/integration/event_cache.rs | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index c9c4f740d9b..861b1249375 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -60,7 +60,7 @@ async fn test_must_explicitly_subscribe() { } #[async_test] -async fn test_add_initial_events() { +async fn test_event_cache_receives_events() { let (client, server) = logged_in_client_with_server().await; // Immediately subscribe the event cache to sync updates. @@ -116,32 +116,6 @@ async fn test_add_initial_events() { assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "bonjour monde"); - // And when I later add initial events to this room, - - // XXX: when we get rid of `add_initial_events`, we can keep this test as a - // smoke test for the event cache. - client - .event_cache() - .add_initial_events(room_id, vec![ev_factory.text_msg("new choice!").into_sync()], None) - .await - .unwrap(); - - // Then I receive an update that the room has been cleared, - let update = timeout(Duration::from_secs(2), subscriber.recv()) - .await - .expect("timeout after receiving a sync update") - .expect("should've received a room event cache update"); - assert_let!(RoomEventCacheUpdate::Clear = update); - - // Before receiving the "initial" event. - let update = timeout(Duration::from_secs(2), subscriber.recv()) - .await - .expect("timeout after receiving a sync update") - .expect("should've received a room event cache update"); - assert_let!(RoomEventCacheUpdate::AddTimelineEvents { events, .. } = update); - assert_eq!(events.len(), 1); - assert_event_matches_msg(&events[0], "new choice!"); - // That's all, folks! assert!(subscriber.is_empty()); } From 5d953879351cefe4a5bf9b8ffd664350d0149f30 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 13:45:06 +0100 Subject: [PATCH 690/979] test: remove unused adding of initial events in sliding sync test helper --- .../tests/integration/timeline/sliding_sync.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs index 96e9c85efc1..3d11c4c4a1a 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs @@ -279,16 +279,6 @@ async fn timeline_test_helper( anyhow::anyhow!("Room {room_id} not found in client. Can't provide a timeline for it") })?; - // TODO: when the event cache handles its own cache, we can remove this. - client - .event_cache() - .add_initial_events( - room_id, - sliding_sync_room.timeline_queue().iter().cloned().collect(), - sliding_sync_room.prev_batch(), - ) - .await?; - let timeline = Timeline::builder(&sdk_room).track_read_marker_and_receipts().build().await?; Ok(timeline.subscribe().await) From 8f1722f2a8868598ad78e070dd8303cdbbe96350 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 13:56:42 +0100 Subject: [PATCH 691/979] test: have the `PendingEdit` test helper use the matrix mock server and event cache storage --- .../tests/integration/timeline/edit.rs | 89 +++++++++++-------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index ddafabe5cdb..4fb235c413e 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use as_variant::as_variant; use assert_matches::assert_matches; @@ -20,9 +20,16 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ - config::SyncSettings, room::edit::EditedContent, test_utils::logged_in_client_with_server, + config::{StoreConfig, SyncSettings}, + linked_chunk::{ChunkIdentifier, Update}, + room::edit::EditedContent, + test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, Client, }; +use matrix_sdk_base::event_cache::{ + store::{EventCacheStore, MemoryStore}, + Gap, +}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, @@ -46,7 +53,7 @@ use ruma::{ MessageType, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, TextMessageEventContent, }, - AnyMessageLikeEventContent, AnyTimelineEvent, + AnyMessageLikeEventContent, AnyStateEvent, AnyTimelineEvent, }, owned_event_id, room_id, serde::Raw, @@ -57,7 +64,7 @@ use stream_assert::assert_next_matches; use tokio::{task::yield_now, time::sleep}; use wiremock::{ matchers::{header, method, path_regex}, - Mock, MockServer, ResponseTemplate, + Mock, ResponseTemplate, }; use crate::mock_sync; @@ -851,69 +858,77 @@ async fn test_edit_local_echo_with_unsupported_content() { struct PendingEditHelper { client: Client, - server: MockServer, + server: MatrixMockServer, timeline: Timeline, - sync_builder: SyncResponseBuilder, - sync_settings: SyncSettings, room_id: OwnedRoomId, } impl PendingEditHelper { async fn new() -> Self { let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + let event_cache_store = Arc::new(MemoryStore::new()); + let server = MatrixMockServer::new().await; - mock_encryption_state(&server, false).await; + let client = server + .client_builder() + .store_config( + StoreConfig::new("hodor".to_owned()).event_cache_store(event_cache_store.clone()), + ) + .build() + .await; { // Fill the initial prev-batch token to avoid waiting for it later. let ec = client.event_cache(); ec.subscribe().unwrap(); - ec.add_initial_events(room_id, vec![], Some("prev-batch-token".to_owned())) + ec.enable_storage().unwrap(); + + event_cache_store + .handle_linked_chunk_updates( + room_id, + vec![ + // Maintain the invariant that the first chunk is an items. + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + // Mind the gap! + Update::NewGapChunk { + previous: Some(ChunkIdentifier::new(0)), + new: ChunkIdentifier::new(1), + next: None, + gap: Gap { prev_token: "prev-batch-token".to_owned() }, + }, + ], + ) .await .unwrap(); } + server.sync_joined_room(&client, room_id).await; + server.mock_room_state_encryption().plain().mount().await; + let room = client.get_room(room_id).unwrap(); let timeline = room.timeline().await.unwrap(); - Self { client, server, timeline, sync_builder, sync_settings, room_id: room_id.to_owned() } + Self { client, server, timeline, room_id: room_id.to_owned() } } async fn handle_sync(&mut self, joined_room_builder: JoinedRoomBuilder) { - self.sync_builder.add_joined_room(joined_room_builder); - - mock_sync(&self.server, self.sync_builder.build_json_sync_response(), None).await; - let _response = self.client.sync_once(self.sync_settings.clone()).await.unwrap(); - - self.server.reset().await; + self.server.sync_room(&self.client, joined_room_builder).await; } async fn handle_backpagination(&mut self, events: Vec>, batch_size: u16) { - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "start": "123", - "end": "yolo", - "chunk": events, - "state": [] - }))) - .expect(1) - .mount(&self.server) + self.server + .mock_room_messages() + .ok("123".to_owned(), Some("yolo".to_owned()), events, Vec::>::new()) + .mock_once() + .mount() .await; self.timeline.live_paginate_backwards(batch_size).await.unwrap(); - - self.server.reset().await; } } From 5bf3b11edfd812ec31b20d6cd31d6e6a8c9c9ff4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 14:06:15 +0100 Subject: [PATCH 692/979] test: rewrite `test_send_edit_when_timeline_is_clear` to not use `add_initial_items` Moar MatrixMockServer \o/ --- .../tests/integration/timeline/edit.rs | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 4fb235c413e..1d177c5e70f 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -675,20 +675,14 @@ async fn test_send_edit_poll() { #[async_test] async fn test_send_edit_when_timeline_is_clear() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = server.sync_joined_room(&client, room_id).await; - mock_encryption_state(&server, false).await; + server.mock_room_state_encryption().plain().mount().await; - let room = client.get_room(room_id).unwrap(); let timeline = room.timeline().await.unwrap(); let (_, mut timeline_stream) = timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; @@ -699,13 +693,13 @@ async fn test_send_edit_when_timeline_is_clear() { .sender(client.user_id().unwrap()) .event_id(event_id!("$original_event")) .into_raw_sync(); - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event(raw_original_event.clone()), - ); - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event(raw_original_event.clone()), + ) + .await; let hello_world_item = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); @@ -713,9 +707,13 @@ async fn test_send_edit_when_timeline_is_clear() { assert!(!hello_world_message.is_edited()); assert!(hello_world_item.is_editable()); - // Clear the event cache (hence the timeline) to make sure the old item does not - // need to be available in it for the edit to work. - client.event_cache().add_initial_events(room_id, vec![], None).await.unwrap(); + // Receive a limited (gappy) sync for this room, which will clear the timeline… + // + // TODO: …until the event cache storage is enabled by default, a time where + // we'll be able to get rid of this test entirely (or update its + // expectations). + + server.sync_room(&client, JoinedRoomBuilder::new(room_id).set_timeline_limited()).await; client.event_cache().empty_immutable_cache().await; yield_now().await; @@ -741,8 +739,7 @@ async fn test_send_edit_when_timeline_is_clear() { // updates, so just wait for a bit before verifying that the endpoint was // called. sleep(Duration::from_millis(200)).await; - - server.verify().await; + assert!(timeline_stream.next().now_or_never().is_none()); } #[async_test] From 1a63d8f0b783e61c4237665b8fdb9e5f1a660049 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 14:22:38 +0100 Subject: [PATCH 693/979] task(event cache): ignore `add_initial_events()` when the event cache storage has been enabled Not worth testing IMO, since this is about the "temporary" API we're going to remove in subsequent patches. --- crates/matrix-sdk/src/event_cache/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index cc0ac6d14f4..dfe2df5a671 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -312,6 +312,11 @@ impl EventCache { events: Vec, prev_batch: Option, ) -> Result<()> { + // If the event cache's storage has been enabled, do nothing. + if self.inner.store.get().is_some() { + return Ok(()); + } + let room_cache = self.inner.for_room(room_id).await?; // We could have received events during a previous sync; remove them all, since From e0b1b5dc050e69f54b158792416ede671156c938 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Dec 2024 11:24:09 +0100 Subject: [PATCH 694/979] test: don't use the event cache storage but regular syncs instead --- .../tests/integration/timeline/edit.rs | 54 ++++------------ .../tests/integration/event_cache.rs | 63 +++++-------------- 2 files changed, 28 insertions(+), 89 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 1d177c5e70f..4ad9e834238 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use as_variant::as_variant; use assert_matches::assert_matches; @@ -20,16 +20,11 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ - config::{StoreConfig, SyncSettings}, - linked_chunk::{ChunkIdentifier, Update}, + config::SyncSettings, room::edit::EditedContent, test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, Client, }; -use matrix_sdk_base::event_cache::{ - store::{EventCacheStore, MemoryStore}, - Gap, -}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, @@ -864,47 +859,20 @@ impl PendingEditHelper { async fn new() -> Self { let room_id = room_id!("!a98sd12bjh:example.org"); - let event_cache_store = Arc::new(MemoryStore::new()); let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + client.event_cache().subscribe().unwrap(); - let client = server - .client_builder() - .store_config( - StoreConfig::new("hodor".to_owned()).event_cache_store(event_cache_store.clone()), + // Fill the initial prev-batch token to avoid waiting for it later. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .set_timeline_prev_batch("prev-batch-token".to_owned()), ) - .build() .await; - { - // Fill the initial prev-batch token to avoid waiting for it later. - let ec = client.event_cache(); - ec.subscribe().unwrap(); - ec.enable_storage().unwrap(); - - event_cache_store - .handle_linked_chunk_updates( - room_id, - vec![ - // Maintain the invariant that the first chunk is an items. - Update::NewItemsChunk { - previous: None, - new: ChunkIdentifier::new(0), - next: None, - }, - // Mind the gap! - Update::NewGapChunk { - previous: Some(ChunkIdentifier::new(0)), - new: ChunkIdentifier::new(1), - next: None, - gap: Gap { prev_token: "prev-batch-token".to_owned() }, - }, - ], - ) - .await - .unwrap(); - } - - server.sync_joined_room(&client, room_id).await; server.mock_room_state_encryption().plain().mount().await; let room = client.get_room(room_id).unwrap(); diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 861b1249375..8dced657e0e 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1,16 +1,13 @@ -use std::{future::ready, ops::ControlFlow, sync::Arc, time::Duration}; +use std::{future::ready, ops::ControlFlow, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ - config::StoreConfig, event_cache::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, - linked_chunk::{ChunkIdentifier, Position, Update}, test_utils::{assert_event_matches_msg, logged_in_client_with_server, mocks::MatrixMockServer}, }; -use matrix_sdk_base::event_cache::store::{EventCacheStore, MemoryStore}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, @@ -123,19 +120,10 @@ async fn test_event_cache_receives_events() { #[async_test] async fn test_ignored_unignored() { let server = MatrixMockServer::new().await; - - let event_cache_store = Arc::new(MemoryStore::new()); - let client = server - .client_builder() - .store_config( - StoreConfig::new("hodor".to_owned()).event_cache_store(event_cache_store.clone()), - ) - .build() - .await; + let client = server.client_builder().build().await; // Immediately subscribe the event cache to sync updates. client.event_cache().subscribe().unwrap(); - client.event_cache().enable_storage().unwrap(); let room_id = room_id!("!omelette:fromage.fr"); let other_room_id = room_id!("!galette:saucisse.bzh"); @@ -144,41 +132,24 @@ async fn test_ignored_unignored() { let ivan = user_id!("@ivan:lab.ch"); let f = EventFactory::new(); - // Given two rooms which add initial items, - event_cache_store - .handle_linked_chunk_updates( - room_id, - vec![ - Update::NewItemsChunk { previous: None, new: ChunkIdentifier::new(0), next: None }, - Update::PushItems { - at: Position::new(ChunkIdentifier::new(0), 0), - items: vec![ - f.text_msg("hey there").sender(dexter).into_sync(), - f.text_msg("hoy!").sender(ivan).into_sync(), - ], - }, - ], + // Given two known rooms with initial items, + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_bulk(vec![ + f.text_msg("hey there").sender(dexter).into_raw_sync(), + f.text_msg("hoy!").sender(ivan).into_raw_sync(), + ]), ) - .await - .unwrap(); + .await; - event_cache_store - .handle_linked_chunk_updates( - other_room_id, - vec![ - Update::NewItemsChunk { previous: None, new: ChunkIdentifier::new(0), next: None }, - Update::PushItems { - at: Position::new(ChunkIdentifier::new(0), 0), - items: vec![f.text_msg("demat!").sender(ivan).into_sync()], - }, - ], + server + .sync_room( + &client, + JoinedRoomBuilder::new(other_room_id) + .add_timeline_bulk(vec![f.text_msg("demat!").sender(ivan).into_raw_sync()]), ) - .await - .unwrap(); - - // If I get informed about these two rooms during sync, - server.sync_joined_room(&client, room_id).await; - server.sync_joined_room(&client, other_room_id).await; + .await; // And subscribe to the room, let room = client.get_room(room_id).unwrap(); From a4434d79c90f83274da4acd9cd681bda2b54773e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 15:30:12 +0100 Subject: [PATCH 695/979] feat(event cache): strip bundled relations before persisting events --- crates/matrix-sdk/src/event_cache/room/mod.rs | 148 +++++++++++++++++- 1 file changed, 145 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 8e6b6c4a942..55938362248 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -539,9 +539,13 @@ impl RoomEventCacheInner { mod private { use std::sync::Arc; - use matrix_sdk_base::event_cache::store::EventCacheStoreLock; + use matrix_sdk_base::{ + deserialized_responses::{SyncTimelineEvent, TimelineEventKind}, + event_cache::store::EventCacheStoreLock, + linked_chunk::Update, + }; use once_cell::sync::OnceCell; - use ruma::OwnedRoomId; + use ruma::{serde::Raw, OwnedRoomId}; use super::events::RoomEvents; use crate::event_cache::EventCacheError; @@ -587,13 +591,72 @@ mod private { Ok(Self { room, store, events, waited_for_initial_prev_token: false }) } + /// Removes the bundled relations from an event, if they were present. + /// + /// Only replaces the present if it contained bundled relations. + fn strip_relations_if_present(event: &mut Raw) { + // We're going to get rid of the `unsigned`/`m.relations` field, if it's + // present. + // Use a closure that returns an option so we can quickly short-circuit. + let mut closure = || -> Option<()> { + let mut val: serde_json::Value = event.deserialize_as().ok()?; + let unsigned = val.get_mut("unsigned")?; + let unsigned_obj = unsigned.as_object_mut()?; + if unsigned_obj.remove("m.relations").is_some() { + *event = Raw::new(&val).ok()?.cast(); + } + None + }; + let _ = closure(); + } + + /// Strips the bundled relations from a collection of events. + fn strip_relations_from_events(items: &mut [SyncTimelineEvent]) { + for ev in items.iter_mut() { + match &mut ev.kind { + TimelineEventKind::Decrypted(decrypted) => { + // Remove all information about encryption info for + // the bundled events. + decrypted.unsigned_encryption_info = None; + + // Remove the `unsigned`/`m.relations` field, if needs be. + Self::strip_relations_if_present(&mut decrypted.event); + } + + TimelineEventKind::UnableToDecrypt { event, .. } + | TimelineEventKind::PlainText { event } => { + Self::strip_relations_if_present(event); + } + } + } + } + /// Propagate changes to the underlying storage. async fn propagate_changes(&mut self) -> Result<(), EventCacheError> { - let updates = self.events.updates().take(); + let mut updates = self.events.updates().take(); if !updates.is_empty() { if let Some(store) = self.store.get() { let locked = store.lock().await?; + + // Strip relations from the `PushItems` updates. + for up in updates.iter_mut() { + match up { + Update::PushItems { items, .. } => { + Self::strip_relations_from_events(items) + } + // Other update kinds don't involve adding new events. + Update::NewItemsChunk { .. } + | Update::NewGapChunk { .. } + | Update::RemoveChunk(_) + | Update::RemoveItem { .. } + | Update::DetachLastItems { .. } + | Update::StartReattachItems + | Update::EndReattachItems + | Update::Clear => {} + } + } + locked.handle_linked_chunk_updates(&self.room, updates).await?; } } @@ -959,6 +1022,85 @@ mod tests { assert!(chunks.next().is_none()); } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. + #[async_test] + async fn test_write_to_storage_strips_bundled_relations() { + use ruma::events::BundledMessageLikeRelations; + + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + let event_cache_store = Arc::new(MemoryStore::new()); + + let client = MockClientBuilder::new("http://localhost".to_owned()) + .store_config( + StoreConfig::new("hodlor".to_owned()).event_cache_store(event_cache_store.clone()), + ) + .build() + .await; + + let event_cache = client.event_cache(); + + // Don't forget to subscribe and like^W enable storage! + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + // Propagate an update for a message with bundled relations. + let mut relations = BundledMessageLikeRelations::new(); + relations.replace = + Some(Box::new(f.text_msg("Hello, Kind Sir").sender(*ALICE).into_raw_sync())); + let ev = f.text_msg("hey yo").sender(*ALICE).bundled_relations(relations).into_sync(); + + let timeline = Timeline { limited: false, prev_batch: None, events: vec![ev] }; + + room_event_cache + .inner + .handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() }) + .await + .unwrap(); + + // The in-memory linked chunk keeps the bundled relation. + { + let (events, _) = room_event_cache.subscribe().await.unwrap(); + + assert_eq!(events.len(), 1); + + let ev = events[0].raw().deserialize().unwrap(); + assert_let!( + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) = ev + ); + + let original = msg.as_original().unwrap(); + assert_eq!(original.content.body(), "hey yo"); + assert!(original.unsigned.relations.replace.is_some()); + } + + // The one in storage does not. + let linked_chunk = event_cache_store.reload_linked_chunk(room_id).await.unwrap().unwrap(); + + assert_eq!(linked_chunk.chunks().count(), 1); + + let mut chunks = linked_chunk.chunks(); + assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => { + assert_eq!(events.len(), 1); + + let ev = events[0].raw().deserialize().unwrap(); + assert_let!(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) = ev); + + let original = msg.as_original().unwrap(); + assert_eq!(original.content.body(), "hey yo"); + assert!(original.unsigned.relations.replace.is_none()); + }); + + // That's all, folks! + assert!(chunks.next().is_none()); + } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. #[async_test] async fn test_load_from_storage() { From 68018112265c2c252e9ff89c0df5a2707512a3ee Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 4 Dec 2024 15:33:08 +0100 Subject: [PATCH 696/979] feat(crypto): Supports new `UtdCause` variants for withheld keys Adds new UtdCause variants for withheld keys, enabling applications to display customised messages when an Unable-To-Decrypt message is expected. refactor(crypto): Move WithheldCode from crypto to common crate --- Cargo.lock | 1 + bindings/matrix-sdk-crypto-ffi/src/error.rs | 2 +- .../matrix-sdk-base/src/sliding_sync/mod.rs | 2 +- .../src/deserialized_responses.rs | 223 +++++++++++++++++- crates/matrix-sdk-crypto/CHANGELOG.md | 5 + crates/matrix-sdk-crypto/src/error.rs | 7 +- .../src/identities/device.rs | 4 +- crates/matrix-sdk-crypto/src/machine/mod.rs | 4 +- .../src/machine/tests/mod.rs | 16 +- .../src/olm/group_sessions/outbound.rs | 3 +- .../src/session_manager/group_sessions/mod.rs | 13 +- .../group_sessions/share_strategy.rs | 4 +- .../src/store/integration_tests.rs | 9 +- .../src/types/events/room_key_withheld.rs | 75 +----- .../src/types/events/utd_cause.rs | 31 ++- crates/matrix-sdk-crypto/src/types/mod.rs | 15 +- crates/matrix-sdk-sqlite/Cargo.toml | 1 + .../src/timeline/tests/encryption.rs | 2 +- .../src/authentication/qrcode/messages.rs | 10 +- testing/matrix-sdk-test/src/event_factory.rs | 5 +- 20 files changed, 293 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be7d083fb1a..cdd479221ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3369,6 +3369,7 @@ dependencies = [ "glob", "itertools 0.13.0", "matrix-sdk-base", + "matrix-sdk-common", "matrix-sdk-crypto", "matrix-sdk-store-encryption", "matrix-sdk-test", diff --git a/bindings/matrix-sdk-crypto-ffi/src/error.rs b/bindings/matrix-sdk-crypto-ffi/src/error.rs index f2a0a2af47e..116244b68cf 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/error.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/error.rs @@ -112,7 +112,7 @@ mod tests { #[test] fn test_withheld_error_mapping() { - use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode; + use matrix_sdk_common::deserialized_responses::WithheldCode; let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified)); diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 19957e1aa08..79f0276f8c0 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -2656,7 +2656,7 @@ mod tests { .unwrap(), UnableToDecryptInfo { session_id: Some("".to_owned()), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }, ) } diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 2eed248ac36..a1b07c0267e 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -17,7 +17,10 @@ use std::{collections::BTreeMap, fmt}; use ruma::{ events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent}, push::Action, - serde::{JsonObject, Raw}, + serde::{ + AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw, + SerializeAsRefStr, + }, DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedUserId, }; use serde::{Deserialize, Serialize}; @@ -666,7 +669,7 @@ pub struct UnableToDecryptInfo { pub session_id: Option, /// Reason code for the decryption failure - #[serde(default = "unknown_utd_reason")] + #[serde(default = "unknown_utd_reason", deserialize_with = "deserialize_utd_reason")] pub reason: UnableToDecryptReason, } @@ -674,6 +677,24 @@ fn unknown_utd_reason() -> UnableToDecryptReason { UnableToDecryptReason::Unknown } +/// Provides basic backward compatibility for deserializing older serialized +/// `UnableToDecryptReason` values. +pub fn deserialize_utd_reason<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + // Start by deserializing as to an untyped JSON value. + let v: serde_json::Value = Deserialize::deserialize(d)?; + // Backwards compatibility: `MissingMegolmSession` used to be stored without the + // withheld code. + if v.as_str().is_some_and(|s| s == "MissingMegolmSession") { + return Ok(UnableToDecryptReason::MissingMegolmSession { withheld_code: None }); + } + // Otherwise, use the derived deserialize impl to turn the JSON into a + // UnableToDecryptReason + serde_json::from_value::(v).map_err(serde::de::Error::custom) +} + /// Reason code for a decryption failure #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum UnableToDecryptReason { @@ -689,9 +710,11 @@ pub enum UnableToDecryptReason { /// Decryption failed because we're missing the megolm session that was used /// to encrypt the event. - /// - /// TODO: support withheld codes? - MissingMegolmSession, + MissingMegolmSession { + /// If the key was withheld on purpose, the associated code. `None` + /// means no withheld code was received. + withheld_code: Option, + }, /// Decryption failed because, while we have the megolm session that was /// used to encrypt the message, it is ratcheted too far forward. @@ -723,7 +746,86 @@ impl UnableToDecryptReason { /// Returns true if this UTD is due to a missing room key (and hence might /// resolve itself if we wait a bit.) pub fn is_missing_room_key(&self) -> bool { - matches!(self, Self::MissingMegolmSession | Self::UnknownMegolmMessageIndex) + // In case of MissingMegolmSession with a withheld code we return false here + // given that this API is used to decide if waiting a bit will help. + matches!( + self, + Self::MissingMegolmSession { withheld_code: None } | Self::UnknownMegolmMessageIndex + ) + } +} + +/// A machine-readable code for why a Megolm key was not sent. +/// +/// Normally sent as the payload of an [`m.room_key.withheld`](https://spec.matrix.org/v1.12/client-server-api/#mroom_keywithheld) to-device message. +#[derive( + Clone, + PartialEq, + Eq, + Hash, + AsStrAsRefStr, + AsRefStr, + FromString, + DebugAsRefStr, + SerializeAsRefStr, + DeserializeFromCowStr, +)] +pub enum WithheldCode { + /// the user/device was blacklisted. + #[ruma_enum(rename = "m.blacklisted")] + Blacklisted, + + /// the user/devices is unverified. + #[ruma_enum(rename = "m.unverified")] + Unverified, + + /// The user/device is not allowed have the key. For example, this would + /// usually be sent in response to a key request if the user was not in + /// the room when the message was sent. + #[ruma_enum(rename = "m.unauthorised")] + Unauthorised, + + /// Sent in reply to a key request if the device that the key is requested + /// from does not have the requested key. + #[ruma_enum(rename = "m.unavailable")] + Unavailable, + + /// An olm session could not be established. + /// This may happen, for example, if the sender was unable to obtain a + /// one-time key from the recipient. + #[ruma_enum(rename = "m.no_olm")] + NoOlm, + + #[doc(hidden)] + _Custom(PrivOwnedStr), +} + +impl fmt::Display for WithheldCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let string = match self { + WithheldCode::Blacklisted => "The sender has blocked you.", + WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.", + WithheldCode::Unauthorised => "You are not authorised to read the message.", + WithheldCode::Unavailable => "The requested key was not found.", + WithheldCode::NoOlm => "Unable to establish a secure channel.", + _ => self.as_str(), + }; + + f.write_str(string) + } +} + +// The Ruma macro expects the type to have this name. +// The payload is counter intuitively made public in order to avoid having +// multiple copies of this struct. +#[doc(hidden)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PrivOwnedStr(pub Box); + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for PrivOwnedStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) } } @@ -817,7 +919,7 @@ mod tests { use super::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, - UnsignedEventLocation, VerificationState, + UnsignedEventLocation, VerificationState, WithheldCode, }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; @@ -1038,4 +1140,111 @@ mod tests { }); }); } + + #[test] + fn sync_timeline_event_deserialisation_migration_for_withheld() { + // Old serialized version was + // "utd_info": { + // "reason": "MissingMegolmSession", + // "session_id": "session000" + // } + + // The new version would be + // "utd_info": { + // "reason": { + // "MissingMegolmSession": { + // "withheld_code": null + // } + // }, + // "session_id": "session000" + // } + + let serialized = json!({ + "kind": { + "UnableToDecrypt": { + "event": { + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "AwgAEoABzL1JYhqhjW9jXrlT3M6H8mJ4qffYtOQOnPuAPNxsuG20oiD/Fnpv6jnQGhU6YbV9pNM+1mRnTvxW3CbWOPjLKqCWTJTc7Q0vDEVtYePg38ncXNcwMmfhgnNAoW9S7vNs8C003x3yUl6NeZ8bH+ci870BZL+kWM/lMl10tn6U7snNmSjnE3ckvRdO+11/R4//5VzFQpZdf4j036lNSls/WIiI67Fk9iFpinz9xdRVWJFVdrAiPFwb8L5xRZ8aX+e2JDMlc1eW8gk", + "device_id": "SKCGPNUWAU", + "sender_key": "Gim/c7uQdSXyrrUbmUOrBT6sMC0gO7QSLmOK6B7NOm0", + "session_id": "hgLyeSqXfb8vc5AjQLsg6TSHVu0HJ7HZ4B6jgMvxkrs" + }, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message" + }, + "utd_info": { + "reason": "MissingMegolmSession", + "session_id": "session000" + } + } + } + }); + + let result = serde_json::from_value(serialized); + assert!(result.is_ok()); + + // should have migrated to the new format + let event: SyncTimelineEvent = result.unwrap(); + assert_matches!( + event.kind, + TimelineEventKind::UnableToDecrypt { utd_info, .. }=> { + assert_matches!( + utd_info.reason, + UnableToDecryptReason::MissingMegolmSession { withheld_code: None } + ); + } + ) + } + + #[test] + fn unable_to_decrypt_info_migration_for_withheld() { + let old_format = json!({ + "reason": "MissingMegolmSession", + "session_id": "session000" + }); + + let deserialized = serde_json::from_value::(old_format).unwrap(); + let session_id = Some("session000".to_owned()); + + assert_eq!(deserialized.session_id, session_id); + assert_eq!( + deserialized.reason, + UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, + ); + + let new_format = json!({ + "session_id": "session000", + "reason": { + "MissingMegolmSession": { + "withheld_code": null + } + } + }); + + let deserialized = serde_json::from_value::(new_format).unwrap(); + + assert_eq!( + deserialized.reason, + UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, + ); + assert_eq!(deserialized.session_id, session_id); + } + + #[test] + fn unable_to_decrypt_reason_is_missing_room_key() { + let reason = UnableToDecryptReason::MissingMegolmSession { withheld_code: None }; + assert!(reason.is_missing_room_key()); + + let reason = UnableToDecryptReason::MissingMegolmSession { + withheld_code: Some(WithheldCode::Blacklisted), + }; + assert!(!reason.is_missing_room_key()); + + let reason = UnableToDecryptReason::UnknownMegolmMessageIndex; + assert!(reason.is_missing_room_key()); + } } diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index e069c95dd4e..92e18098859 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +- Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`. + These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors + when the sender either did not wish to share or was unable to share the room_key. + ([#4305](https://github.com/matrix-org/matrix-rust-sdk/pull/4305)) + ## [0.8.0] - 2024-11-19 ### Features diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 2e3348dce11..7a8ba63a8ed 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; -use matrix_sdk_common::deserialized_responses::VerificationLevel; +use matrix_sdk_common::deserialized_responses::{VerificationLevel, WithheldCode}; use ruma::{CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId}; use serde::{ser::SerializeMap, Serializer}; use serde_json::Error as SerdeError; @@ -22,10 +22,7 @@ use thiserror::Error; use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; use super::store::CryptoStoreError; -use crate::{ - olm::SessionExportError, - types::{events::room_key_withheld::WithheldCode, SignedKey}, -}; +use crate::{olm::SessionExportError, types::SignedKey}; #[cfg(doc)] use crate::{CollectStrategy, Device, LocalTrust, OtherUserIdentity}; diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index d518c12ac7a..da84d068a41 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -21,6 +21,7 @@ use std::{ }, }; +use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{ api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest, events::{key::verification::VerificationMethod, AnyToDeviceEventContent}, @@ -48,8 +49,7 @@ use crate::{ types::{ events::{ forwarded_room_key::ForwardedRoomKeyContent, - room::encrypted::ToDeviceEncryptedEventContent, room_key_withheld::WithheldCode, - EventType, + room::encrypted::ToDeviceEncryptedEventContent, EventType, }, requests::{OutgoingVerificationRequest, ToDeviceRequest}, DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures, SignedKey, diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index d5c80809b38..aa5bbc9a00e 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -2582,7 +2582,9 @@ fn megolm_error_to_utd_info( let reason = match error { EventError(_) => UnableToDecryptReason::MalformedEncryptedEvent, Decode(_) => UnableToDecryptReason::MalformedEncryptedEvent, - MissingRoomKey(_) => UnableToDecryptReason::MissingMegolmSession, + MissingRoomKey(maybe_withheld) => { + UnableToDecryptReason::MissingMegolmSession { withheld_code: maybe_withheld } + } Decryption(DecryptionError::UnknownMessageIndex(_, _)) => { UnableToDecryptReason::UnknownMegolmMessageIndex } diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index f1c52747c6b..86c2526e4b9 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -19,6 +19,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt}; use itertools::Itertools; use matrix_sdk_common::deserialized_responses::{ UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, + WithheldCode, }; use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json}; use ruma::{ @@ -61,9 +62,7 @@ use crate::{ types::{ events::{ room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent}, - room_key_withheld::{ - MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, WithheldCode, - }, + room_key_withheld::{MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent}, ToDeviceEvent, }, requests::{AnyOutgoingRequest, ToDeviceRequest}, @@ -683,7 +682,12 @@ async fn test_withheld_unverified() { bob.try_decrypt_room_event(&room_event, room_id, &decryption_settings).await.unwrap(); assert_let!(RoomEventDecryptionResult::UnableToDecrypt(utd_info) = decrypt_result); assert!(utd_info.session_id.is_some()); - assert_eq!(utd_info.reason, UnableToDecryptReason::MissingMegolmSession); + assert_eq!( + utd_info.reason, + UnableToDecryptReason::MissingMegolmSession { + withheld_code: Some(WithheldCode::Unverified) + } + ); } /// Test what happens when we feed an unencrypted event into the decryption @@ -1362,7 +1366,7 @@ async fn test_unsigned_decryption() { replace_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { session_id: Some(second_room_key_session_id), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }) ); @@ -1468,7 +1472,7 @@ async fn test_unsigned_decryption() { thread_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { session_id: Some(third_room_key_session_id), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }) ); diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index e7a51644704..1c5539d7203 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -23,6 +23,7 @@ use std::{ time::Duration, }; +use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{ events::{ room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility}, @@ -54,7 +55,7 @@ use crate::{ MegolmV1AesSha2Content, RoomEncryptedEventContent, RoomEventEncryptionScheme, }, room_key::{MegolmV1AesSha2Content as MegolmV1AesSha2RoomKeyContent, RoomKeyContent}, - room_key_withheld::{RoomKeyWithheldContent, WithheldCode}, + room_key_withheld::RoomKeyWithheldContent, }, requests::ToDeviceRequest, EventEncryptionAlgorithm, diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs index 72c44ca7365..b6ddeaaa290 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs @@ -22,7 +22,7 @@ use std::{ use futures_util::future::join_all; use itertools::Itertools; -use matrix_sdk_common::executor::spawn; +use matrix_sdk_common::{deserialized_responses::WithheldCode, executor::spawn}; use ruma::{ events::{AnyMessageLikeEventContent, ToDeviceEventType}, serde::Raw, @@ -41,10 +41,7 @@ use crate::{ ShareInfo, ShareState, }, store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store}, - types::{ - events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode}, - requests::ToDeviceRequest, - }, + types::{events::room::encrypted::RoomEncryptedEventContent, requests::ToDeviceRequest}, Device, DeviceData, EncryptionSettings, OlmError, }; @@ -782,6 +779,7 @@ mod tests { }; use assert_matches2::assert_let; + use matrix_sdk_common::deserialized_responses::WithheldCode; use matrix_sdk_test::{async_test, ruma_response_from_json}; use ruma::{ api::client::{ @@ -804,10 +802,7 @@ mod tests { types::{ events::{ room::encrypted::EncryptedToDeviceEvent, - room_key_withheld::{ - RoomKeyWithheldContent::{self, MegolmV1AesSha2}, - WithheldCode, - }, + room_key_withheld::RoomKeyWithheldContent::{self, MegolmV1AesSha2}, }, requests::ToDeviceRequest, DeviceKeys, EventEncryptionAlgorithm, diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 7b3df39f41a..f3e5b074b00 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -19,6 +19,7 @@ use std::{ }; use itertools::{Either, Itertools}; +use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId}; use serde::{Deserialize, Serialize}; use tracing::{debug, instrument, trace}; @@ -27,7 +28,6 @@ use super::OutboundGroupSession; use crate::{ error::{OlmResult, SessionRecipientCollectionError}, store::Store, - types::events::room_key_withheld::WithheldCode, DeviceData, EncryptionSettings, LocalTrust, OlmError, OwnUserIdentityData, UserIdentityData, }; #[cfg(doc)] @@ -517,6 +517,7 @@ mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; + use matrix_sdk_common::deserialized_responses::WithheldCode; use matrix_sdk_test::{ async_test, test_json, test_json::keys_query_sets::{ @@ -536,7 +537,6 @@ mod tests { group_sessions::share_strategy::collect_session_recipients, CollectStrategy, }, testing::simulate_key_query_response_for_verification, - types::events::room_key_withheld::WithheldCode, CrossSigningKeyExport, EncryptionSettings, LocalTrust, OlmError, OlmMachine, }; diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index f454a3e9567..a84442f4525 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -44,6 +44,7 @@ macro_rules! cryptostore_integration_tests { }; use serde_json::value::to_raw_value; use serde_json::json; + use matrix_sdk_common::deserialized_responses::WithheldCode; use $crate::{ olm::{ Account, Curve25519PublicKey, InboundGroupSession, OlmMessageHash, @@ -61,7 +62,7 @@ macro_rules! cryptostore_integration_tests { room_key_request::MegolmV1AesSha2Content, room_key_withheld::{ CommonWithheldCodeContent, MegolmV1AesSha2WithheldContent, - RoomKeyWithheldContent, WithheldCode, + RoomKeyWithheldContent, }, secret_send::SecretSendContent, ToDeviceEvent, @@ -70,10 +71,8 @@ macro_rules! cryptostore_integration_tests { DeviceKeys, EventEncryptionAlgorithm, }, - GossippedSecret, LocalTrust, DeviceData, SecretInfo, TrackedUser, - vodozemac::{ - megolm::{GroupSession, SessionConfig}, - }, + vodozemac::megolm::{GroupSession, SessionConfig}, DeviceData, GossippedSecret, LocalTrust, SecretInfo, + TrackedUser, }; use super::get_store; diff --git a/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs b/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs index 8b9a8367763..61b1bd91d5c 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs @@ -16,19 +16,14 @@ use std::collections::BTreeMap; -use ruma::{ - exports::ruma_macros::AsStrAsRefStr, - serde::{AsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, SerializeAsRefStr}, - OwnedDeviceId, OwnedRoomId, -}; +use matrix_sdk_common::deserialized_responses::WithheldCode; +use ruma::{OwnedDeviceId, OwnedRoomId}; use serde::{Deserialize, Serialize}; use serde_json::Value; use vodozemac::Curve25519PublicKey; use super::{EventType, ToDeviceEvent}; -use crate::types::{ - deserialize_curve_key, serialize_curve_key, EventEncryptionAlgorithm, PrivOwnedStr, -}; +use crate::types::{deserialize_curve_key, serialize_curve_key, EventEncryptionAlgorithm}; /// The `m.room_key_request` to-device event. pub type RoomKeyWithheldEvent = ToDeviceEvent; @@ -160,65 +155,6 @@ impl EventType for RoomKeyWithheldContent { const EVENT_TYPE: &'static str = "m.room_key.withheld"; } -/// A machine-readable code for why the megolm key was not sent. -#[derive( - Clone, - PartialEq, - Eq, - Hash, - AsStrAsRefStr, - AsRefStr, - FromString, - DebugAsRefStr, - SerializeAsRefStr, - DeserializeFromCowStr, -)] -#[non_exhaustive] -pub enum WithheldCode { - /// the user/device was blacklisted. - #[ruma_enum(rename = "m.blacklisted")] - Blacklisted, - - /// the user/devices is unverified. - #[ruma_enum(rename = "m.unverified")] - Unverified, - - /// The user/device is not allowed have the key. For example, this would - /// usually be sent in response to a key request if the user was not in - /// the room when the message was sent. - #[ruma_enum(rename = "m.unauthorised")] - Unauthorised, - - /// Sent in reply to a key request if the device that the key is requested - /// from does not have the requested key. - #[ruma_enum(rename = "m.unavailable")] - Unavailable, - - /// An olm session could not be established. - /// This may happen, for example, if the sender was unable to obtain a - /// one-time key from the recipient. - #[ruma_enum(rename = "m.no_olm")] - NoOlm, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} - -impl std::fmt::Display for WithheldCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - let string = match self { - WithheldCode::Blacklisted => "The sender has blocked you.", - WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.", - WithheldCode::Unauthorised => "You are not authorised to read the message.", - WithheldCode::Unavailable => "The requested key was not found.", - WithheldCode::NoOlm => "Unable to establish a secure channel.", - _ => self.as_str(), - }; - - f.write_str(string) - } -} - #[derive(Debug, Deserialize, Serialize)] struct WithheldHelper { pub algorithm: EventEncryptionAlgorithm, @@ -490,15 +426,14 @@ pub(super) mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; + use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{device_id, room_id, serde::Raw, to_device::DeviceIdOrAllDevices, user_id}; use serde_json::{json, Value}; use vodozemac::Curve25519PublicKey; use super::RoomKeyWithheldEvent; use crate::types::{ - events::room_key_withheld::{ - MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, WithheldCode, - }, + events::room_key_withheld::{MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent}, EventEncryptionAlgorithm, }; diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index c7d2a0810fc..bef568ea10c 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -13,7 +13,7 @@ // limitations under the License. use matrix_sdk_common::deserialized_responses::{ - UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, + UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, WithheldCode, }; use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch}; use serde::Deserialize; @@ -57,6 +57,19 @@ pub enum UtdCause { /// be confused with pre-join or pre-invite messages (see /// [`UtdCause::SentBeforeWeJoined`] for that). HistoricalMessage = 5, + + /// The keys for this event are intentionally withheld. + /// + /// The sender has refused to share the key because our device does not meet + /// the sender's security requirements. + WithheldForUnverifiedOrInsecureDevice = 6, + + /// The keys for this event are missing, likely because the sender was + /// unable to share them (e.g., failure to establish an Olm 1:1 + /// channel). Alternatively, the sender may have deliberately excluded + /// this device by cherry-picking and blocking it, in which case, no action + /// can be taken on our side. + WithheldBySender = 7, } /// MSC4115 membership info in the unsigned area. @@ -97,8 +110,18 @@ impl UtdCause { unable_to_decrypt_info: &UnableToDecryptInfo, ) -> Self { // TODO: in future, use more information to give a richer answer. E.g. - match unable_to_decrypt_info.reason { - UnableToDecryptReason::MissingMegolmSession + match &unable_to_decrypt_info.reason { + UnableToDecryptReason::MissingMegolmSession { withheld_code: Some(reason) } => { + match reason { + WithheldCode::Unverified => UtdCause::WithheldForUnverifiedOrInsecureDevice, + WithheldCode::Blacklisted + | WithheldCode::Unauthorised + | WithheldCode::Unavailable + | WithheldCode::NoOlm + | WithheldCode::_Custom(_) => UtdCause::WithheldBySender, + } + } + UnableToDecryptReason::MissingMegolmSession { withheld_code: None } | UnableToDecryptReason::UnknownMegolmMessageIndex => { // Look in the unsigned area for a `membership` field. if let Some(unsigned) = @@ -424,7 +447,7 @@ mod tests { fn missing_megolm_session() -> UnableToDecryptInfo { UnableToDecryptInfo { session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, } } diff --git a/crates/matrix-sdk-crypto/src/types/mod.rs b/crates/matrix-sdk-crypto/src/types/mod.rs index bd22d3a78ec..8bb3f324583 100644 --- a/crates/matrix-sdk-crypto/src/types/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/mod.rs @@ -34,6 +34,7 @@ use std::{ }; use as_variant::as_variant; +use matrix_sdk_common::deserialized_responses::PrivOwnedStr; use ruma::{ serde::StringEnum, DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceKeyId, OwnedUserId, UserId, }; @@ -425,20 +426,6 @@ impl Algorithm for DeviceKeyAlgorithm { } } -// Wrapper around `Box` that cannot be used in a meaningful way outside of -// this crate. Used for string enums because their `_Custom` variant can't be -// truly private (only `#[doc(hidden)]`). -#[doc(hidden)] -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PrivOwnedStr(Box); - -#[cfg(not(tarpaulin_include))] -impl std::fmt::Debug for PrivOwnedStr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - /// An encryption algorithm to be used to encrypt messages sent to a room. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index f23d45a9433..c94b0f764f6 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -37,6 +37,7 @@ vodozemac = { workspace = true } assert_matches = { workspace = true } glob = "0.3.1" matrix-sdk-base = { workspace = true, features = ["testing"] } +matrix-sdk-common = { workspace = true } matrix-sdk-crypto = { workspace = true, features = ["testing"] } matrix-sdk-test = { workspace = true } once_cell = { workspace = true } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index 131cffae580..1daebc20160 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -774,7 +774,7 @@ fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { raw, matrix_sdk::deserialized_responses::UnableToDecryptInfo { session_id: Some("SESSION_ID".into()), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }, ) } diff --git a/crates/matrix-sdk/src/authentication/qrcode/messages.rs b/crates/matrix-sdk/src/authentication/qrcode/messages.rs index 6ea48198823..2abbef5a979 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/messages.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/messages.rs @@ -13,6 +13,7 @@ // limitations under the License. use matrix_sdk_base::crypto::types::SecretsBundle; +use matrix_sdk_common::deserialized_responses::PrivOwnedStr; use openidconnect::{ core::CoreDeviceAuthorizationResponse, EndUserVerificationUrl, VerificationUriComplete, }; @@ -183,15 +184,6 @@ where s.serialize_str(&key.to_base64()) } -// Wrapper around `Box` that cannot be used in a meaningful way outside of -// this crate. Used for string enums because their `_Custom` variant can't be -// truly private (only `#[doc(hidden)]`). -// TODO: It probably makes sense to move the above messages into Ruma, if for -// nothing else, to get rid of this `PrivOwnedStr`. -#[doc(hidden)] -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PrivOwnedStr(Box); - #[cfg(test)] mod test { use assert_matches2::assert_let; diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index 16787599cc6..1055215a029 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -211,7 +211,10 @@ impl EventBuilder { SyncTimelineEvent::new_utd_event( self.into(), - UnableToDecryptInfo { session_id, reason: UnableToDecryptReason::MissingMegolmSession }, + UnableToDecryptInfo { + session_id, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, + }, ) } } From 136522c69455c76520bb02a429fe2d5c86e25fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 4 Dec 2024 13:36:28 +0100 Subject: [PATCH 697/979] Revert "doc(timeline): tweak comments when inserting a new item" This reverts commit 197da2c585961df9b97a2af18687945156e5a3f8. --- .../src/timeline/event_handler.rs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 0b878b89961..ab0a6b52442 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -1158,6 +1158,17 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { None => self.meta.new_timeline_item(item), }; + trace!("Adding new remote timeline item after all non-local events"); + + // We are about to insert the `new_item`, great! Though, we try to keep + // precise insertion semantics here, in this exact order: + // + // * _push back_ when the new item is inserted after all items, + // * _push front_ when the new item is inserted at index 0, + // * _insert_ otherwise. + // + // It means that the first inserted item will generate a _push back_ for + // example. match position { TimelineItemPosition::Start { .. } => { trace!("Adding new remote timeline item at the front"); @@ -1176,15 +1187,9 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // local echo, or at the start if there is no such item. let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); - // Try to keep precise insertion semantics here, in this exact order: - // - // * _push back_ when the new item is inserted after all items (the - // assumption - // being that this is the hot path, because most of the time new events - // come from the sync), - // * _push front_ when the new item is inserted at index 0, - // * _insert_ otherwise. - + // Let's prioritize push backs because it's the hot path. Events are more + // generally added at the back because they come from the sync most of the + // time. if insert_idx == self.items.len() { trace!("Adding new remote timeline item at the back"); self.items.push_back(new_item); From 7d8e7af30839c9729b1fad4e08a563d88b93dd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 4 Dec 2024 13:36:33 +0100 Subject: [PATCH 698/979] Revert "chore(ui): Unify the logic for timeline item insertions" This reverts commit d2ecd745f6dadfa5ebedc8e038fbdacce977e19b. --- .../src/timeline/event_handler.rs | 181 ++++++++---------- 1 file changed, 79 insertions(+), 102 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index ab0a6b52442..cc01448714b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -1085,71 +1085,88 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { self.items.push_back(item); } - Flow::Remote { - position: position @ TimelineItemPosition::Start { .. }, - txn_id, - event_id, - .. + Flow::Remote { position: TimelineItemPosition::Start { .. }, event_id, .. } => { + if self + .items + .iter() + .filter_map(|ev| ev.as_event()?.event_id()) + .any(|id| id == event_id) + { + trace!("Skipping back-paginated event that has already been seen"); + return; + } + + trace!("Adding new remote timeline item at the start"); + + let item = self.meta.new_timeline_item(item); + self.items.push_front(item); } - | Flow::Remote { - position: position @ TimelineItemPosition::End { .. }, - txn_id, - event_id, - .. + + Flow::Remote { + position: TimelineItemPosition::End { .. }, txn_id, event_id, .. } => { - // This block tries to find duplicated events. - - let removed_event_item_id = { - // Look if we already have a corresponding item somewhere, based on the - // transaction id (if this is a local echo) or the event id (if this is a - // duplicate remote event). - let result = rfind_event_item(self.items, |it| { - txn_id.is_some() && it.transaction_id() == txn_id.as_deref() - || it.event_id() == Some(event_id) - }); - - if let Some((idx, old_item)) = result { - if old_item.as_remote().is_some() { - // The item was previously received from the server. This should be very - // rare normally, but with the sliding- sync proxy, it is actually very - // common. - // NOTE: This is a SS proxy workaround. - trace!(?item, old_item = ?*old_item, "Received duplicate event"); - - if old_item.content.is_redacted() && !item.content.is_redacted() { - warn!("Got original form of an event that was previously redacted"); - item.content = item.content.redact(&self.meta.room_version); - item.reactions.clear(); - } + // Look if we already have a corresponding item somewhere, based on the + // transaction id (if a local echo) or the event id (if a + // duplicate remote event). + let result = rfind_event_item(self.items, |it| { + txn_id.is_some() && it.transaction_id() == txn_id.as_deref() + || it.event_id() == Some(event_id) + }); + + let mut removed_event_item_id = None; + + if let Some((idx, old_item)) = result { + if old_item.as_remote().is_some() { + // Item was previously received from the server. This should be very rare + // normally, but with the sliding- sync proxy, it is actually very + // common. + // NOTE: SS proxy workaround. + trace!(?item, old_item = ?*old_item, "Received duplicate event"); + + if old_item.content.is_redacted() && !item.content.is_redacted() { + warn!("Got original form of an event that was previously redacted"); + item.content = item.content.redact(&self.meta.room_version); + item.reactions.clear(); } + } - // TODO: Check whether anything is different about the - // old and new item? + // TODO: Check whether anything is different about the + // old and new item? - transfer_details(&mut item, &old_item); + transfer_details(&mut item, &old_item); - let old_item_id = old_item.internal_id; + let old_item_id = old_item.internal_id; - if idx == self.items.len() - 1 { - // If the old item is the last one and no day divider - // changes need to happen, replace and return early. - trace!(idx, "Replacing existing event"); - self.items.set(idx, TimelineItem::new(item, old_item_id.to_owned())); - return; - } + if idx == self.items.len() - 1 { + // If the old item is the last one and no day divider + // changes need to happen, replace and return early. + trace!(idx, "Replacing existing event"); + self.items.set(idx, TimelineItem::new(item, old_item_id.to_owned())); + return; + } - // In more complex cases, remove the item before re-adding the item. - trace!("Removing local echo or duplicate timeline item"); + // In more complex cases, remove the item before re-adding the item. + trace!("Removing local echo or duplicate timeline item"); + removed_event_item_id = Some(self.items.remove(idx).internal_id.clone()); - // no return here, the below logic for adding a new event - // will run to re-add the removed item + // no return here, below code for adding a new event + // will run to re-add the removed item + } - Some(self.items.remove(idx).internal_id.clone()) - } else { - None - } - }; + // Local echoes that are pending should stick to the bottom, + // find the latest event that isn't that. + let latest_event_idx = self + .items + .iter() + .enumerate() + .rev() + .find_map(|(idx, item)| (!item.as_event()?.is_local_echo()).then_some(idx)); + + // Insert the next item after the latest event item that's not a + // pending local echo, or at the start if there is no such item. + let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); + trace!("Adding new remote timeline item after all non-pending events"); let new_item = match removed_event_item_id { // If a previous version of the same item (usually a local // echo) was removed and we now need to add it again, reuse @@ -1158,54 +1175,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { None => self.meta.new_timeline_item(item), }; - trace!("Adding new remote timeline item after all non-local events"); - - // We are about to insert the `new_item`, great! Though, we try to keep - // precise insertion semantics here, in this exact order: - // - // * _push back_ when the new item is inserted after all items, - // * _push front_ when the new item is inserted at index 0, - // * _insert_ otherwise. - // - // It means that the first inserted item will generate a _push back_ for - // example. - match position { - TimelineItemPosition::Start { .. } => { - trace!("Adding new remote timeline item at the front"); - self.items.push_front(new_item); - } - - TimelineItemPosition::End { .. } => { - // Local echoes that are pending should stick to the bottom, - // find the latest event that isn't that. - let latest_event_idx = - self.items.iter().enumerate().rev().find_map(|(idx, item)| { - (!item.as_event()?.is_local_echo()).then_some(idx) - }); - - // Insert the next item after the latest event item that's not a pending - // local echo, or at the start if there is no such item. - let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); - - // Let's prioritize push backs because it's the hot path. Events are more - // generally added at the back because they come from the sync most of the - // time. - if insert_idx == self.items.len() { - trace!("Adding new remote timeline item at the back"); - self.items.push_back(new_item); - } else if insert_idx == 0 { - trace!("Adding new remote timeline item at the front"); - self.items.push_front(new_item); - } else { - trace!(insert_idx, "Adding new remote timeline item at specific index"); - self.items.insert(insert_idx, new_item); - } - } - - p => unreachable!( - "An unexpected `TimelineItemPosition` has been received: {p:?}" - ), - }; + // Keep push semantics, if we're inserting at the front or the back. + if insert_idx == self.items.len() { + self.items.push_back(new_item); + } else if insert_idx == 0 { + self.items.push_front(new_item); + } else { + self.items.insert(insert_idx, new_item); + } } Flow::Remote { From ee93c278dff6f343b322bbf2a8be4115cfb30b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 4 Dec 2024 15:49:30 +0100 Subject: [PATCH 699/979] chore: Update the hashbrown version we're using --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdd479221ec..f06cb85e48a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2049,9 +2049,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashlink" @@ -2525,7 +2525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "serde", ] From d317e5d73c61fcf1d07ad3cfd39ff5243c789015 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 16:05:05 +0100 Subject: [PATCH 700/979] feat(event cache): don't react specifically to limited timelines, when storage's enabled --- crates/matrix-sdk/src/event_cache/mod.rs | 20 ++++- crates/matrix-sdk/src/event_cache/room/mod.rs | 25 +++++-- .../tests/integration/event_cache.rs | 73 ++++++++++++++++++- 3 files changed, 104 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index dfe2df5a671..089d42eb46b 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -313,7 +313,7 @@ impl EventCache { prev_batch: Option, ) -> Result<()> { // If the event cache's storage has been enabled, do nothing. - if self.inner.store.get().is_some() { + if self.inner.has_storage() { return Ok(()); } @@ -387,6 +387,11 @@ impl EventCacheInner { self.client.get().ok_or(EventCacheError::ClientDropped) } + /// Has persistent storage been enabled for the event cache? + fn has_storage(&self) -> bool { + self.store.get().is_some() + } + /// Clears all the room's data. async fn clear_all_rooms(&self) -> Result<()> { // Note: one must NOT clear the `by_room` map, because if something subscribed @@ -419,7 +424,9 @@ impl EventCacheInner { for (room_id, left_room_update) in updates.leave { let room = self.for_room(&room_id).await?; - if let Err(err) = room.inner.handle_left_room_update(left_room_update).await { + if let Err(err) = + room.inner.handle_left_room_update(self.has_storage(), left_room_update).await + { // Non-fatal error, try to continue to the next room. error!("handling left room update: {err}"); } @@ -429,7 +436,9 @@ impl EventCacheInner { for (room_id, joined_room_update) in updates.join { let room = self.for_room(&room_id).await?; - if let Err(err) = room.inner.handle_joined_room_update(joined_room_update).await { + if let Err(err) = + room.inner.handle_joined_room_update(self.has_storage(), joined_room_update).await + { // Non-fatal error, try to continue to the next room. error!("handling joined room update: {err}"); } @@ -604,7 +613,10 @@ mod tests { room_event_cache .inner - .handle_joined_room_update(JoinedRoomUpdate { account_data, ..Default::default() }) + .handle_joined_room_update( + event_cache.inner.has_storage(), + JoinedRoomUpdate { account_data, ..Default::default() }, + ) .await .unwrap(); diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 55938362248..cdebfc10326 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -286,8 +286,13 @@ impl RoomEventCacheInner { } } - pub(super) async fn handle_joined_room_update(&self, updates: JoinedRoomUpdate) -> Result<()> { + pub(super) async fn handle_joined_room_update( + &self, + has_storage: bool, + updates: JoinedRoomUpdate, + ) -> Result<()> { self.handle_timeline( + has_storage, updates.timeline, updates.ephemeral.clone(), updates.ambiguity_changes, @@ -301,11 +306,12 @@ impl RoomEventCacheInner { async fn handle_timeline( &self, + has_storage: bool, timeline: Timeline, ephemeral_events: Vec>, ambiguity_changes: BTreeMap, ) -> Result<()> { - if timeline.limited { + if !has_storage && timeline.limited { // Ideally we'd try to reconcile existing events against those received in the // timeline, but we're not there yet. In the meanwhile, clear the // items from the room. TODO: implement Smart Matching™. @@ -334,8 +340,13 @@ impl RoomEventCacheInner { Ok(()) } - pub(super) async fn handle_left_room_update(&self, updates: LeftRoomUpdate) -> Result<()> { - self.handle_timeline(updates.timeline, Vec::new(), updates.ambiguity_changes).await?; + pub(super) async fn handle_left_room_update( + &self, + has_storage: bool, + updates: LeftRoomUpdate, + ) -> Result<()> { + self.handle_timeline(has_storage, updates.timeline, Vec::new(), updates.ambiguity_changes) + .await?; Ok(()) } @@ -990,7 +1001,7 @@ mod tests { room_event_cache .inner - .handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() }) + .handle_joined_room_update(true, JoinedRoomUpdate { timeline, ..Default::default() }) .await .unwrap(); @@ -1060,7 +1071,7 @@ mod tests { room_event_cache .inner - .handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() }) + .handle_joined_room_update(true, JoinedRoomUpdate { timeline, ..Default::default() }) .await .unwrap(); @@ -1188,7 +1199,7 @@ mod tests { let timeline = Timeline { limited: false, prev_batch: None, events: vec![ev1] }; room_event_cache .inner - .handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() }) + .handle_joined_room_update(true, JoinedRoomUpdate { timeline, ..Default::default() }) .await .unwrap(); diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 8dced657e0e..24d8d155a8a 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1,10 +1,11 @@ use std::{future::ready, ops::ControlFlow, time::Duration}; -use assert_matches2::{assert_let, assert_matches}; +use assert_matches::assert_matches; +use assert_matches2::assert_let; use matrix_sdk::{ event_cache::{ - paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, - TimelineHasBeenResetWhilePaginating, + paginator::PaginatorState, BackPaginationOutcome, EventCacheError, EventsOrigin, + RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, test_utils::{assert_event_matches_msg, logged_in_client_with_server, mocks::MatrixMockServer}, }; @@ -852,3 +853,69 @@ async fn test_limited_timeline_resets_pagination() { assert!(room_stream.is_empty()); } + +#[async_test] +async fn test_limited_timeline_with_storage() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Don't forget to subscribe and like^W enable storage! + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + let room_id = room_id!("!galette:saucisse.bzh"); + let room = server.sync_joined_room(&client, room_id).await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + // First sync: get a message. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hey yo")), + ) + .await; + + let (initial_events, mut subscriber) = room_event_cache.subscribe().await.unwrap(); + + // This is racy: either the sync has been handled, or it hasn't yet. + if initial_events.is_empty() { + assert_let_timeout!( + Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() + ); + assert_eq!(events.len(), 1); + assert_event_matches_msg(&events[0], "hey yo"); + } else { + assert_eq!(initial_events.len(), 1); + assert_event_matches_msg(&initial_events[0], "hey yo"); + } + + assert!(subscriber.is_empty()); + + // Second update: get a message but from a limited timeline. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("gappy!")) + .set_timeline_limited(), + ) + .await; + + let update = timeout(Duration::from_secs(2), subscriber.recv()) + .await + .expect("timeout after receiving a sync update") + .expect("should've received a room event cache update"); + + assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { events, origin: EventsOrigin::Sync } => { + assert_eq!(events.len(), 1); + assert_event_matches_msg(&events[0], "gappy!"); + }); + + // That's all, folks! + assert!(subscriber.is_empty()); +} From 713039279c8ab846be622c82d0451906a8f28b6e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 16:39:30 +0100 Subject: [PATCH 701/979] Enable persistent storage in multiverse And fix an issue that would cause a crash because a timeline wasn't initialized and we tried to unwrap it later. --- labs/multiverse/src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index f0deb997416..9032993726b 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -64,6 +64,10 @@ async fn main() -> anyhow::Result<()> { let config_path = env::args().nth(2).unwrap_or("/tmp/".to_owned()); let client = configure_client(server_name, config_path).await?; + let ec = client.event_cache(); + ec.subscribe().unwrap(); + ec.enable_storage().unwrap(); + init_error_hooks()?; let terminal = init_terminal()?; @@ -256,6 +260,7 @@ impl App { if let Err(err) = ui_room.init_timeline_with_builder(builder).await { error!("error when creating default timeline: {err}"); + continue; } // Save the timeline in the cache. From 0b64c6819162c17716a2be22e7be58fc170d3ade Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Dec 2024 12:47:03 +0100 Subject: [PATCH 702/979] test(event cache): make use of macros to avoid manual timeouts --- .../tests/integration/event_cache.rs | 65 ++++++------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 24d8d155a8a..7bfe6d98e25 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1,11 +1,11 @@ use std::{future::ready, ops::ControlFlow, time::Duration}; use assert_matches::assert_matches; -use assert_matches2::assert_let; use matrix_sdk::{ + assert_let_timeout, assert_next_matches_with_timeout, event_cache::{ - paginator::PaginatorState, BackPaginationOutcome, EventCacheError, EventsOrigin, - RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, + paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, + TimelineHasBeenResetWhilePaginating, }, test_utils::{assert_event_matches_msg, logged_in_client_with_server, mocks::MatrixMockServer}, }; @@ -15,7 +15,7 @@ use matrix_sdk_test::{ }; use ruma::{event_id, events::AnyTimelineEvent, room_id, serde::Raw, user_id}; use serde_json::json; -use tokio::{spawn, time::timeout}; +use tokio::spawn; use wiremock::{ matchers::{header, method, path_regex, query_param}, Mock, MockServer, ResponseTemplate, @@ -104,13 +104,11 @@ async fn test_event_cache_receives_events() { server.reset().await; // It does receive one update, - let update = timeout(Duration::from_secs(2), subscriber.recv()) - .await - .expect("timeout after receiving a sync update") - .expect("should've received a room event cache update"); + assert_let_timeout!( + Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() + ); // Which contains the event that was sent beforehand. - assert_let!(RoomEventCacheUpdate::AddTimelineEvents { events, .. } = update); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "bonjour monde"); @@ -177,14 +175,8 @@ async fn test_ignored_unignored() { }) .await; - // It does receive one update, - let update = timeout(Duration::from_secs(2), subscriber.recv()) - .await - .expect("timeout after receiving a sync update") - .expect("should've received a room event cache update"); - - // Which notifies about the clear. - assert_matches!(update, RoomEventCacheUpdate::Clear); + // It does receive one update, which notifies about the clear. + assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = subscriber.recv()); // Receiving new events still works. server @@ -198,12 +190,9 @@ async fn test_ignored_unignored() { .await; // We do receive one update, - let update = timeout(Duration::from_secs(2), subscriber.recv()) - .await - .expect("timeout after receiving a sync update") - .expect("should've received a room event cache update"); - - assert_let!(RoomEventCacheUpdate::AddTimelineEvents { events, .. } = update); + assert_let_timeout!( + Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() + ); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "i don't like this dexter"); @@ -817,10 +806,7 @@ async fn test_limited_timeline_resets_pagination() { // And the paginator state delives this as an update, and is internally // consistent with it: - assert_eq!( - timeout(Duration::from_secs(1), pagination_status.next()).await, - Ok(Some(PaginatorState::Idle)) - ); + assert_next_matches_with_timeout!(pagination_status, PaginatorState::Idle); assert!(pagination.hit_timeline_start()); // When a limited sync comes back from the server, @@ -834,10 +820,7 @@ async fn test_limited_timeline_resets_pagination() { } // We receive an update about the limited timeline. - assert_matches!( - timeout(Duration::from_secs(1), room_stream.recv()).await, - Ok(Ok(RoomEventCacheUpdate::Clear)) - ); + assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = room_stream.recv()); // The paginator state is reset: status set to Initial, hasn't hit the timeline // start. @@ -845,11 +828,7 @@ async fn test_limited_timeline_resets_pagination() { assert_eq!(pagination_status.get(), PaginatorState::Initial); // We receive an update about the paginator status. - let next_state = timeout(Duration::from_secs(1), pagination_status.next()) - .await - .expect("timeout") - .expect("no update"); - assert_eq!(next_state, PaginatorState::Initial); + assert_next_matches_with_timeout!(pagination_status, PaginatorState::Initial); assert!(room_stream.is_empty()); } @@ -906,15 +885,11 @@ async fn test_limited_timeline_with_storage() { ) .await; - let update = timeout(Duration::from_secs(2), subscriber.recv()) - .await - .expect("timeout after receiving a sync update") - .expect("should've received a room event cache update"); - - assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { events, origin: EventsOrigin::Sync } => { - assert_eq!(events.len(), 1); - assert_event_matches_msg(&events[0], "gappy!"); - }); + assert_let_timeout!( + Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() + ); + assert_eq!(events.len(), 1); + assert_event_matches_msg(&events[0], "gappy!"); // That's all, folks! assert!(subscriber.is_empty()); From a6e1f05957524b0fc28d4d6db89ff06b9bccd334 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 4 Dec 2024 13:07:26 +0100 Subject: [PATCH 703/979] test(event cache): use the MatrixMockServer for integration testing --- crates/matrix-sdk/src/test_utils/mocks.rs | 9 +- .../tests/integration/event_cache.rs | 387 ++++++++---------- crates/matrix-sdk/tests/integration/widget.rs | 8 +- 3 files changed, 180 insertions(+), 224 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 46981c4b6c5..f1637e20dac 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -1406,6 +1406,11 @@ impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { Self { mock: self.mock.and(query_param("limit", limit.to_string())), ..self } } + /// Expects an optional `from` to be set on the request. + pub fn from(self, from: &str) -> Self { + Self { mock: self.mock.and(query_param("from", from)), ..self } + } + /// Returns a messages endpoint that emulates success, i.e. the messages /// provided as `response` could be retrieved. /// @@ -1416,13 +1421,13 @@ impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { start: String, end: Option, chunk: Vec>>, - state: Vec>>, + state: Vec>, ) -> MatrixMock<'a> { let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "start": start, "end": end, "chunk": chunk.into_iter().map(|ev| ev.into()).collect::>(), - "state": state.into_iter().map(|ev| ev.into()).collect::>(), + "state": state, }))); MatrixMock { server: self.server, mock } } diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 7bfe6d98e25..c593f8e96e5 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -13,13 +13,10 @@ use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, }; -use ruma::{event_id, events::AnyTimelineEvent, room_id, serde::Raw, user_id}; +use ruma::{event_id, room_id, user_id}; use serde_json::json; use tokio::spawn; -use wiremock::{ - matchers::{header, method, path_regex, query_param}, - Mock, MockServer, ResponseTemplate, -}; +use wiremock::ResponseTemplate; use crate::mock_sync; @@ -208,64 +205,33 @@ async fn test_ignored_unignored() { assert!(subscriber.is_empty()); } -/// Puts a mounting point for /messages for a pagination request, matching -/// against a precise `from` token given as `expected_from`, and returning the -/// chunk of events and the next token as `end` (if available). -// TODO: replace this with the `mock_room_messages` from mocks.rs -async fn mock_messages( - server: &MockServer, - expected_from: &str, - next_token: Option<&str>, - chunk: Vec>>, -) { - let response_json = json!({ - "chunk": chunk.into_iter().map(|item| item.into()).collect::>(), - "start": "t392-516_47314_0_7_1_1_1_11444_1", - "end": next_token, - }); - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .and(query_param("from", expected_from)) - .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) - .expect(1) - .mount(server) - .await; -} - #[async_test] async fn test_backpaginate_once() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; let event_cache = client.event_cache(); // Immediately subscribe the event cache to sync updates. event_cache.subscribe().unwrap(); - // If I sync and get informed I've joined The Room, and get a previous batch - // token, let room_id = room_id!("!omelette:fromage.fr"); - let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); - let mut sync_builder = SyncResponseBuilder::new(); - { - sync_builder.add_joined_room( + // If I sync and get informed I've joined The Room, and get a previous batch + // token, + let room = server + .sync_room( + &client, JoinedRoomBuilder::new(room_id) // Note to self: a timeline must have at least single event to be properly // serialized. .add_timeline_event(f.text_msg("heyo")) .set_timeline_prev_batch("prev_batch".to_owned()), - ); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } + ) + .await; - let (room_event_cache, _drop_handles) = - client.get_room(room_id).unwrap().event_cache().await.unwrap(); + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, mut room_stream) = room_event_cache.subscribe().await.unwrap(); @@ -281,16 +247,21 @@ async fn test_backpaginate_once() { let outcome = { // Note: events must be presented in reversed order, since this is // back-pagination. - mock_messages( - &server, - "prev_batch", - None, - vec![ - f.text_msg("world").event_id(event_id!("$2")), - f.text_msg("hello").event_id(event_id!("$3")), - ], - ) - .await; + server + .mock_room_messages() + .from("prev_batch") + .ok( + "start-token-unused".to_owned(), + None, + vec![ + f.text_msg("world").event_id(event_id!("$2")), + f.text_msg("hello").event_id(event_id!("$3")), + ], + Vec::new(), + ) + .mock_once() + .mount() + .await; // Then if I backpaginate, let pagination = room_event_cache.pagination(); @@ -313,7 +284,8 @@ async fn test_backpaginate_once() { #[async_test] async fn test_backpaginate_many_times_with_many_iterations() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; let event_cache = client.event_cache(); @@ -325,25 +297,19 @@ async fn test_backpaginate_many_times_with_many_iterations() { let room_id = room_id!("!omelette:fromage.fr"); let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); - let mut sync_builder = SyncResponseBuilder::new(); - { - sync_builder.add_joined_room( + let room = server + .sync_room( + &client, JoinedRoomBuilder::new(room_id) // Note to self: a timeline must have at least single event to be properly // serialized. .add_timeline_event(f.text_msg("heyo")) .set_timeline_prev_batch("prev_batch".to_owned()), - ); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } + ) + .await; - let (room_event_cache, _drop_handles) = - client.get_room(room_id).unwrap().event_cache().await.unwrap(); + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, mut room_stream) = room_event_cache.subscribe().await.unwrap(); @@ -362,25 +328,35 @@ async fn test_backpaginate_many_times_with_many_iterations() { let mut global_reached_start = false; // The first back-pagination will return these two. - mock_messages( - &server, - "prev_batch", - Some("prev_batch2"), - vec![ - f.text_msg("world").event_id(event_id!("$2")), - f.text_msg("hello").event_id(event_id!("$3")), - ], - ) - .await; + server + .mock_room_messages() + .from("prev_batch") + .ok( + "start-token-unused".to_owned(), + Some("prev_batch2".to_owned()), + vec![ + f.text_msg("world").event_id(event_id!("$2")), + f.text_msg("hello").event_id(event_id!("$3")), + ], + Vec::new(), + ) + .mock_once() + .mount() + .await; // The second round of back-pagination will return this one. - mock_messages( - &server, - "prev_batch2", - None, - vec![f.text_msg("oh well").event_id(event_id!("$4"))], - ) - .await; + server + .mock_room_messages() + .from("prev_batch2") + .ok( + "start-token-unused".to_owned(), + None, + vec![f.text_msg("oh well").event_id(event_id!("$4"))], + Vec::new(), + ) + .mock_once() + .mount() + .await; // Then if I backpaginate in a loop, let pagination = room_event_cache.pagination(); @@ -429,7 +405,8 @@ async fn test_backpaginate_many_times_with_many_iterations() { #[async_test] async fn test_backpaginate_many_times_with_one_iteration() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; let event_cache = client.event_cache(); @@ -441,22 +418,17 @@ async fn test_backpaginate_many_times_with_one_iteration() { let room_id = room_id!("!omelette:fromage.fr"); let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); - let mut sync_builder = SyncResponseBuilder::new(); - { - sync_builder.add_joined_room( + server + .sync_room( + &client, JoinedRoomBuilder::new(room_id) // Note to self: a timeline must have at least single event to be properly // serialized. .add_timeline_event(f.text_msg("heyo")) .set_timeline_prev_batch("prev_batch".to_owned()), - ); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } + ) + .await; let (room_event_cache, _drop_handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap(); @@ -478,25 +450,35 @@ async fn test_backpaginate_many_times_with_one_iteration() { let mut global_reached_start = false; // The first back-pagination will return these two. - mock_messages( - &server, - "prev_batch", - Some("prev_batch2"), - vec![ - f.text_msg("world").event_id(event_id!("$2")), - f.text_msg("hello").event_id(event_id!("$3")), - ], - ) - .await; + server + .mock_room_messages() + .from("prev_batch") + .ok( + "start-token-unused1".to_owned(), + Some("prev_batch2".to_owned()), + vec![ + f.text_msg("world").event_id(event_id!("$2")), + f.text_msg("hello").event_id(event_id!("$3")), + ], + Vec::new(), + ) + .mock_once() + .mount() + .await; // The second round of back-pagination will return this one. - mock_messages( - &server, - "prev_batch2", - None, - vec![f.text_msg("oh well").event_id(event_id!("$4"))], - ) - .await; + server + .mock_room_messages() + .from("prev_batch2") + .ok( + "start-token-unused2".to_owned(), + None, + vec![f.text_msg("oh well").event_id(event_id!("$4"))], + Vec::new(), + ) + .mock_once() + .mount() + .await; // Then if I backpaginate in a loop, let pagination = room_event_cache.pagination(); @@ -549,7 +531,8 @@ async fn test_backpaginate_many_times_with_one_iteration() { #[async_test] async fn test_reset_while_backpaginating() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; let event_cache = client.event_cache(); @@ -560,23 +543,18 @@ async fn test_reset_while_backpaginating() { // token, let room_id = room_id!("!omelette:fromage.fr"); - let ev_factory = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); - let mut sync_builder = SyncResponseBuilder::new(); + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); - { - sync_builder.add_joined_room( + server + .sync_room( + &client, JoinedRoomBuilder::new(room_id) // Note to self: a timeline must have at least single event to be properly // serialized. - .add_timeline_event(ev_factory.text_msg("heyo").into_raw_sync()) + .add_timeline_event(f.text_msg("heyo").into_raw_sync()) .set_timeline_prev_batch("first_backpagination".to_owned()), - ); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } + ) + .await; let (room_event_cache, _drop_handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap(); @@ -604,45 +582,37 @@ async fn test_reset_while_backpaginating() { // // The backpagination should result in an unknown-token-error. - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id) - // Note to self: a timeline must have at least single event to be properly - // serialized. - .add_timeline_event(ev_factory.text_msg("heyo").into_raw_sync()) - .set_timeline_prev_batch("second_backpagination".to_owned()) - .set_timeline_limited(), - ); - let sync_response_body = sync_builder.build_json_sync_response(); - - // Mock the first back-pagination request: - let chunk = vec![ev_factory.text_msg("lalala").into_raw_timeline()]; - let response_json = json!({ - "chunk": chunk, - "start": "t392-516_47314_0_7_1_1_1_11444_1", - }); - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .and(query_param("from", "first_backpagination")) + // Mock the first back-pagination request, with a delay. + server + .mock_room_messages() + .from("first_backpagination") .respond_with( ResponseTemplate::new(200) - .set_body_json(response_json.clone()) - .set_delay(Duration::from_millis(500)), /* This is why we don't use - * `mock_messages`. */ + .set_body_json(json!({ + "chunk": vec![f.text_msg("lalala").into_raw_timeline()], + "start": "t392-516_47314_0_7_1_1_1_11444_1", + })) + // This is why we don't use `server.mock_room_messages()`. + .set_delay(Duration::from_millis(500)), ) - .expect(1) - .mount(&server) + .mock_once() + .mount() .await; // Mock the second back-pagination request, that will be hit after the reset // caused by the sync. - mock_messages( - &server, - "second_backpagination", - Some("third_backpagination"), - vec![ev_factory.text_msg("finally!").into_raw_timeline()], - ) - .await; + server + .mock_room_messages() + .from("second_backpagination") + .ok( + "start-token-unused".to_owned(), + Some("third_backpagination".to_owned()), + vec![f.text_msg("finally!").into_raw_timeline()], + Vec::new(), + ) + .mock_once() + .mount() + .await; // Run the pagination! let pagination = room_event_cache.pagination(); @@ -667,8 +637,17 @@ async fn test_reset_while_backpaginating() { }); // Receive the sync response (which clears the timeline). - mock_sync(&server, sync_response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + // Note to self: a timeline must have at least single event to be properly + // serialized. + .add_timeline_event(f.text_msg("heyo").into_raw_sync()) + .set_timeline_prev_batch("second_backpagination".to_owned()) + .set_timeline_limited(), + ) + .await; let outcome = backpagination.await.expect("join failed").unwrap(); @@ -684,7 +663,8 @@ async fn test_reset_while_backpaginating() { #[async_test] async fn test_backpaginating_without_token() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; let event_cache = client.event_cache(); @@ -696,36 +676,24 @@ async fn test_backpaginating_without_token() { let room_id = room_id!("!omelette:fromage.fr"); let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); - let mut sync_builder = SyncResponseBuilder::new(); - - { - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } - - let (room_event_cache, _drop_handles) = - client.get_room(room_id).unwrap().event_cache().await.unwrap(); + let room = server.sync_joined_room(&client, room_id).await; + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, room_stream) = room_event_cache.subscribe().await.unwrap(); assert!(events.is_empty()); assert!(room_stream.is_empty()); - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "chunk": vec![ - f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline() - ], - "start": "t392-516_47314_0_7_1_1_1_11444_1", - }))) - .expect(1) - .mount(&server) + server + .mock_room_messages() + .ok( + "start-token-unused".to_owned(), + None, + vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()], + Vec::new(), + ) + .mock_once() + .mount() .await; // We don't have a token. @@ -748,7 +716,8 @@ async fn test_backpaginating_without_token() { #[async_test] async fn test_limited_timeline_resets_pagination() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; let event_cache = client.event_cache(); @@ -759,36 +728,25 @@ async fn test_limited_timeline_resets_pagination() { // token, let room_id = room_id!("!omelette:fromage.fr"); let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); - let mut sync_builder = SyncResponseBuilder::new(); - - { - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } + let room = server.sync_joined_room(&client, room_id).await; - let (room_event_cache, _drop_handles) = - client.get_room(room_id).unwrap().event_cache().await.unwrap(); + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, mut room_stream) = room_event_cache.subscribe().await.unwrap(); assert!(events.is_empty()); assert!(room_stream.is_empty()); - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "chunk": vec![ - f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline() - ], - "start": "t392-516_47314_0_7_1_1_1_11444_1", - }))) - .expect(1) - .mount(&server) + server + .mock_room_messages() + .ok( + "start-token-unused".to_owned(), + None, + vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()], + Vec::new(), + ) + .mock_once() + .mount() .await; // At the beginning, the paginator is in the initial state. @@ -810,14 +768,7 @@ async fn test_limited_timeline_resets_pagination() { assert!(pagination.hit_timeline_start()); // When a limited sync comes back from the server, - { - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).set_timeline_limited()); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } + server.sync_room(&client, JoinedRoomBuilder::new(room_id).set_timeline_limited()).await; // We receive an update about the limited timeline. assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = room_stream.recv()); diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index d5ed2702d3f..c2f2b03c076 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -38,10 +38,10 @@ use ruma::{ name::RoomNameEventContent, topic::RoomTopicEventContent, }, - AnyStateEvent, StateEventType, + StateEventType, }, owned_room_id, - serde::{JsonObject, Raw}, + serde::JsonObject, user_id, OwnedRoomId, }; use serde::Serialize; @@ -307,7 +307,7 @@ async fn test_read_messages_with_msgtype_capabilities() { { let start = "t392-516_47314_0_7_1_1_1_11444_1".to_owned(); let end = Some("t47409-4357353_219380_26003_2269".to_owned()); - let chun2 = vec![ + let chunk2 = vec![ f.notice("custom content").event_id(event_id!("$msda7m0df9E9op3")).into_raw_timeline(), f.text_msg("hello").event_id(event_id!("$msda7m0df9E9op5")).into_raw_timeline(), f.reaction(event_id!("$event_id"), "annotation".to_owned()).into_raw_timeline(), @@ -315,7 +315,7 @@ async fn test_read_messages_with_msgtype_capabilities() { mock_server .mock_room_messages() .limit(3) - .ok(start, end, chun2, Vec::>::new()) + .ok(start, end, chunk2, Vec::new()) .mock_once() .mount() .await; From 111f916a781f54a1a7c43d2630d704a3b43078a2 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:52:02 +0100 Subject: [PATCH 704/979] feat(WidgetDriver): Pass Matrix API errors to the widget (#4241) Currently the WidgetDriver just returns unspecified error strings to the widget that can be used to display an issue description to the user. It is not helpful to run code like a retry or other error mitigation logic. Here it is proposed to add standardized errors for issues that every widget driver implementation can run into (all matrix cs api errors): https://github.com/matrix-org/matrix-spec-proposals/pull/2762#discussion_r1838804895 This PR forwards the errors that occur during the widget processing to the widget in the correct format. NOTE: It does not include request Url and http Headers. See also: https://github.com/matrix-org/matrix-spec-proposals/pull/2762#discussion_r1839802292 Co-authored-by: Benjamin Bouvier --- crates/matrix-sdk/src/test_utils/mocks.rs | 130 ++++++++++++++ .../src/widget/machine/driver_req.rs | 4 +- .../src/widget/machine/from_widget.rs | 71 +++++++- .../matrix-sdk/src/widget/machine/incoming.rs | 7 +- crates/matrix-sdk/src/widget/machine/mod.rs | 163 +++++++++++------- .../src/widget/machine/tests/capabilities.rs | 2 +- .../src/widget/machine/tests/openid.rs | 2 +- crates/matrix-sdk/src/widget/matrix.rs | 12 +- crates/matrix-sdk/src/widget/mod.rs | 20 +-- crates/matrix-sdk/tests/integration/widget.rs | 103 ++++++++--- 10 files changed, 390 insertions(+), 124 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index f1637e20dac..9c583b95b5e 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -31,6 +31,7 @@ use ruma::{ directory::PublicRoomsChunk, events::{AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType}, serde::Raw, + time::Duration, MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName, }; use serde::Deserialize; @@ -952,6 +953,72 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { } } + /// Ensures the event was sent as a delayed event. + /// + /// Note: works with *any* room. + /// + /// # Examples + /// + /// see also [`MatrixMockServer::mock_room_send`] for more context. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{ + /// api::client::delayed_events::{delayed_message_event, DelayParameters}, + /// events::{message::MessageEventContent, AnyMessageLikeEventContent}, + /// room_id, + /// time::Duration, + /// TransactionId, + /// }, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// use serde_json::json; + /// use wiremock::ResponseTemplate; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await; + /// + /// mock_server + /// .mock_room_send() + /// .with_delay(Duration::from_millis(500)) + /// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"}))) + /// .mock_once() + /// .mount() + /// .await; + /// + /// let response_not_mocked = + /// room.send_raw("m.room.message", json!({ "body": "Hello world" })).await; + /// + /// // A non delayed event should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let r = delayed_message_event::unstable::Request::new( + /// room.room_id().to_owned(), + /// TransactionId::new(), + /// DelayParameters::Timeout { timeout: Duration::from_millis(500) }, + /// &AnyMessageLikeEventContent::Message(MessageEventContent::plain("hello world")), + /// ) + /// .unwrap(); + /// + /// let response = room.client().send(r, None).await.unwrap(); + /// // The delayed `m.room.message` event type should be mocked by the server. + /// assert_eq!("$some_id", response.delay_id); + /// # anyhow::Ok(()) }); + /// ``` + pub fn with_delay(self, delay: Duration) -> Self { + Self { + mock: self + .mock + .and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())), + ..self + } + } + /// Returns a send endpoint that emulates success, i.e. the event has been /// sent with the given event id. /// @@ -1117,6 +1184,69 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self } } + /// Ensures the event was sent as a delayed event. + /// + /// Note: works with *any* room. + /// + /// # Examples + /// + /// see also [`MatrixMockServer::mock_room_send`] for more context. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{ + /// api::client::delayed_events::{delayed_state_event, DelayParameters}, + /// events::{room::create::RoomCreateEventContent, AnyStateEventContent}, + /// room_id, + /// time::Duration, + /// }, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// use wiremock::ResponseTemplate; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await; + /// + /// mock_server + /// .mock_room_send_state() + /// .with_delay(Duration::from_millis(500)) + /// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"}))) + /// .mock_once() + /// .mount() + /// .await; + /// + /// let response_not_mocked = room.send_state_event(RoomCreateEventContent::new_v11()).await; + /// // A non delayed event should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let r = delayed_state_event::unstable::Request::new( + /// room.room_id().to_owned(), + /// "".to_owned(), + /// DelayParameters::Timeout { timeout: Duration::from_millis(500) }, + /// &AnyStateEventContent::RoomCreate(RoomCreateEventContent::new_v11()), + /// ) + /// .unwrap(); + /// let response = room.client().send(r, None).await.unwrap(); + /// // The delayed `m.room.message` event type should be mocked by the server. + /// assert_eq!("$some_id", response.delay_id); + /// + /// # anyhow::Ok(()) }); + /// ``` + pub fn with_delay(self, delay: Duration) -> Self { + Self { + mock: self + .mock + .and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())), + ..self + } + } + /// /// ``` /// # tokio_test::block_on(async { diff --git a/crates/matrix-sdk/src/widget/machine/driver_req.rs b/crates/matrix-sdk/src/widget/machine/driver_req.rs index feeba69888b..675a070696d 100644 --- a/crates/matrix-sdk/src/widget/machine/driver_req.rs +++ b/crates/matrix-sdk/src/widget/machine/driver_req.rs @@ -74,9 +74,11 @@ where Self { request_meta: None, _phantom: PhantomData } } + /// Setup a callback function that will be called once the matrix driver has + /// processed the request. pub(crate) fn then( self, - response_handler: impl FnOnce(Result, &mut WidgetMachine) -> Vec + response_handler: impl FnOnce(Result, &mut WidgetMachine) -> Vec + Send + 'static, ) { diff --git a/crates/matrix-sdk/src/widget/machine/from_widget.rs b/crates/matrix-sdk/src/widget/machine/from_widget.rs index e70d53cf7d5..ed5384631ea 100644 --- a/crates/matrix-sdk/src/widget/machine/from_widget.rs +++ b/crates/matrix-sdk/src/widget/machine/from_widget.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt; - +use as_variant::as_variant; use ruma::{ - api::client::delayed_events::{ - delayed_message_event, delayed_state_event, update_delayed_event, + api::client::{ + delayed_events::{delayed_message_event, delayed_state_event, update_delayed_event}, + error::{ErrorBody, StandardErrorBody}, }, events::{AnyTimelineEvent, MessageLikeEventType, StateEventType}, serde::Raw, @@ -25,7 +25,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use super::{SendEventRequest, UpdateDelayedEventRequest}; -use crate::widget::StateKeySelector; +use crate::{widget::StateKeySelector, Error, HttpError, RumaApiError}; #[derive(Deserialize, Debug)] #[serde(tag = "action", rename_all = "snake_case", content = "data")] @@ -41,28 +41,85 @@ pub(super) enum FromWidgetRequest { DelayedEventUpdate(UpdateDelayedEventRequest), } +/// The full response a client sends to a [`FromWidgetRequest`] in case of an +/// error. #[derive(Serialize)] pub(super) struct FromWidgetErrorResponse { error: FromWidgetError, } impl FromWidgetErrorResponse { - pub(super) fn new(e: impl fmt::Display) -> Self { - Self { error: FromWidgetError { message: e.to_string() } } + /// Create a error response to send to the widget from an http error. + pub(crate) fn from_http_error(error: HttpError) -> Self { + let message = error.to_string(); + let matrix_api_error = as_variant!(error, HttpError::Api(ruma::api::error::FromHttpResponseError::Server(RumaApiError::ClientApi(err))) => err); + + Self { + error: FromWidgetError { + message, + matrix_api_error: matrix_api_error.and_then(|api_error| match api_error.body { + ErrorBody::Standard { kind, message } => Some(FromWidgetMatrixErrorBody { + http_status: api_error.status_code.as_u16().into(), + response: StandardErrorBody { kind, message }, + }), + _ => None, + }), + }, + } + } + + /// Create a error response to send to the widget from a matrix sdk error. + pub(crate) fn from_error(error: Error) -> Self { + match error { + Error::Http(e) => FromWidgetErrorResponse::from_http_error(e), + // For UnknownError's we do not want to have the `unknown error` bit in the message. + // Hence we only convert the inner error to a string. + Error::UnknownError(e) => FromWidgetErrorResponse::from_string(e.to_string()), + _ => FromWidgetErrorResponse::from_string(error.to_string()), + } + } + + /// Create a error response to send to the widget from a string. + pub(crate) fn from_string>(error: S) -> Self { + Self { error: FromWidgetError { message: error.into(), matrix_api_error: None } } } } +/// Serializable section of an error response send by the client as a +/// response to a [`FromWidgetRequest`]. #[derive(Serialize)] struct FromWidgetError { + /// Unspecified error message text that caused this widget action to + /// fail. + /// + /// This is useful to prompt the user on an issue but cannot be used to + /// decide on how to deal with the error. message: String, + + /// Optional matrix error hinting at workarounds for specific errors. + matrix_api_error: Option, +} + +/// Serializable section of a widget response that represents a matrix error. +#[derive(Serialize)] +struct FromWidgetMatrixErrorBody { + /// Status code of the http response. + http_status: u32, + + /// Standard error response including the `errorcode` and the `error` + /// message as defined in the [spec](https://spec.matrix.org/v1.12/client-server-api/#standard-error-response). + response: StandardErrorBody, } +/// The serializable section of a widget response containing the supported +/// versions. #[derive(Serialize)] pub(super) struct SupportedApiVersionsResponse { supported_versions: Vec, } impl SupportedApiVersionsResponse { + /// The currently supported widget api versions from the rust widget driver. pub(super) fn new() -> Self { Self { supported_versions: vec![ diff --git a/crates/matrix-sdk/src/widget/machine/incoming.rs b/crates/matrix-sdk/src/widget/machine/incoming.rs index 7d165b59c99..d90f3740869 100644 --- a/crates/matrix-sdk/src/widget/machine/incoming.rs +++ b/crates/matrix-sdk/src/widget/machine/incoming.rs @@ -37,8 +37,11 @@ pub(crate) enum IncomingMessage { /// The ID of the request that this response corresponds to. request_id: Uuid, - /// The result of the request: response data or error message. - response: Result, + /// Result of the request: the response data, or a matrix sdk error. + /// + /// Http errors will be forwarded to the widget in a specified format so + /// the widget can parse the error. + response: Result, }, /// The `MatrixDriver` notified the `WidgetMachine` of a new matrix event. diff --git a/crates/matrix-sdk/src/widget/machine/mod.rs b/crates/matrix-sdk/src/widget/machine/mod.rs index ca000d37632..e367b5cc199 100644 --- a/crates/matrix-sdk/src/widget/machine/mod.rs +++ b/crates/matrix-sdk/src/widget/machine/mod.rs @@ -14,7 +14,7 @@ //! No I/O logic of the [`WidgetDriver`]. -use std::{fmt, iter, time::Duration}; +use std::{iter, time::Duration}; use driver_req::UpdateDelayedEventRequest; use from_widget::UpdateDelayedEventResponse; @@ -48,10 +48,11 @@ use self::{ #[cfg(doc)] use super::WidgetDriver; use super::{ - capabilities, + capabilities::{SEND_DELAYED_EVENT, UPDATE_DELAYED_EVENT}, filter::{MatrixEventContent, MatrixEventFilterInput}, Capabilities, StateKeySelector, }; +use crate::Result; mod driver_req; mod from_widget; @@ -212,18 +213,23 @@ impl WidgetMachine { ) -> Vec { let request = match raw_request.deserialize() { Ok(r) => r, - Err(e) => return vec![Self::send_from_widget_error_response(raw_request, e)], + Err(e) => { + return vec![Self::send_from_widget_err_response( + raw_request, + FromWidgetErrorResponse::from_error(crate::Error::SerdeJson(e)), + )] + } }; match request { FromWidgetRequest::SupportedApiVersions {} => { let response = SupportedApiVersionsResponse::new(); - vec![Self::send_from_widget_response(raw_request, response)] + vec![Self::send_from_widget_response(raw_request, Ok(response))] } FromWidgetRequest::ContentLoaded {} => { let mut response = - vec![Self::send_from_widget_response(raw_request, JsonObject::new())]; + vec![Self::send_from_widget_response(raw_request, Ok(JsonObject::new()))]; if self.capabilities.is_unset() { response.append(&mut self.negotiate_capabilities()); } @@ -256,24 +262,22 @@ impl WidgetMachine { }); let response = - Self::send_from_widget_response(raw_request, OpenIdResponse::Pending); + Self::send_from_widget_response(raw_request, Ok(OpenIdResponse::Pending)); iter::once(response).chain(request_action).collect() } FromWidgetRequest::DelayedEventUpdate(req) => { let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else { - let text = - "Received send update delayed event request before capabilities were negotiated"; - return vec![Self::send_from_widget_error_response(raw_request, text)]; + return vec![Self::send_from_widget_error_string_response( + raw_request, + "Received send update delayed event request before capabilities were negotiated" + )]; }; if !capabilities.update_delayed_event { - return vec![Self::send_from_widget_error_response( + return vec![Self::send_from_widget_error_string_response( raw_request, - format!( - "Not allowed: missing the {} capability.", - capabilities::UPDATE_DELAYED_EVENT - ), + format!("Not allowed: missing the {UPDATE_DELAYED_EVENT} capability."), )]; } @@ -282,13 +286,14 @@ impl WidgetMachine { action: req.action, delay_id: req.delay_id, }); - - request.then(|res, _machine| { - vec![Self::send_from_widget_result_response( + request.then(|result, _machine| { + vec![Self::send_from_widget_response( raw_request, // This is mapped to another type because the update_delay_event::Response // does not impl Serialize - res.map(Into::::into), + result + .map(Into::::into) + .map_err(FromWidgetErrorResponse::from_error), )] }); @@ -303,15 +308,17 @@ impl WidgetMachine { raw_request: Raw, ) -> Option { let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else { - let text = "Received read event request before capabilities were negotiated"; - return Some(Self::send_from_widget_error_response(raw_request, text)); + return Some(Self::send_from_widget_error_string_response( + raw_request, + "Received read event request before capabilities were negotiated", + )); }; match request { ReadEventRequest::ReadMessageLikeEvent { event_type, limit } => { if !capabilities.read.iter().any(|f| f.matches_message_like_event_type(&event_type)) { - return Some(Self::send_from_widget_error_response( + return Some(Self::send_from_widget_error_string_response( raw_request, "Not allowed to read message like event", )); @@ -324,18 +331,24 @@ impl WidgetMachine { let (request, action) = self.send_matrix_driver_request(request); request.then(|result, machine| { - let response = result.and_then(|mut events| { - let CapabilitiesState::Negotiated(capabilities) = &machine.capabilities - else { - let err = "Received read event request before capabilities negotiation"; - return Err(err.into()); - }; - - events.retain(|e| capabilities.raw_event_matches_read_filter(e)); - Ok(ReadEventResponse { events }) - }); + let response = match &machine.capabilities { + CapabilitiesState::Unset => Err(FromWidgetErrorResponse::from_string( + "Received read event request before capabilities negotiation", + )), + CapabilitiesState::Negotiating => { + Err(FromWidgetErrorResponse::from_string( + "Received read event request while capabilities were negotiating", + )) + } + CapabilitiesState::Negotiated(capabilities) => result + .map(|mut events| { + events.retain(|e| capabilities.raw_event_matches_read_filter(e)); + ReadEventResponse { events } + }) + .map_err(FromWidgetErrorResponse::from_error), + }; - vec![Self::send_from_widget_result_response(raw_request, response)] + vec![Self::send_from_widget_response(raw_request, response)] }); action @@ -364,12 +377,14 @@ impl WidgetMachine { let request = ReadStateEventRequest { event_type, state_key }; let (request, action) = self.send_matrix_driver_request(request); request.then(|result, _machine| { - let response = result.map(|events| ReadEventResponse { events }); - vec![Self::send_from_widget_result_response(raw_request, response)] + let response = result + .map(|events| ReadEventResponse { events }) + .map_err(FromWidgetErrorResponse::from_error); + vec![Self::send_from_widget_response(raw_request, response)] }); action } else { - Some(Self::send_from_widget_error_response( + Some(Self::send_from_widget_error_string_response( raw_request, "Not allowed to read state event", )) @@ -400,17 +415,14 @@ impl WidgetMachine { }; if !capabilities.send_delayed_event && request.delay.is_some() { - return Some(Self::send_from_widget_error_response( + return Some(Self::send_from_widget_error_string_response( raw_request, - format!( - "Not allowed: missing the {} capability.", - capabilities::SEND_DELAYED_EVENT - ), + format!("Not allowed: missing the {SEND_DELAYED_EVENT} capability."), )); } if !capabilities.send.iter().any(|filter| filter.matches(&filter_in)) { - return Some(Self::send_from_widget_error_response( + return Some(Self::send_from_widget_error_string_response( raw_request, "Not allowed to send event", )); @@ -422,7 +434,10 @@ impl WidgetMachine { if let Ok(r) = result.as_mut() { r.set_room_id(machine.room_id.clone()); } - vec![Self::send_from_widget_result_response(raw_request, result)] + vec![Self::send_from_widget_response( + raw_request, + result.map_err(FromWidgetErrorResponse::from_error), + )] }); action @@ -465,7 +480,7 @@ impl WidgetMachine { fn process_matrix_driver_response( &mut self, request_id: Uuid, - response: Result, + response: Result, ) -> Vec { match self.pending_matrix_driver_requests.extract(&request_id) { Ok(request) => request @@ -479,39 +494,55 @@ impl WidgetMachine { } } - #[instrument(skip_all)] - fn send_from_widget_response( + fn send_from_widget_error_string_response( raw_request: Raw, - response_data: impl Serialize, + error: impl Into, ) -> Action { - let f = || { - let mut object = raw_request.deserialize_as::>>()?; - let response_data = serde_json::value::to_raw_value(&response_data)?; - object.insert("response".to_owned(), response_data); - serde_json::to_string(&object) - }; - - // SAFETY: we expect the raw request to be a valid JSON map, to which we add a - // new field. - let serialized = f().expect("error when attaching response to incoming request"); - - Action::SendToWidget(serialized) + Self::send_from_widget_err_response( + raw_request, + FromWidgetErrorResponse::from_string(error), + ) } - fn send_from_widget_error_response( + fn send_from_widget_err_response( raw_request: Raw, - error: impl fmt::Display, + error: FromWidgetErrorResponse, ) -> Action { - Self::send_from_widget_response(raw_request, FromWidgetErrorResponse::new(error)) + Self::send_from_widget_response( + raw_request, + Err::(error), + ) } - fn send_from_widget_result_response( + fn send_from_widget_response( raw_request: Raw, - result: Result, + result: Result, ) -> Action { + // we do not want tho expose this to never allow sending arbitrary errors. + // Errors always need to be `FromWidgetErrorResponse`. + #[instrument(skip_all)] + fn send_response_data( + raw_request: Raw, + response_data: impl Serialize, + ) -> Action { + let f = || { + let mut object = + raw_request.deserialize_as::>>()?; + let response_data = serde_json::value::to_raw_value(&response_data)?; + object.insert("response".to_owned(), response_data); + serde_json::to_string(&object) + }; + + // SAFETY: we expect the raw request to be a valid JSON map, to which we add a + // new field. + let serialized = f().expect("error when attaching response to incoming request"); + + Action::SendToWidget(serialized) + } + match result { - Ok(res) => Self::send_from_widget_response(raw_request, res), - Err(msg) => Self::send_from_widget_error_response(raw_request, msg), + Ok(res) => send_response_data(raw_request, res), + Err(error_response) => send_response_data(raw_request, error_response), } } @@ -613,7 +644,7 @@ impl ToWidgetRequestMeta { } type MatrixDriverResponseFn = - Box, &mut WidgetMachine) -> Vec + Send>; + Box, &mut WidgetMachine) -> Vec + Send>; pub(crate) struct MatrixDriverRequestMeta { response_fn: Option, diff --git a/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs b/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs index 633d7880163..0b221a3a17f 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/capabilities.rs @@ -114,7 +114,7 @@ fn test_capabilities_failure_results_into_empty_capabilities() { machine.process(IncomingMessage::MatrixDriverResponse { request_id, - response: Err("OHMG!".into()), + response: Err(crate::Error::UnknownError("OHMG!".into())), }) }; diff --git a/crates/matrix-sdk/src/widget/machine/tests/openid.rs b/crates/matrix-sdk/src/widget/machine/tests/openid.rs index 2aef7f66963..bebd67faf99 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/openid.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/openid.rs @@ -154,7 +154,7 @@ fn test_openid_fail_results_in_response_blocked() { machine.process(IncomingMessage::MatrixDriverResponse { request_id, - response: Err("Unlucky one".into()), + response: Err(crate::Error::UnknownError("Unlucky one".into())), }) }; diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index 84e25de4c31..6365779d333 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -38,9 +38,7 @@ use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::error; use super::{machine::SendEventResponse, StateKeySelector}; -use crate::{ - event_handler::EventHandlerDropGuard, room::MessagesOptions, HttpResult, Result, Room, -}; +use crate::{event_handler::EventHandlerDropGuard, room::MessagesOptions, Error, Result, Room}; /// Thin wrapper around a [`Room`] that provides functionality relevant for /// widgets. @@ -55,9 +53,9 @@ impl MatrixDriver { } /// Requests an OpenID token for the current user. - pub(crate) async fn get_open_id(&self) -> HttpResult { + pub(crate) async fn get_open_id(&self) -> Result { let user_id = self.room.own_user_id().to_owned(); - self.room.client.send(OpenIdRequest::new(user_id), None).await + self.room.client.send(OpenIdRequest::new(user_id), None).await.map_err(Error::Http) } /// Reads the latest `limit` events of a given `event_type` from the room. @@ -172,9 +170,9 @@ impl MatrixDriver { &self, delay_id: String, action: UpdateAction, - ) -> HttpResult { + ) -> Result { let r = delayed_events::update_delayed_event::unstable::Request::new(delay_id, action); - self.room.client.send(r, None).await + self.room.client.send(r, None).await.map_err(Error::Http) } /// Starts forwarding new room events. Once the returned `EventReceiver` diff --git a/crates/matrix-sdk/src/widget/mod.rs b/crates/matrix-sdk/src/widget/mod.rs index 4f75a006b3a..c67e0f6cfee 100644 --- a/crates/matrix-sdk/src/widget/mod.rs +++ b/crates/matrix-sdk/src/widget/mod.rs @@ -29,7 +29,7 @@ use self::{ }, matrix::MatrixDriver, }; -use crate::{room::Room, HttpError, Result}; +use crate::{room::Room, Result}; mod capabilities; mod filter; @@ -202,23 +202,19 @@ impl WidgetDriver { Ok(MatrixDriverResponse::CapabilitiesAcquired(obtained)) } - MatrixDriverRequestData::GetOpenId => matrix_driver - .get_open_id() - .await - .map(MatrixDriverResponse::OpenIdReceived) - .map_err(|e| e.to_string()), + MatrixDriverRequestData::GetOpenId => { + matrix_driver.get_open_id().await.map(MatrixDriverResponse::OpenIdReceived) + } MatrixDriverRequestData::ReadMessageLikeEvent(cmd) => matrix_driver .read_message_like_events(cmd.event_type.clone(), cmd.limit) .await - .map(MatrixDriverResponse::MatrixEventRead) - .map_err(|e| e.to_string()), + .map(MatrixDriverResponse::MatrixEventRead), MatrixDriverRequestData::ReadStateEvent(cmd) => matrix_driver .read_state_events(cmd.event_type.clone(), &cmd.state_key) .await - .map(MatrixDriverResponse::MatrixEventRead) - .map_err(|e| e.to_string()), + .map(MatrixDriverResponse::MatrixEventRead), MatrixDriverRequestData::SendMatrixEvent(req) => { let SendEventRequest { event_type, state_key, content, delay } = req; @@ -233,14 +229,12 @@ impl WidgetDriver { .send(event_type, state_key, content, delay_event_parameter) .await .map(MatrixDriverResponse::MatrixEventSent) - .map_err(|e: crate::Error| e.to_string()) } MatrixDriverRequestData::UpdateDelayedEvent(req) => matrix_driver .update_delayed_event(req.delay_id, req.action) .await - .map(MatrixDriverResponse::MatrixDelayedEventUpdate) - .map_err(|e: HttpError| e.to_string()), + .map(MatrixDriverResponse::MatrixDelayedEventUpdate), }; // Forward the matrix driver response to the incoming message stream. diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index c2f2b03c076..1ed2b91dc7f 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -38,7 +38,7 @@ use ruma::{ name::RoomNameEventContent, topic::RoomTopicEventContent, }, - StateEventType, + MessageLikeEventType, StateEventType, }, owned_room_id, serde::JsonObject, @@ -48,7 +48,7 @@ use serde::Serialize; use serde_json::{json, Value as JsonValue}; use tracing::error; use wiremock::{ - matchers::{header, method, path_regex, query_param}, + matchers::{method, path_regex}, Mock, ResponseTemplate, }; @@ -245,13 +245,12 @@ async fn test_read_messages() { "end": "t47409-4357353_219380_26003_2269", "start": "t392-516_47314_0_7_1_1_1_11444_1" }); - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .and(query_param("limit", "2")) + mock_server + .mock_room_messages() + .limit(2) .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) - .expect(1) - .mount(mock_server.server()) + .mock_once() + .mount() .await; // Ask the driver to read messages @@ -512,8 +511,6 @@ async fn test_send_room_message() { assert_eq!(msg["action"], "send_event"); let event_id = msg["response"]["event_id"].as_str().unwrap(); assert_eq!(event_id, "$foobar"); - - // Make sure the event-sending endpoint was hit exactly once } #[async_test] @@ -554,8 +551,6 @@ async fn test_send_room_name() { assert_eq!(msg["action"], "send_event"); let event_id = msg["response"]["event_id"].as_str().unwrap(); assert_eq!(event_id, "$foobar"); - - // Make sure the event-sending endpoint was hit exactly once } #[async_test] @@ -570,15 +565,15 @@ async fn test_send_delayed_message_event() { ]), ) .await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*")) - .and(query_param("org.matrix.msc4140.delay", "1000")) + mock_server + .mock_room_send() + .with_delay(Duration::from_millis(1000)) + .for_type(MessageLikeEventType::RoomMessage) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "delay_id": "1234", }))) - .expect(1) - .mount(mock_server.server()) + .mock_once() + .mount() .await; send_request( @@ -602,8 +597,6 @@ async fn test_send_delayed_message_event() { assert_eq!(msg["action"], "send_event"); let delay_id = msg["response"]["delay_id"].as_str().unwrap(); assert_eq!(delay_id, "1234"); - - // Make sure the event-sending endpoint was hit exactly once } #[async_test] @@ -619,14 +612,15 @@ async fn test_send_delayed_state_event() { ) .await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/.*")) - .and(query_param("org.matrix.msc4140.delay", "1000")) + mock_server + .mock_room_send_state() + .with_delay(Duration::from_millis(1000)) + .for_type(StateEventType::RoomName) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "delay_id": "1234", }))) - .expect(1) - .mount(mock_server.server()) + .mock_once() + .mount() .await; send_request( @@ -650,8 +644,65 @@ async fn test_send_delayed_state_event() { assert_eq!(msg["action"], "send_event"); let delay_id = msg["response"]["delay_id"].as_str().unwrap(); assert_eq!(delay_id, "1234"); +} + +#[async_test] +async fn test_fail_sending_delay_rate_limit() { + let (_, mock_server, driver_handle) = run_test_driver(false).await; + + negotiate_capabilities( + &driver_handle, + json!([ + "org.matrix.msc4157.send.delayed_event", + "org.matrix.msc2762.send.event:m.room.message" + ]), + ) + .await; + + mock_server + .mock_room_send() + .respond_with(ResponseTemplate::new(400).set_body_json(json!({ + "errcode": "M_LIMIT_EXCEEDED", + "error": "Sending too many delay events" + }))) + .mock_once() + .mount() + .await; + + send_request( + &driver_handle, + "send-room-message", + "send_event", + json!({ + "type": "m.room.message", + "content": { + "msgtype": "m.text", + "body": "Message from a widget!", + }, + "delay":1000, + }), + ) + .await; - // Make sure the event-sending endpoint was hit exactly once + let msg = recv_message(&driver_handle).await; + assert_eq!(msg["api"], "fromWidget"); + assert_eq!(msg["action"], "send_event"); + // Receive the response in the correct widget error response format + assert_eq!( + msg["response"], + json!({ + "error": { + "matrix_api_error": { + "http_status": 400, + "response": { + "errcode": "M_LIMIT_EXCEEDED", + "error": "Sending too many delay events" + }, + }, + "message": "the server returned an error: [400 / M_LIMIT_EXCEEDED] Sending too many delay events" + } + }) + ); } #[async_test] From 22cb8a1878bf5e1dcc578278c3dd70bfdfb98bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 5 Dec 2024 11:50:33 +0100 Subject: [PATCH 705/979] chore: Bump the pprof version to fix a security issue --- Cargo.lock | 34 ++++++++++++++++++++++++++++++++-- benchmarks/Cargo.toml | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f06cb85e48a..f244073509b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0966165eaf052580bd70eb1b32cb3d6245774c0104d1b2793e9650bf83b52a" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -1407,6 +1416,26 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "equator" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35da53b5a021d2484a7cc49b2ac7f2d840f8236a286f84202369bd338d761ea" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf679796c0322556351f287a51b49e48f7c4986e727b5dd78c972d30e2e16cc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -4206,10 +4235,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "pprof" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5c97c51bd34c7e742402e216abdeb44d415fbe6ae41d56b114723e953711cb" +checksum = "ebbe2f8898beba44815fdc9e5a4ae9c929e21c5dc29b0c774a15555f7f58d6d0" dependencies = [ + "aligned-vec", "backtrace", "cfg-if", "criterion", diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 0fa7d70cb0d..e8de66d6622 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -23,7 +23,7 @@ tokio = { workspace = true, default-features = false, features = ["rt-multi-thre wiremock = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] -pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] } +pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] } [[bench]] name = "crypto_bench" From ee30008f38961f8a5fb4878189b3aaecbdf8f798 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Thu, 5 Dec 2024 12:37:16 +0000 Subject: [PATCH 706/979] feat: Accept any string as a key for `m.direct` account data (#4228) This is the follow up of this [Ruma PR](https://github.com/ruma/ruma/pull/1946) for the SDK. --------- Signed-off-by: Mathieu Velten Co-authored-by: Benjamin Bouvier --- Cargo.lock | 37 +++++++------------ Cargo.toml | 4 +- .../src/response_processors.rs | 12 +++--- crates/matrix-sdk-base/src/rooms/mod.rs | 3 +- crates/matrix-sdk-base/src/rooms/normal.rs | 3 +- .../matrix-sdk-base/src/sliding_sync/mod.rs | 28 +++++++------- .../src/store/migration_helpers.rs | 8 +++- crates/matrix-sdk/src/account.rs | 2 +- crates/matrix-sdk/src/encryption/mod.rs | 7 +++- crates/matrix-sdk/src/room/mod.rs | 2 +- crates/matrix-sdk/tests/integration/client.rs | 9 ++++- .../tests/integration/room/common.rs | 3 +- .../tests/integration/room/joined.rs | 35 +++++++++++++++++- .../matrix-sdk/tests/integration/room/left.rs | 12 ++++-- 14 files changed, 108 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f244073509b..c0ce6f048d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3755,7 +3755,7 @@ version = "5.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d385da3c602d29036d2f70beed71c36604df7570be17fed4c5b839616785bf" dependencies = [ - "base64 0.22.1", + "base64 0.21.7", "chrono", "getrandom", "http", @@ -4753,8 +4753,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94984418ae8a5e1160e6c87608141330e9ae26330abf22e3d15416efa96d48a" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "assign", "js_int", @@ -4771,8 +4770,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325e054db8d5545c00767d9868356d61e63f2c6cb8b54768346d66696ea4ad48" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "as_variant", "assign", @@ -4787,7 +4785,7 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror 1.0.63", + "thiserror 2.0.3", "url", "web-time", ] @@ -4795,8 +4793,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad71c7f49abaa047ba228339d34f9aaefa4d8b50ebeb8e859d0340cc2138bda8" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "as_variant", "base64 0.22.1", @@ -4816,7 +4813,7 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror 1.0.63", + "thiserror 2.0.3", "time", "tracing", "url", @@ -4828,8 +4825,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be86dccf3504588c1f4dc1bda4ce1f8bbd646fc6dda40c77cc7de6e203e62dad" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4844,7 +4840,7 @@ dependencies = [ "ruma-macros", "serde", "serde_json", - "thiserror 1.0.63", + "thiserror 2.0.3", "tracing", "url", "web-time", @@ -4854,8 +4850,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5a09ac22b3352bf7a350514dc9a87e1b56aba04c326ac9ce142740f7218afa" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "http", "js_int", @@ -4869,8 +4864,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7571886b6df90a4ed72e7481a5a39cc2a5b3a4e956e9366ad798e4e2e9fe8005" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "as_variant", "html5ever", @@ -4882,18 +4876,16 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7f9b534a65698d7db3c08d94bf91de0046fe6c7893a7b360502f65e7011ac4" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "js_int", - "thiserror 1.0.63", + "thiserror 2.0.3", ] [[package]] name = "ruma-macros" version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d57d3cb20e8e758e8f7c5e408ce831d46758003b615100099852e468631934" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "cfg-if", "once_cell", @@ -4909,8 +4901,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfced466fbed6277f74ac3887eeb96c185a09f4323dc3c39bcea04870430fe9a" +source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index 56c356e22d5..0b2fb061b6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ proptest = { version = "1.5.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } rmp-serde = "1.3.0" -ruma = { version = "0.11.1", features = [ +ruma = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60b223dbf136fc143bc8", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -70,7 +70,7 @@ ruma = { version = "0.11.1", features = [ "unstable-msc4075", "unstable-msc4140", ] } -ruma-common = "0.14.1" +ruma-common = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60b223dbf136fc143bc8" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" diff --git a/crates/matrix-sdk-base/src/response_processors.rs b/crates/matrix-sdk-base/src/response_processors.rs index a6a35a5446c..512f2bd2f10 100644 --- a/crates/matrix-sdk-base/src/response_processors.rs +++ b/crates/matrix-sdk-base/src/response_processors.rs @@ -18,9 +18,11 @@ use std::{ }; use ruma::{ - events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType}, + events::{ + direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType, + }, serde::Raw, - OwnedUserId, RoomId, + RoomId, }; use tracing::{debug, instrument, trace, warn}; @@ -94,10 +96,10 @@ impl AccountDataProcessor { for event in events { let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue }; - let mut new_dms = HashMap::<&RoomId, HashSet>::new(); - for (user_id, rooms) in direct_event.content.iter() { + let mut new_dms = HashMap::<&RoomId, HashSet>::new(); + for (user_identifier, rooms) in direct_event.content.iter() { for room_id in rooms { - new_dms.entry(room_id).or_default().insert(user_id.clone()); + new_dms.entry(room_id).or_default().insert(user_identifier.clone()); } } diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index 9d63e6a8401..d86ba3f665b 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -22,6 +22,7 @@ use ruma::{ events::{ beacon_info::BeaconInfoEventContent, call::member::{CallMemberEventContent, CallMemberStateKey}, + direct::OwnedDirectUserIdentifier, macros::EventContent, room::{ avatar::RoomAvatarEventContent, @@ -128,7 +129,7 @@ pub struct BaseRoomInfo { pub(crate) create: Option>, /// A list of user ids this room is considered as direct message, if this /// room is a DM. - pub(crate) dm_targets: HashSet, + pub(crate) dm_targets: HashSet, /// The `m.room.encryption` event content that enabled E2EE in this room. pub(crate) encryption: Option, /// The guest access policy of this room. diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 641e6a12282..91367d42675 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -33,6 +33,7 @@ use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, events::{ call::member::{CallMemberStateKey, MembershipData}, + direct::OwnedDirectUserIdentifier, ignored_user_list::IgnoredUserListEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ @@ -460,7 +461,7 @@ impl Room { /// only be considered as guidance. We leave members in this list to allow /// us to re-find a DM with a user even if they have left, since we may /// want to re-invite them. - pub fn direct_targets(&self) -> HashSet { + pub fn direct_targets(&self) -> HashSet { self.inner.read().base_info.dm_targets.clone() } diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 79f0276f8c0..3bf2669030e 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -911,7 +911,7 @@ mod tests { api::client::sync::sync_events::UnreadNotificationsCount, assign, event_id, events::{ - direct::DirectEventContent, + direct::{DirectEventContent, DirectUserIdentifier, OwnedDirectUserIdentifier}, room::{ avatar::RoomAvatarEventContent, canonical_alias::RoomCanonicalAliasEventContent, @@ -1337,7 +1337,7 @@ mod tests { create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await; // (Sanity: B is a direct target, and is in Join state) - assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))); assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join); // When B leaves @@ -1346,7 +1346,7 @@ mod tests { // Then B is still a direct target, and is in Leave state (B is a direct target // because we want to return to our old DM in the UI even if the other // user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017) - assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))); assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave); } @@ -1362,7 +1362,7 @@ mod tests { create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await; // (Sanity: B is a direct target, and is in Invite state) - assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))); assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite); // When B declines the invitation (i.e. leaves) @@ -1371,7 +1371,7 @@ mod tests { // Then B is still a direct target, and is in Leave state (B is a direct target // because we want to return to our old DM in the UI even if the other // user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017) - assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))); assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave); } @@ -1389,7 +1389,7 @@ mod tests { assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join); // (Sanity: B is a direct target, and is in Join state) - assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))); assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join); let room = client.get_room(room_id).unwrap(); @@ -1413,7 +1413,7 @@ mod tests { assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join); // (Sanity: B is a direct target, and is in Join state) - assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))); assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite); let room = client.get_room(room_id).unwrap(); @@ -2558,9 +2558,10 @@ mod tests { let mut room_response = http::response::Room::new(); set_room_joined(&mut room_response, user_a_id); let mut response = response_with_room(room_id_1, room_response); - let mut direct_content = BTreeMap::new(); - direct_content.insert(user_a_id.to_owned(), vec![room_id_1.to_owned()]); - direct_content.insert(user_b_id.to_owned(), vec![room_id_2.to_owned()]); + let mut direct_content: BTreeMap> = + BTreeMap::new(); + direct_content.insert(user_a_id.into(), vec![room_id_1.to_owned()]); + direct_content.insert(user_b_id.into(), vec![room_id_2.to_owned()]); response .extensions .account_data @@ -2671,7 +2672,7 @@ mod tests { member.membership().clone() } - fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet { + fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet { let room = client.get_room(room_id).expect("Room not found!"); room.direct_targets() } @@ -2730,8 +2731,9 @@ mod tests { user_id: OwnedUserId, room_ids: Vec, ) { - let mut direct_content = BTreeMap::new(); - direct_content.insert(user_id, room_ids); + let mut direct_content: BTreeMap> = + BTreeMap::new(); + direct_content.insert(user_id.into(), room_ids); response .extensions .account_data diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index 76acb86cf48..c8164096757 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -23,6 +23,7 @@ use std::{ use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use ruma::{ events::{ + direct::OwnedDirectUserIdentifier, room::{ avatar::RoomAvatarEventContent, canonical_alias::RoomCanonicalAliasEventContent, @@ -200,12 +201,17 @@ impl BaseRoomInfoV1 { MinimalStateEvent::Redacted(ev) => MinimalStateEvent::Redacted(ev), }); + let mut converted_dm_targets = HashSet::new(); + for dm_target in dm_targets { + converted_dm_targets.insert(OwnedDirectUserIdentifier::from(dm_target)); + } + Box::new(BaseRoomInfo { avatar, beacons: BTreeMap::new(), canonical_alias, create, - dm_targets, + dm_targets: converted_dm_targets, encryption, guest_access, history_visibility, diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 2e08bb64154..b077d610641 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -861,7 +861,7 @@ impl Account { }; for user_id in user_ids { - content.entry(user_id.to_owned()).or_default().push(room_id.to_owned()); + content.entry(user_id.into()).or_default().push(room_id.to_owned()); } // TODO: We should probably save the fact that we need to send this out diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index f8c7909940d..9a4130e4ffb 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -50,7 +50,10 @@ use ruma::{ uiaa::{AuthData, UiaaInfo}, }, assign, - events::room::{MediaSource, ThumbnailInfo}, + events::{ + direct::DirectUserIdentifier, + room::{MediaSource, ThumbnailInfo}, + }, DeviceId, OwnedDeviceId, OwnedUserId, TransactionId, UserId, }; use serde::Deserialize; @@ -605,7 +608,7 @@ impl Client { // Find the room we share with the `user_id` and only with `user_id` let room = rooms.into_iter().find(|r| { let targets = r.direct_targets(); - targets.len() == 1 && targets.contains(user_id) + targets.len() == 1 && targets.contains(<&DirectUserIdentifier>::from(user_id)) }); trace!(?room, "Found room"); diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index fcafbca63b9..8b378be0feb 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1224,7 +1224,7 @@ impl Room { room_members.retain(|member| member.user_id() != self.own_user_id()); for member in room_members { - let entry = content.entry(member.user_id().to_owned()).or_default(); + let entry = content.entry(member.user_id().into()).or_default(); if !entry.iter().any(|room_id| room_id == this_room_id) { entry.push(this_room_id.to_owned()); } diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index a8d7a9e1115..2269d0018fb 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -35,7 +35,10 @@ use ruma::{ assign, device_id, directory::Filter, event_id, - events::{direct::DirectEventContent, AnyInitialStateEvent}, + events::{ + direct::{DirectEventContent, OwnedDirectUserIdentifier}, + AnyInitialStateEvent, + }, room_id, serde::Raw, user_id, OwnedUserId, @@ -496,7 +499,9 @@ async fn test_marking_room_as_dm() { "The body of the PUT /account_data request should be a valid DirectEventContent", ); - let bob_entry = content.get(bob).expect("We should have bob in the direct event content"); + let bob_entry = content + .get(&OwnedDirectUserIdentifier::from(bob.to_owned())) + .expect("We should have bob in the direct event content"); assert_eq!(content.len(), 2, "We should have entries for bob and foo"); assert_eq!(bob_entry.len(), 3, "Bob should have 3 direct rooms"); diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index f525f497436..3021a114a8d 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -11,6 +11,7 @@ use matrix_sdk_test::{ use ruma::{ event_id, events::{ + direct::DirectUserIdentifier, room::{ avatar::{self, RoomAvatarEventContent}, member::MembershipState, @@ -830,7 +831,7 @@ async fn test_is_direct() { // The room is direct now. let direct_targets = room.direct_targets(); assert_eq!(direct_targets.len(), 1); - assert!(direct_targets.contains(*BOB)); + assert!(direct_targets.contains(<&DirectUserIdentifier>::from(*BOB))); assert!(room.is_direct().await.unwrap()); // Unset the room as direct. diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 322b0269488..fa0b3f66af2 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -22,13 +22,14 @@ use ruma::{ api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType}, assign, event_id, events::{ + direct::DirectUserIdentifier, receipt::ReceiptThread, room::message::{RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, TimelineEventType, }, int, mxc_uri, owned_event_id, room_id, thirdparty, user_id, OwnedUserId, TransactionId, }; -use serde_json::{json, Value}; +use serde_json::{from_value, json, Value}; use wiremock::{ matchers::{body_json, body_partial_json, header, method, path_regex}, Mock, ResponseTemplate, @@ -631,6 +632,38 @@ async fn test_reset_power_levels() { room.reset_power_levels().await.unwrap(); } +#[async_test] +async fn test_is_direct_invite_by_3pid() { + let (client, server) = logged_in_client_with_server().await; + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::default()); + let data = json!({ + "content": { + "invited@localhost.com": [*DEFAULT_TEST_ROOM_ID], + }, + "event_id": "$757957878228ekrDs:localhost", + "origin_server_ts": 17195787, + "sender": "@example:localhost", + "state_key": "", + "type": "m.direct", + "unsigned": { + "age": 139298 + } + }); + sync_builder.add_global_account_data_bulk(vec![from_value(data).unwrap()]); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + assert!(room.is_direct().await.unwrap()); + assert!(room.direct_targets().contains(<&DirectUserIdentifier>::from("invited@localhost.com"))); +} + #[async_test] async fn test_call_notifications_ring_for_dms() { let (client, server) = logged_in_client_with_server().await; diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs index f2de95649b5..7bad0b2377b 100644 --- a/crates/matrix-sdk/tests/integration/room/left.rs +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -7,7 +7,10 @@ use matrix_sdk_test::{ async_test, test_json, GlobalAccountDataTestEvent, LeftRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, }; -use ruma::{events::direct::DirectEventContent, user_id, OwnedRoomOrAliasId}; +use ruma::{ + events::direct::{DirectEventContent, DirectUserIdentifier}, + user_id, OwnedRoomOrAliasId, +}; use serde_json::json; use wiremock::{ matchers::{header, method, path, path_regex}, @@ -70,7 +73,7 @@ async fn test_forget_direct_room() { let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); assert_eq!(room.state(), RoomState::Left); assert!(room.is_direct().await.unwrap()); - assert!(room.direct_targets().contains(invited_user_id)); + assert!(room.direct_targets().contains(<&DirectUserIdentifier>::from(invited_user_id))); let direct_account_data = client .account() @@ -80,7 +83,10 @@ async fn test_forget_direct_room() { .expect("no m.direct account data") .deserialize() .expect("failed to deserialize m.direct account data"); - assert_matches!(direct_account_data.get(invited_user_id), Some(invited_user_dms)); + assert_matches!( + direct_account_data.get(<&DirectUserIdentifier>::from(invited_user_id)), + Some(invited_user_dms) + ); assert_eq!(invited_user_dms, &[DEFAULT_TEST_ROOM_ID.to_owned()]); Mock::given(method("POST")) From 6501a44e6ad2c04ff94dcc8c951796953279aaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 21 Nov 2024 13:22:53 +0100 Subject: [PATCH 707/979] feat: Add support for MSC4171 Introduce support for MSC4171, enabling the designation of certain users as service members. These flagged users are excluded from the room display name calculation. MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4171 --- Cargo.lock | 18 +- Cargo.toml | 7 +- crates/matrix-sdk-base/CHANGELOG.md | 8 + crates/matrix-sdk-base/src/rooms/normal.rs | 170 +++++++++++++++++-- testing/matrix-sdk-test/src/event_factory.rs | 34 +++- 5 files changed, 214 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0ce6f048d8..a44bc67e1c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4753,7 +4753,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.11.1" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "assign", "js_int", @@ -4770,7 +4770,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.19.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "assign", @@ -4793,7 +4793,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.14.1" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "base64 0.22.1", @@ -4825,7 +4825,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.29.1" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4850,7 +4850,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.10.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "http", "js_int", @@ -4864,7 +4864,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.3.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "as_variant", "html5ever", @@ -4876,7 +4876,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "js_int", "thiserror 2.0.3", @@ -4885,7 +4885,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.14.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "cfg-if", "once_cell", @@ -4901,7 +4901,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.10.0" -source = "git+https://github.com/ruma/ruma.git?rev=35fda7f2f7811156df2e60b223dbf136fc143bc8#35fda7f2f7811156df2e60b223dbf136fc143bc8" +source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index 0b2fb061b6d..79ae1c835cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ default-members = ["benchmarks", "crates/*", "labs/*"] resolver = "2" [workspace.package] -rust-version = "1.80" +rust-version = "1.82" [workspace.dependencies] anyhow = "1.0.93" @@ -56,7 +56,7 @@ proptest = { version = "1.5.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60b223dbf136fc143bc8", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "c91499fc464adc865a7c99d0ce0b35982ad96711", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -69,8 +69,9 @@ ruma = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60 "unstable-msc3489", "unstable-msc4075", "unstable-msc4140", + "unstable-msc4171", ] } -ruma-common = { git = "https://github.com/ruma/ruma.git", rev = "35fda7f2f7811156df2e60b223dbf136fc143bc8" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "c91499fc464adc865a7c99d0ce0b35982ad96711" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 2e596658ffc..862d8a3fa0b 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Features + +- Introduced support for + [MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling + the designation of certain users as service members. These flagged users are + excluded from the room display name calculation. + ([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335)) + ### Bug Fixes - Fix an off-by-one error in the `ObservableMap` when the `remove()` method is diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 91367d42675..1cdfa7dba21 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -20,6 +20,7 @@ use std::{ sync::{atomic::AtomicBool, Arc}, }; +use as_variant::as_variant; use bitflags::bitflags; use eyeball::{SharedObservable, Subscriber}; use futures_util::{Stream, StreamExt}; @@ -35,6 +36,7 @@ use ruma::{ call::member::{CallMemberStateKey, MembershipData}, direct::OwnedDirectUserIdentifier, ignored_user_list::IgnoredUserListEventContent, + member_hints::MemberHintsEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ avatar::{self, RoomAvatarEventContent}, @@ -50,7 +52,7 @@ use ruma::{ }, tag::{TagEventContent, Tags}, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, - RoomAccountDataEventType, + RoomAccountDataEventType, SyncStateEvent, }, room::RoomType, serde::Raw, @@ -59,7 +61,7 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; -use tracing::{debug, field::debug, info, instrument, warn}; +use tracing::{debug, field::debug, info, instrument, trace, warn}; use super::{ members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName, @@ -68,7 +70,9 @@ use super::{ #[cfg(feature = "experimental-sliding-sync")] use crate::latest_event::LatestEvent; use crate::{ - deserialized_responses::{DisplayName, MemberEvent, RawSyncOrStrippedState}, + deserialized_responses::{ + DisplayName, MemberEvent, RawSyncOrStrippedState, SyncOrStrippedState, + }, notification_settings::RoomNotificationMode, read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, @@ -222,6 +226,18 @@ impl From<&MembershipState> for RoomState { /// try to behave similarly here. const NUM_HEROES: usize = 5; +/// A filter to remove our own user and the users specified in the member hints +/// state event, so called service members, from the list of heroes. +/// +/// The heroes will then be used to calculate a display name for the room if one +/// wasn't explicitly defined. +fn heroes_filter<'a>( + own_user_id: &'a UserId, + member_hints: &'a MemberHintsEventContent, +) -> impl Fn(&UserId) -> bool + use<'a> { + move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id) +} + impl Room { /// The size of the latest_encrypted_events RingBuffer // SAFETY: `new_unchecked` is safe because 10 is not zero. @@ -678,12 +694,16 @@ impl Room { /// /// Returns the display names as a list of strings. async fn extract_heroes(&self, heroes: &[RoomHero]) -> StoreResult> { - let own_user_id = self.own_user_id().as_str(); - let mut names = Vec::with_capacity(heroes.len()); - let heroes = heroes.iter().filter(|hero| hero.user_id != own_user_id); + let own_user_id = self.own_user_id(); + let member_hints = self.get_member_hints().await?; + + // Construct a filter that is specific to this own user id, set of member hints, + // and accepts a `RoomHero` type. + let heroes_filter = heroes_filter(own_user_id, &member_hints); + let heroes_filter = |hero: &&RoomHero| heroes_filter(&hero.user_id); - for hero in heroes { + for hero in heroes.iter().filter(heroes_filter) { if let Some(display_name) = &hero.display_name { names.push(display_name.clone()); } else { @@ -710,12 +730,30 @@ impl Room { /// /// Returns a `(heroes_names, num_joined_invited)` tuple. async fn compute_summary(&self) -> StoreResult<(Vec, u64)> { + let member_hints = self.get_member_hints().await?; + + // Construct a filter that is specific to this own user id, set of member hints, + // and accepts a `RoomMember` type. + let heroes_filter = heroes_filter(&self.own_user_id, &member_hints); + let heroes_filter = |u: &RoomMember| heroes_filter(u.user_id()); + let mut members = self.members(RoomMemberships::JOIN | RoomMemberships::INVITE).await?; + // If we have some service members, they shouldn't count to the number of + // joined/invited members, otherwise we'll wrongly assume that there are more + // members in the room than they are for the "Bob and 2 others" case. + let num_service_members = members + .iter() + .filter(|member| member_hints.service_members.contains(member.user_id())) + .count(); + // We can make a good prediction of the total number of joined and invited // members here. This might be incorrect if the database info is // outdated. - let num_joined_invited = members.len() as u64; + // + // Note: Subtracting here is fine because `num_service_members` is a subset of + // `members.len()` due to the above filter operation. + let num_joined_invited = members.len() - num_service_members; if num_joined_invited == 0 || (num_joined_invited == 1 && members[0].user_id() == self.own_user_id) @@ -729,12 +767,34 @@ impl Room { let heroes = members .into_iter() - .filter(|u| u.user_id() != self.own_user_id) + .filter(heroes_filter) .take(NUM_HEROES) .map(|u| u.name().to_owned()) .collect(); - Ok((heroes, num_joined_invited)) + trace!( + ?heroes, + num_joined_invited, + num_service_members, + "Computed a room summary since we didn't receive one." + ); + + Ok((heroes, num_joined_invited as u64)) + } + + async fn get_member_hints(&self) -> StoreResult { + Ok(self + .store + .get_state_event_static::(self.room_id()) + .await? + .and_then(|event| { + event + .deserialize() + .inspect_err(|e| warn!("Couldn't deserialize the member hints event: {e}")) + .ok() + }) + .and_then(|event| as_variant!(event, SyncOrStrippedState::Sync(SyncStateEvent::Original(e)) => e.content)) + .unwrap_or_default()) } /// Returns the cached computed display name, if available. @@ -1854,6 +1914,7 @@ fn compute_display_name_from_heroes( #[cfg(test)] mod tests { use std::{ + collections::BTreeSet, ops::{Not, Sub}, str::FromStr, sync::Arc, @@ -1894,6 +1955,7 @@ mod tests { OwnedEventId, OwnedUserId, UserId, }; use serde_json::json; + use similar_asserts::assert_eq; use stream_assert::{assert_pending, assert_ready}; use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo}; @@ -2546,6 +2608,53 @@ mod tests { ); } + #[async_test] + async fn test_display_name_dm_joined_service_members() { + let (store, room) = make_room_test_helper(RoomState::Joined); + let room_id = room_id!("!test:localhost"); + + let matthew = user_id!("@sahasrhala:example.org"); + let me = user_id!("@me:example.org"); + let bot = user_id!("@bot:example.org"); + + let mut changes = StateChanges::new("".to_owned()); + let summary = assign!(RumaSummary::new(), { + joined_member_count: Some(2u32.into()), + heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()], + }); + + let f = EventFactory::new().room(room_id!("!test:localhost")); + + let members = changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::RoomMember) + .or_default(); + members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw()); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); + members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw()); + + let member_hints_content = + f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw(); + changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::MemberHints) + .or_default() + .insert("".to_owned(), member_hints_content); + + store.save_changes(&changes).await.unwrap(); + + room.inner.update_if(|info| info.update_from_ruma_summary(&summary)); + // Bot should not contribute to the display name. + assert_eq!( + room.compute_display_name().await.unwrap(), + RoomDisplayName::Calculated("Matthew".to_owned()) + ); + } + #[async_test] async fn test_display_name_dm_joined_no_heroes() { let (store, room) = make_room_test_helper(RoomState::Joined); @@ -2573,6 +2682,47 @@ mod tests { ); } + #[async_test] + async fn test_display_name_dm_joined_no_heroes_service_members() { + let (store, room) = make_room_test_helper(RoomState::Joined); + let room_id = room_id!("!test:localhost"); + + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let bot = user_id!("@bot:example.org"); + + let mut changes = StateChanges::new("".to_owned()); + + let f = EventFactory::new().room(room_id!("!test:localhost")); + + let members = changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::RoomMember) + .or_default(); + members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw()); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); + members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw()); + + let member_hints_content = + f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw(); + changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::MemberHints) + .or_default() + .insert("".to_owned(), member_hints_content); + + store.save_changes(&changes).await.unwrap(); + + assert_eq!( + room.compute_display_name().await.unwrap(), + RoomDisplayName::Calculated("Matthew".to_owned()) + ); + } + #[async_test] async fn test_display_name_deterministic() { let (store, room) = make_room_test_helper(RoomState::Joined); diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index 1055215a029..82c25b427aa 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -14,7 +14,10 @@ #![allow(missing_docs)] -use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; +use std::{ + collections::BTreeSet, + sync::atomic::{AtomicU64, Ordering::SeqCst}, +}; use as_variant::as_variant; use matrix_sdk_common::deserialized_responses::{ @@ -22,6 +25,7 @@ use matrix_sdk_common::deserialized_responses::{ }; use ruma::{ events::{ + member_hints::MemberHintsEventContent, message::TextContentBlock, poll::{ end::PollEndEventContent, @@ -399,6 +403,34 @@ impl EventFactory { event } + /// Create a new `m.member_hints` event with the given service members. + /// + /// ``` + /// use std::collections::BTreeSet; + /// + /// use matrix_sdk_test::event_factory::EventFactory; + /// use ruma::{ + /// events::{member_hints::MemberHintsEventContent, SyncStateEvent}, + /// owned_user_id, room_id, + /// serde::Raw, + /// user_id, + /// }; + /// + /// let factory = EventFactory::new().room(room_id!("!test:localhost")); + /// + /// let event: Raw> = factory + /// .member_hints(BTreeSet::from([owned_user_id!("@alice:localhost")])) + /// .sender(user_id!("@alice:localhost")) + /// .into_raw(); + /// ``` + pub fn member_hints( + &self, + service_members: BTreeSet, + ) -> EventBuilder { + // The `m.member_hints` event always has an empty state key, so let's set it. + self.event(MemberHintsEventContent::new(service_members)).state_key("") + } + /// Create a new plain/html `m.room.message`. pub fn text_html( &self, From bf6fa4cd5533de3d59ed3bc7714123961dbd8d31 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 6 Dec 2024 12:37:34 +0100 Subject: [PATCH 708/979] fix(event cache): don't fill initial items if the room already had events (#4381) The test requires subtle conditions to trigger: - initialize a timeline from a room-list-service's room - start a backpagination with that timeline (so the room event cache's paginator is busy) - try to initialize another timeline with the same room-list-service's room (e.g. because the first room has been closed, and the app using it doesn't have a room cache) This would fail, because initializing a timeline calls `EventCache::add_initial_events()` all the time, which tries to reset the paginator's state, which assumes the paginator's not paginating at this point. In a soon future, we'll get rid of the `add_initial_events()` function because the event cache will handle its own persistent storage; in the meantime, a correct fix is to skip `add_initial_events()` if there was already something in the linked chunk. After all, we're likely to fill the initial events with the same events all the time, or a subset of more recent events. By doing that, we're likely keeping *more* events in the linked chunk, instead. Thanks to @stefanceriu for reporting the issue and confirming the fix works! --- .../tests/integration/room_list_service.rs | 89 ++++++++++++++++++- crates/matrix-sdk/src/event_cache/mod.rs | 6 ++ 2 files changed, 92 insertions(+), 3 deletions(-) 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 40c448aab83..c9e8374c16b 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -8,11 +8,16 @@ use eyeball_im::VectorDiff; use futures_util::{pin_mut, FutureExt, StreamExt}; use matrix_sdk::{ config::RequestConfig, - test_utils::{logged_in_client_with_server, set_client_session, test_client_builder}, + test_utils::{ + logged_in_client_with_server, mocks::MatrixMockServer, set_client_session, + test_client_builder, + }, Client, }; use matrix_sdk_base::sync::UnreadNotificationsCount; -use matrix_sdk_test::{async_test, mocks::mock_encryption_state}; +use matrix_sdk_test::{ + async_test, event_factory::EventFactory, mocks::mock_encryption_state, ALICE, +}; use matrix_sdk_ui::{ room_list_service::{ filters::{new_filter_fuzzy_match_room_name, new_filter_non_left, new_filter_none}, @@ -28,7 +33,7 @@ use ruma::{ use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; use tempfile::TempDir; -use tokio::{spawn, sync::mpsc::channel, task::yield_now}; +use tokio::{spawn, sync::mpsc::channel, task::yield_now, time::sleep}; use wiremock::{ matchers::{header, method, path}, Mock, MockServer, ResponseTemplate, @@ -2795,3 +2800,81 @@ async fn test_sync_indicator() -> Result<(), Error> { Ok(()) } + +#[async_test] +async fn test_multiple_timeline_init() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_list = RoomListService::new(client.clone()).await.unwrap(); + + let sync = room_list.sync(); + pin_mut!(sync); + + let room_id = room_id!("!r0:bar.org"); + + let mock_server = server.server(); + sync_then_assert_request_and_fake_response! { + [mock_server, room_list, sync] + assert request >= {}, + respond with = { + "pos": "0", + "lists": { + ALL_ROOMS: { + "count": 2, + }, + }, + "rooms": { + room_id: { + "initial": true, + "timeline": [ + timeline_event!("$x0:bar.org" at 0 sec), + ], + "prev_batch": "prev-batch-token" + }, + }, + }, + }; + + server.mock_room_state_encryption().plain().mount().await; + + let f = EventFactory::new().room(room_id).sender(*ALICE); + + // Send back-pagination responses with a small delay. + server + .mock_room_messages() + .respond_with( + ResponseTemplate::new(200) + .set_body_json(json!({ + "start": "unused-start", + "end": null, + "chunk": vec![f.text_msg("hello").into_raw_timeline()], + "state": [], + })) + .set_delay(Duration::from_millis(500)), + ) + .mount() + .await; + + let task = { + // Get a RoomListService::Room, initialize the timeline, start a pagination. + let room = room_list.room(room_id).unwrap(); + + let builder = room.default_room_timeline_builder().await.unwrap(); + room.init_timeline_with_builder(builder).await.unwrap(); + + let timeline = room.timeline().unwrap(); + + spawn(async move { timeline.paginate_backwards(20).await }) + }; + + // Rinse and repeat. + let room = room_list.room(room_id).unwrap(); + + // Let the pagination start in the other timeline, and quickly abort it. + sleep(Duration::from_millis(200)).await; + task.abort(); + + // A new timeline for the same room can still be constructed. + let builder = room.default_room_timeline_builder().await.unwrap(); + room.init_timeline_with_builder(builder).await.unwrap(); +} diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 089d42eb46b..205f9d2e1d3 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -319,6 +319,12 @@ impl EventCache { let room_cache = self.inner.for_room(room_id).await?; + // If the linked chunked already has at least one chunk (gap or events), ignore + // this request, as it should happen at most once per room. + if room_cache.inner.state.read().await.events().chunks().next().is_some() { + return Ok(()); + } + // We could have received events during a previous sync; remove them all, since // we can't know where to insert the "initial events" with respect to // them. From a277e6d37f732a58c07001d1b496ecc3cde3d18d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 7 Dec 2024 11:50:29 +0100 Subject: [PATCH 709/979] chore(xtask): Disable unexpected_cfgs lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is triggered by the `xshell::cmd!` macro, and is fixed in xshell 0.2.7, which we cannot upgrade to. Signed-off-by: Kévin Commaille --- xtask/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 0534e008d95..f86012f9b22 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,3 +1,5 @@ +#![allow(unexpected_cfgs)] + mod ci; mod fixup; mod kotlin; From 42193f1b069211138401e4f17e76722686c884f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 7 Dec 2024 11:51:18 +0100 Subject: [PATCH 710/979] chore(xtask): Remove unnecessary lifetime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `const` variables are always `'static`. Detected by clippy. Signed-off-by: Kévin Commaille --- xtask/src/release.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtask/src/release.rs b/xtask/src/release.rs index e54726c7897..093c831a23a 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -127,7 +127,7 @@ fn publish(execute: bool) -> Result<()> { } fn weekly_report() -> Result<()> { - const JSON_FIELDS: &'static str = "title,number,url,author"; + const JSON_FIELDS: &str = "title,number,url,author"; let sh = sh(); From 3bd57d430718632311398828c8c7de17e1a96d4e Mon Sep 17 00:00:00 2001 From: torrybr <16907963+torrybr@users.noreply.github.com> Date: Wed, 27 Nov 2024 07:52:50 -0500 Subject: [PATCH 711/979] feat(sdk): support for observing m.beacon events --- crates/matrix-sdk/src/lib.rs | 1 + crates/matrix-sdk/src/live_location_share.rs | 83 +++++++ crates/matrix-sdk/src/room/mod.rs | 20 +- .../tests/integration/room/beacon/mod.rs | 227 +++++++++++++++++- 4 files changed, 323 insertions(+), 8 deletions(-) create mode 100644 crates/matrix-sdk/src/live_location_share.rs diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 0c7864d3abe..6d74698f71a 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -94,6 +94,7 @@ pub use sliding_sync::{ #[cfg(feature = "uniffi")] uniffi::setup_scaffolding!(); +pub mod live_location_share; #[cfg(any(test, feature = "testing"))] pub mod test_utils; diff --git a/crates/matrix-sdk/src/live_location_share.rs b/crates/matrix-sdk/src/live_location_share.rs new file mode 100644 index 00000000000..f2d3e4fa097 --- /dev/null +++ b/crates/matrix-sdk/src/live_location_share.rs @@ -0,0 +1,83 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types for live location sharing. +//! +//! Live location sharing allows users to share their real-time location with +//! others in a room via [MSC3489](https://github.com/matrix-org/matrix-spec-proposals/pull/3489). +use async_stream::stream; +use futures_util::Stream; +use ruma::{ + events::{ + beacon::OriginalSyncBeaconEvent, beacon_info::BeaconInfoEventContent, + location::LocationContent, + }, + MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, +}; + +use crate::{event_handler::ObservableEventHandler, Client, Room}; + +/// An observable live location. +#[derive(Debug)] +pub struct ObservableLiveLocation { + observable_room_events: ObservableEventHandler<(OriginalSyncBeaconEvent, Room)>, +} + +impl ObservableLiveLocation { + /// Create a new `ObservableLiveLocation` for a particular room. + pub fn new(client: &Client, room_id: &RoomId) -> Self { + Self { observable_room_events: client.observe_room_events(room_id) } + } + + /// Get a stream of [`LiveLocationShare`]. + pub fn subscribe(&self) -> impl Stream { + let stream = self.observable_room_events.subscribe(); + stream! { + for await (event, room) in stream { + yield LiveLocationShare { + last_location: LastLocation { + location: event.content.location, + ts: event.origin_server_ts, + }, + beacon_info: room + .get_user_beacon_info(&event.sender) + .await + .ok() + .map(|info| info.content), + user_id: event.sender, + }; + } + } + } +} + +/// Details of the last known location beacon. +#[derive(Clone, Debug)] +pub struct LastLocation { + /// The most recent location content of the user. + pub location: LocationContent, + /// The timestamp of when the location was updated. + pub ts: MilliSecondsSinceUnixEpoch, +} + +/// Details of a users live location share. +#[derive(Clone, Debug)] +pub struct LiveLocationShare { + /// The user's last known location. + pub last_location: LastLocation, + /// Information about the associated beacon event. + pub beacon_info: Option, + /// The user ID of the person sharing their live location. + pub user_id: OwnedUserId, +} diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 8b378be0feb..a5908d29897 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -135,6 +135,7 @@ use crate::{ error::{BeaconError, WrongRoomState}, event_cache::{self, EventCacheDropHandles, RoomEventCache}, event_handler::{EventHandler, EventHandlerDropGuard, EventHandlerHandle, SyncEvent}, + live_location_share::ObservableLiveLocation, media::{MediaFormat, MediaRequestParameters}, notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode}, room::power_levels::{RoomPowerLevelChanges, RoomPowerLevelsExt}, @@ -3013,17 +3014,18 @@ impl Room { Ok(()) } - /// Get the beacon information event in the room for the current user. + /// Get the beacon information event in the room for the `user_id`. /// /// # Errors /// /// Returns an error if the event is redacted, stripped, not found or could /// not be deserialized. - async fn get_user_beacon_info( + pub(crate) async fn get_user_beacon_info( &self, + user_id: &UserId, ) -> Result, BeaconError> { let raw_event = self - .get_state_event_static_for_key::(self.own_user_id()) + .get_state_event_static_for_key::(user_id) .await? .ok_or(BeaconError::NotFound)?; @@ -3076,7 +3078,7 @@ impl Room { ) -> Result { self.ensure_room_joined()?; - let mut beacon_info_event = self.get_user_beacon_info().await?; + let mut beacon_info_event = self.get_user_beacon_info(self.own_user_id()).await?; beacon_info_event.content.stop(); Ok(self.send_state_event_for_key(self.own_user_id(), beacon_info_event.content).await?) } @@ -3098,7 +3100,7 @@ impl Room { ) -> Result { self.ensure_room_joined()?; - let beacon_info_event = self.get_user_beacon_info().await?; + let beacon_info_event = self.get_user_beacon_info(self.own_user_id()).await?; if beacon_info_event.content.is_live() { let content = BeaconEventContent::new(beacon_info_event.event_id, geo_uri, None); @@ -3189,6 +3191,14 @@ impl Room { }, } } + + /// Observe live location sharing events for this room. + /// + /// The returned observable will receive the newest event for each sync + /// response that contains an `m.beacon` event. + pub fn observe_live_location_shares(&self) -> ObservableLiveLocation { + ObservableLiveLocation::new(&self.client, self.room_id()) + } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] diff --git a/crates/matrix-sdk/tests/integration/room/beacon/mod.rs b/crates/matrix-sdk/tests/integration/room/beacon/mod.rs index d83bfda7c70..2f3403087a4 100644 --- a/crates/matrix-sdk/tests/integration/room/beacon/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/beacon/mod.rs @@ -1,8 +1,13 @@ use std::time::{Duration, UNIX_EPOCH}; -use matrix_sdk::config::SyncSettings; -use matrix_sdk_test::{async_test, mocks::mock_encryption_state, test_json, DEFAULT_TEST_ROOM_ID}; -use ruma::{event_id, time::SystemTime}; +use futures_util::{pin_mut, StreamExt as _}; +use js_int::uint; +use matrix_sdk::{config::SyncSettings, live_location_share::LiveLocationShare}; +use matrix_sdk_test::{ + async_test, mocks::mock_encryption_state, sync_timeline_event, test_json, JoinedRoomBuilder, + SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, +}; +use ruma::{event_id, events::location::AssetType, time::SystemTime, MilliSecondsSinceUnixEpoch}; use serde_json::json; use wiremock::{ matchers::{body_partial_json, header, method, path_regex}, @@ -153,3 +158,219 @@ async fn test_send_location_beacon_with_expired_live_share() { assert!(response.is_err()); } + +#[async_test] +async fn test_most_recent_event_in_stream() { + let (client, server) = logged_in_client_with_server().await; + + let mut sync_builder = SyncResponseBuilder::new(); + + let current_time = MilliSecondsSinceUnixEpoch::now(); + let millis_time = current_time + .to_system_time() + .unwrap() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64; + + mock_sync( + &server, + json!({ + "next_batch": "s526_47314_0_7_1_1_1_1_1", + "rooms": { + "join": { + *DEFAULT_TEST_ROOM_ID: { + "state": { + "events": [ + { + "content": { + "description": "Live Share", + "live": true, + "org.matrix.msc3488.ts": millis_time, + "timeout": 3000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$15139375514XsgmR:localhost", + "origin_server_ts": millis_time, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "org.matrix.msc3672.beacon_info", + "unsigned": { + "age": 7034220 + } + }, + ] + } + } + } + } + + }), + None, + ) + .await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(*DEFAULT_TEST_ROOM_ID).unwrap(); + + let observable_live_location_shares = room.observe_live_location_shares(); + let stream = observable_live_location_shares.subscribe(); + pin_mut!(stream); + + let mut timeline_events = Vec::new(); + + for nth in 0..25 { + timeline_events.push(sync_timeline_event!({ + "content": { + "m.relates_to": { + "event_id": "$15139375514XsgmR:localhost", + "rel_type": "m.reference" + }, + "org.matrix.msc3488.location": { + "uri": format!("geo:{nth}.9575274619722,12.494122581370175;u={nth}") + }, + "org.matrix.msc3488.ts": 1_636_829_458 + }, + "event_id": format!("$event_for_stream_{nth}"), + "origin_server_ts": 1_636_829_458, + "sender": "@example:localhost", + "type": "org.matrix.msc3672.beacon", + "unsigned": { + "age": 598971 + } + })); + } + + sync_builder.add_joined_room( + JoinedRoomBuilder::new(*DEFAULT_TEST_ROOM_ID).add_timeline_bulk(timeline_events), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + // Stream should only process the latest beacon event for the user, ignoring any + // previous events. + let LiveLocationShare { user_id, last_location, beacon_info } = + stream.next().await.expect("Another live location was expected"); + + assert_eq!(user_id.to_string(), "@example:localhost"); + + assert_eq!(last_location.location.uri, "geo:24.9575274619722,12.494122581370175;u=24"); + + assert!(last_location.location.description.is_none()); + assert!(last_location.location.zoom_level.is_none()); + assert_eq!(last_location.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458))); + + let beacon_info = beacon_info.expect("Live location share is missing the beacon_info"); + + assert!(beacon_info.live); + assert!(beacon_info.is_live()); + assert_eq!(beacon_info.description, Some("Live Share".to_owned())); + assert_eq!(beacon_info.timeout, Duration::from_millis(3000)); + assert_eq!(beacon_info.ts, current_time); + assert_eq!(beacon_info.asset.type_, AssetType::Self_); +} + +#[async_test] +async fn test_observe_single_live_location_share() { + let (client, server) = logged_in_client_with_server().await; + + let current_time = MilliSecondsSinceUnixEpoch::now(); + let millis_time = current_time + .to_system_time() + .unwrap() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64; + + mock_sync( + &server, + json!({ + "next_batch": "s526_47314_0_7_1_1_1_1_1", + "rooms": { + "join": { + *DEFAULT_TEST_ROOM_ID: { + "state": { + "events": [ + { + "content": { + "description": "Test Live Share", + "live": true, + "org.matrix.msc3488.ts": millis_time, + "timeout": 3000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$test_beacon_info", + "origin_server_ts": millis_time, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "org.matrix.msc3672.beacon_info", + } + ] + } + } + } + } + }), + None, + ) + .await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(*DEFAULT_TEST_ROOM_ID).unwrap(); + let observable_live_location_shares = room.observe_live_location_shares(); + let stream = observable_live_location_shares.subscribe(); + pin_mut!(stream); + + let timeline_event = sync_timeline_event!({ + "content": { + "m.relates_to": { + "event_id": "$test_beacon_info", + "rel_type": "m.reference" + }, + "org.matrix.msc3488.location": { + "uri": "geo:10.000000,20.000000;u=5" + }, + "org.matrix.msc3488.ts": 1_636_829_458 + }, + "event_id": "$location_event", + "origin_server_ts": millis_time, + "sender": "@example:localhost", + "type": "org.matrix.msc3672.beacon", + }); + + mock_sync( + &server, + SyncResponseBuilder::new() + .add_joined_room( + JoinedRoomBuilder::new(*DEFAULT_TEST_ROOM_ID).add_timeline_event(timeline_event), + ) + .build_json_sync_response(), + None, + ) + .await; + + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let LiveLocationShare { user_id, last_location, beacon_info } = + stream.next().await.expect("Another live location was expected"); + + assert_eq!(user_id.to_string(), "@example:localhost"); + assert_eq!(last_location.location.uri, "geo:10.000000,20.000000;u=5"); + assert_eq!(last_location.ts, current_time); + + let beacon_info = beacon_info.expect("Live location share is missing the beacon_info"); + + assert!(beacon_info.live); + assert!(beacon_info.is_live()); + assert_eq!(beacon_info.description, Some("Test Live Share".to_owned())); + assert_eq!(beacon_info.timeout, Duration::from_millis(3000)); + assert_eq!(beacon_info.ts, current_time); +} From d8184e72eb85a244fbd5fc42a78543abe3842c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:43:49 +0100 Subject: [PATCH 712/979] fix(media): Make sure that local MXC URIs only try to get media from the cache and ignore requested dimensions (#4387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted from #4329. This does not change the `MediaFormat` of the request used in the media cache by the send queue. --------- Signed-off-by: Kévin Commaille --- .../event_cache/store/integration_tests.rs | 28 ++++ .../src/event_cache/store/memory_store.rs | 11 ++ .../src/event_cache/store/traits.rs | 24 +++ .../src/event_cache_store.rs | 29 ++++ crates/matrix-sdk/src/media.rs | 157 +++++++++++++++++- crates/matrix-sdk/src/send_queue/upload.rs | 65 ++------ .../tests/integration/send_queue.rs | 26 ++- 7 files changed, 283 insertions(+), 57 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index 388490c98ac..ad484c56985 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -163,6 +163,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { Some(&content), "media not found though added" ); + assert_eq!( + self.get_media_content_for_uri(uri).await.unwrap().as_ref(), + Some(&content), + "media not found by URI though added" + ); // Let's remove the media. self.remove_media_content(&request_file).await.expect("removing media failed"); @@ -172,6 +177,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { self.get_media_content(&request_file).await.unwrap().is_none(), "media still there after removing" ); + assert!( + self.get_media_content_for_uri(uri).await.unwrap().is_none(), + "media still found by URI after removing" + ); // Let's add the media again. self.add_media_content(&request_file, content.clone()) @@ -196,6 +205,12 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { "thumbnail not found" ); + // We get a file with the URI, we don't know which one. + assert!( + self.get_media_content_for_uri(uri).await.unwrap().is_some(), + "media not found by URI though two where added" + ); + // Let's add another media with a different URI. self.add_media_content(&request_other_file, other_content.clone()) .await @@ -207,6 +222,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { Some(&other_content), "other file not found" ); + assert_eq!( + self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(), + Some(&other_content), + "other file not found by URI" + ); // Let's remove media based on URI. self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed"); @@ -223,6 +243,14 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { self.get_media_content(&request_other_file).await.unwrap().is_some(), "other media was removed" ); + assert!( + self.get_media_content_for_uri(uri).await.unwrap().is_none(), + "media found by URI wasn't removed" + ); + assert!( + self.get_media_content_for_uri(other_uri).await.unwrap().is_some(), + "other media found by URI was removed" + ); } async fn test_replace_media_key(&self) { diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index b7b81d1439e..78fc8cfa567 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -176,6 +176,17 @@ impl EventCacheStore for MemoryStore { Ok(()) } + async fn get_media_content_for_uri( + &self, + uri: &MxcUri, + ) -> Result>, Self::Error> { + let inner = self.inner.read().unwrap(); + + Ok(inner.media.iter().find_map(|(media_uri, _media_key, media_content)| { + (media_uri == uri).then(|| media_content.to_owned()) + })) + } + async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { let mut inner = self.inner.write().unwrap(); diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 939433727fb..94bc4a94f1e 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -120,6 +120,23 @@ pub trait EventCacheStore: AsyncTraitDeps { request: &MediaRequestParameters, ) -> Result<(), Self::Error>; + /// Get a media file's content associated to an `MxcUri` from the + /// media store. + /// + /// In theory, there could be several files stored using the same URI and a + /// different `MediaFormat`. This API is meant to be used with a media file + /// that has only been stored with a single format. + /// + /// If there are several media files for a given URI in different formats, + /// this API will only return one of them. Which one is left as an + /// implementation detail. + /// + /// # Arguments + /// + /// * `uri` - The `MxcUri` of the media file. + async fn get_media_content_for_uri(&self, uri: &MxcUri) + -> Result>, Self::Error>; + /// Remove all the media files' content associated to an `MxcUri` from the /// media store. /// @@ -201,6 +218,13 @@ impl EventCacheStore for EraseEventCacheStoreError { self.0.remove_media_content(request).await.map_err(Into::into) } + async fn get_media_content_for_uri( + &self, + uri: &MxcUri, + ) -> Result>, Self::Error> { + self.0.get_media_content_for_uri(uri).await.map_err(Into::into) + } + async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> { self.0.remove_media_content_for_uri(uri).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 5848122b9ce..0a0cbe30609 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -734,6 +734,35 @@ impl EventCacheStore for SqliteEventCacheStore { Ok(()) } + async fn get_media_content_for_uri( + &self, + uri: &ruma::MxcUri, + ) -> Result>, Self::Error> { + let uri = self.encode_key(keys::MEDIA, uri); + let conn = self.acquire().await?; + let data = conn + .with_transaction::<_, rusqlite::Error, _>(move |txn| { + // Update the last access. + // We need to do this first so the transaction is in write mode right away. + // See: https://sqlite.org/lang_transaction.html#read_transactions_versus_write_transactions + txn.execute( + "UPDATE media SET last_access = CAST(strftime('%s') as INT) \ + WHERE uri = ?", + (&uri,), + )?; + + txn.query_row::, _, _>( + "SELECT data FROM media WHERE uri = ?", + (&uri,), + |row| row.get(0), + ) + .optional() + }) + .await?; + + data.map(|v| self.decode_value(&v).map(Into::into)).transpose() + } + async fn remove_media_content_for_uri(&self, uri: &ruma::MxcUri) -> Result<()> { let uri = self.encode_key(keys::MEDIA, uri); diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 108915984a4..137756ff561 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -32,7 +32,7 @@ use ruma::{ }, assign, events::room::{MediaSource, ThumbnailInfo}, - MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, + MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, TransactionId, UInt, }; #[cfg(not(target_arch = "wasm32"))] use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir}; @@ -48,6 +48,15 @@ use crate::{ const DEFAULT_UPLOAD_SPEED: u64 = 125_000; /// 5 min minimal upload request timeout, used to clamp the request timeout. const MIN_UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 5); +/// The server name used to generate local MXC URIs. +// This mustn't represent a potentially valid media server, otherwise it'd be +// possible for an attacker to return malicious content under some +// preconditions (e.g. the cache store has been cleared before the upload +// took place). To mitigate against this, we use the .localhost TLD, +// which is guaranteed to be on the local machine. As a result, the only attack +// possible would be coming from the user themselves, which we consider a +// non-threat. +const LOCAL_MXC_SERVER_NAME: &str = "send-queue.localhost"; /// A high-level API to interact with the media API. #[derive(Debug, Clone)] @@ -128,6 +137,10 @@ pub enum MediaError { /// Preallocated media already had content, cannot overwrite. #[error("preallocated media already had content, cannot overwrite")] CannotOverwriteMedia, + + /// Local-only media content was not found. + #[error("local-only media content was not found")] + LocalMediaNotFound, } /// `IntoFuture` returned by [`Media::upload`]. @@ -391,6 +404,12 @@ impl Media { request: &MediaRequestParameters, use_cache: bool, ) -> Result> { + // Ignore request parameters for local medias, notably those pending in the send + // queue. + if let Some(uri) = Self::as_local_uri(&request.source) { + return self.get_local_media_content(uri).await; + } + // Read from the cache. if use_cache { if let Some(content) = @@ -506,6 +525,22 @@ impl Media { Ok(content) } + /// Get a media file's content that is only available in the media cache. + /// + /// # Arguments + /// + /// * `uri` - The local MXC URI of the media content. + async fn get_local_media_content(&self, uri: &MxcUri) -> Result> { + // Read from the cache. + self.client + .event_cache_store() + .lock() + .await? + .get_media_content_for_uri(uri) + .await? + .ok_or_else(|| MediaError::LocalMediaNotFound.into()) + } + /// Remove a media file's content from the store. /// /// # Arguments @@ -679,4 +714,124 @@ impl Media { Ok(Some((MediaSource::Plain(url), thumbnail_info))) } + + /// Create an [`OwnedMxcUri`] for a file or thumbnail we want to store + /// locally before sending it. + /// + /// This uses a MXC ID that is only locally valid. + pub(crate) fn make_local_uri(txn_id: &TransactionId) -> OwnedMxcUri { + OwnedMxcUri::from(format!("mxc://{LOCAL_MXC_SERVER_NAME}/{txn_id}")) + } + + /// Create a [`MediaRequest`] for a file we want to store locally before + /// sending it. + /// + /// This uses a MXC ID that is only locally valid. + pub(crate) fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters { + MediaRequestParameters { + source: MediaSource::Plain(Self::make_local_uri(txn_id)), + format: MediaFormat::File, + } + } + + /// Create a [`MediaRequest`] for a file we want to store locally before + /// sending it. + /// + /// This uses a MXC ID that is only locally valid. + pub(crate) fn make_local_thumbnail_media_request( + txn_id: &TransactionId, + height: UInt, + width: UInt, + ) -> MediaRequestParameters { + MediaRequestParameters { + source: MediaSource::Plain(Self::make_local_uri(txn_id)), + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(width, height)), + } + } + + /// Returns the local MXC URI contained by the given source, if any. + /// + /// A local MXC URI is a URI that was generated with `make_local_uri`. + fn as_local_uri(source: &MediaSource) -> Option<&MxcUri> { + let uri = match source { + MediaSource::Plain(uri) => uri, + MediaSource::Encrypted(file) => &file.url, + }; + + uri.server_name() + .is_ok_and(|server_name| server_name == LOCAL_MXC_SERVER_NAME) + .then_some(uri) + } +} + +#[cfg(test)] +mod tests { + use assert_matches2::assert_matches; + use ruma::{ + events::room::{EncryptedFile, MediaSource}, + mxc_uri, owned_mxc_uri, uint, MxcUri, + }; + use serde_json::json; + + use super::Media; + + /// Create an `EncryptedFile` with the given MXC URI. + fn encrypted_file(mxc_uri: &MxcUri) -> Box { + Box::new( + serde_json::from_value(json!({ + "url": mxc_uri, + "key": { + "kty": "oct", + "key_ops": ["encrypt", "decrypt"], + "alg": "A256CTR", + "k": "b50ACIv6LMn9AfMCFD1POJI_UAFWIclxAN1kWrEO2X8", + "ext": true, + }, + "iv": "AK1wyzigZtQAAAABAAAAKK", + "hashes": { + "sha256": "foobar", + }, + "v": "v2", + })) + .unwrap(), + ) + } + + #[test] + fn test_as_local_uri() { + let txn_id = "abcdef"; + + // Request generated with `make_local_file_media_request`. + let request = Media::make_local_file_media_request(txn_id.into()); + assert_matches!(Media::as_local_uri(&request.source), Some(uri)); + assert_eq!(uri.media_id(), Ok(txn_id)); + + // Request generated with `make_local_thumbnail_media_request`. + let request = + Media::make_local_thumbnail_media_request(txn_id.into(), uint!(100), uint!(100)); + assert_matches!(Media::as_local_uri(&request.source), Some(uri)); + assert_eq!(uri.media_id(), Ok(txn_id)); + + // Local plain source. + let source = MediaSource::Plain(Media::make_local_uri(txn_id.into())); + assert_matches!(Media::as_local_uri(&source), Some(uri)); + assert_eq!(uri.media_id(), Ok(txn_id)); + + // Local encrypted source. + let source = MediaSource::Encrypted(encrypted_file(&Media::make_local_uri(txn_id.into()))); + assert_matches!(Media::as_local_uri(&source), Some(uri)); + assert_eq!(uri.media_id(), Ok(txn_id)); + + // Test non-local plain source. + let source = MediaSource::Plain(owned_mxc_uri!("mxc://server.local/poiuyt")); + assert_matches!(Media::as_local_uri(&source), None); + + // Test non-local encrypted source. + let source = MediaSource::Encrypted(encrypted_file(mxc_uri!("mxc://server.local/mlkjhg"))); + assert_matches!(Media::as_local_uri(&source), None); + + // Test invalid MXC URI. + let source = MediaSource::Plain("https://server.local/nbvcxw".into()); + assert_matches!(Media::as_local_uri(&source), None); + } } diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 269f6ca3858..adc7456a1ff 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -15,7 +15,7 @@ //! Private implementations of the media upload mechanism. use matrix_sdk_base::{ - media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, + media::{MediaFormat, MediaRequestParameters}, store::{ ChildTransactionId, DependentQueuedRequestKind, FinishUploadThumbnailInfo, QueuedRequestKind, SentMediaInfo, SentRequestKey, SerializableEventContent, @@ -25,13 +25,10 @@ use matrix_sdk_base::{ use mime::Mime; use ruma::{ events::{ - room::{ - message::{FormattedBody, MessageType, RoomMessageEventContent}, - MediaSource, - }, + room::message::{FormattedBody, MessageType, RoomMessageEventContent}, AnyMessageLikeEventContent, }, - OwnedMxcUri, OwnedTransactionId, TransactionId, UInt, + OwnedTransactionId, TransactionId, }; use tracing::{debug, error, instrument, trace, warn, Span}; @@ -43,51 +40,9 @@ use crate::{ LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle, }, - Client, Room, + Client, Media, Room, }; -/// Create an [`OwnedMxcUri`] for a file or thumbnail we want to store locally -/// before sending it. -/// -/// This uses a MXC ID that is only locally valid. -fn make_local_uri(txn_id: &TransactionId) -> OwnedMxcUri { - // This mustn't represent a potentially valid media server, otherwise it'd be - // possible for an attacker to return malicious content under some - // preconditions (e.g. the cache store has been cleared before the upload - // took place). To mitigate against this, we use the .localhost TLD, - // which is guaranteed to be on the local machine. As a result, the only attack - // possible would be coming from the user themselves, which we consider a - // non-threat. - OwnedMxcUri::from(format!("mxc://send-queue.localhost/{txn_id}")) -} - -/// Create a [`MediaRequest`] for a file we want to store locally before -/// sending it. -/// -/// This uses a MXC ID that is only locally valid. -fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters { - MediaRequestParameters { - source: MediaSource::Plain(make_local_uri(txn_id)), - format: MediaFormat::File, - } -} - -/// Create a [`MediaRequest`] for a file we want to store locally before -/// sending it. -/// -/// This uses a MXC ID that is only locally valid. -fn make_local_thumbnail_media_request( - txn_id: &TransactionId, - height: UInt, - width: UInt, -) -> MediaRequestParameters { - // See comment in [`make_local_file_media_request`]. - MediaRequestParameters { - source: MediaSource::Plain(make_local_uri(txn_id)), - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(width, height)), - } -} - /// Replace the source by the final ones in all the media types handled by /// [`Room::make_attachment_type()`]. fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) { @@ -164,7 +119,7 @@ impl RoomSendQueue { Span::current().record("event_txn", tracing::field::display(&*send_event_txn)); debug!(filename, %content_type, %upload_file_txn, "sending an attachment"); - let file_media_request = make_local_file_media_request(&upload_file_txn); + let file_media_request = Media::make_local_file_media_request(&upload_file_txn); let (upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info) = { let client = room.client(); @@ -195,7 +150,7 @@ impl RoomSendQueue { // Cache thumbnail in the cache store. let thumbnail_media_request = - make_local_thumbnail_media_request(&txn, height, width); + Media::make_local_thumbnail_media_request(&txn, height, width); cache_store .add_media_content(&thumbnail_media_request, data) .await @@ -287,7 +242,7 @@ impl QueueStorage { // Update cache keys in the cache store. { // Do it for the file itself. - let from_req = make_local_file_media_request(&file_upload_txn); + let from_req = Media::make_local_file_media_request(&file_upload_txn); trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store"); let cache_store = client @@ -312,7 +267,7 @@ impl QueueStorage { thumbnail_info.as_ref().zip(sent_media.thumbnail.clone()) { let from_req = - make_local_thumbnail_media_request(&info.txn, info.height, info.width); + Media::make_local_thumbnail_media_request(&info.txn, info.height, info.width); trace!(from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); @@ -514,10 +469,10 @@ impl QueueStorage { { let event_cache = client.event_cache_store().lock().await?; event_cache - .remove_media_content_for_uri(&make_local_uri(&handles.upload_file_txn)) + .remove_media_content_for_uri(&Media::make_local_uri(&handles.upload_file_txn)) .await?; if let Some(txn) = &handles.upload_thumbnail_txn { - event_cache.remove_media_content_for_uri(&make_local_uri(txn)).await?; + event_cache.remove_media_content_for_uri(&Media::make_local_uri(txn)).await?; } } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 8024a26fb04..70f1181e833 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1892,7 +1892,7 @@ async fn test_media_uploads() { .media() .get_media_content( &MediaRequestParameters { - source: local_thumbnail_source, + source: local_thumbnail_source.clone(), format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( tinfo.width.unwrap(), tinfo.height.unwrap(), @@ -1904,6 +1904,20 @@ async fn test_media_uploads() { .expect("media should be found"); assert_eq!(thumbnail_media, b"thumbnail"); + // The format should be ignored when requesting a local media. + let thumbnail_media = client + .media() + .get_media_content( + &MediaRequestParameters { + source: local_thumbnail_source.clone(), + format: MediaFormat::File, + }, + true, + ) + .await + .expect("media should be found"); + assert_eq!(thumbnail_media, b"thumbnail"); + // ---------------------- // Send handle operations. @@ -1967,6 +1981,16 @@ async fn test_media_uploads() { .expect("media should be found"); assert_eq!(thumbnail_media, b"thumbnail"); + // The local URI does not work anymore. + client + .media() + .get_media_content( + &MediaRequestParameters { source: local_thumbnail_source, format: MediaFormat::File }, + true, + ) + .await + .expect_err("media with local URI should not be found"); + // The event is sent, at some point. assert_update!(watch => sent { txn = transaction_id, From 8db78efbbc1372057c75999f8257b6800bb9781c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 14:06:51 +0100 Subject: [PATCH 713/979] fix(event cache): use a correcter heuristic to decide whether to add initial events or not Thanks Hywan for spotting the issue. --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 2 +- crates/matrix-sdk/src/event_cache/mod.rs | 30 +++++++++++++++++-- .../matrix-sdk/src/event_cache/room/events.rs | 5 ++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 8131db3cec3..b9fa737be80 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -879,7 +879,7 @@ impl LinkedChunk { } /// Returns the number of items of the linked chunk. - fn len(&self) -> usize { + pub fn len(&self) -> usize { self.items().count() } } diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 205f9d2e1d3..cda4fe48567 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -319,9 +319,9 @@ impl EventCache { let room_cache = self.inner.for_room(room_id).await?; - // If the linked chunked already has at least one chunk (gap or events), ignore - // this request, as it should happen at most once per room. - if room_cache.inner.state.read().await.events().chunks().next().is_some() { + // If the linked chunked already has at least one event, ignore this request, as + // it should happen at most once per room. + if !room_cache.inner.state.read().await.events().is_empty() { return Ok(()); } @@ -737,4 +737,28 @@ mod tests { assert!(room_event_cache.event(event_id).await.is_none()); assert!(event_cache.event(event_id).await.is_none()); } + + #[async_test] + async fn test_add_initial_events() { + // TODO: remove this test when the event cache uses its own persistent storage. + let client = logged_in_client(None).await; + let room_id = room_id!("!galette:saucisse.bzh"); + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + event_cache + .add_initial_events(room_id, vec![f.text_msg("hey").into()], None) + .await + .unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + let (initial_events, _) = room_event_cache.subscribe().await.unwrap(); + // `add_initial_events` had an effect. + assert_eq!(initial_events.len(), 1); + } } diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 6313ac3d3fa..ea53ae23702 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -76,6 +76,11 @@ impl RoomEvents { Self { chunks, chunks_updates_as_vectordiffs, deduplicator } } + /// Returns whether the room has at least one event. + pub fn is_empty(&self) -> bool { + self.chunks.len() == 0 + } + /// Clear all events. /// /// All events, all gaps, everything is dropped, move into the void, into From affdc25256990a3c5ea4e8ed1266c1d8719315ef Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 14:27:30 +0100 Subject: [PATCH 714/979] refactor(linked chunk): rename `len()` to `num_items()` This makes it clearer that it's only concerned about the number of items, not the number of chunks. --- .../src/linked_chunk/builder.rs | 2 +- .../matrix-sdk-common/src/linked_chunk/mod.rs | 80 +++++++++---------- .../matrix-sdk/src/event_cache/room/events.rs | 2 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/builder.rs b/crates/matrix-sdk-common/src/linked_chunk/builder.rs index 20247d83533..16550866cbd 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/builder.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/builder.rs @@ -356,7 +356,7 @@ mod tests { assert!(chunks.next().is_none()); // The linked chunk had 5 items. - assert_eq!(lc.len(), 5); + assert_eq!(lc.num_items(), 5); // Now, if we add a new chunk, its identifier should be the previous one we used // + 1. diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index b9fa737be80..40f3fbb019f 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -879,7 +879,7 @@ impl LinkedChunk { } /// Returns the number of items of the linked chunk. - pub fn len(&self) -> usize { + pub fn num_items(&self) -> usize { self.items().count() } } @@ -1455,7 +1455,7 @@ mod tests { fn test_empty() { let items = LinkedChunk::<3, char, ()>::new(); - assert_eq!(items.len(), 0); + assert_eq!(items.num_items(), 0); // This test also ensures that `Drop` for `LinkedChunk` works when // there is only one chunk. @@ -1538,7 +1538,7 @@ mod tests { ] ); - assert_eq!(linked_chunk.len(), 10); + assert_eq!(linked_chunk.num_items(), 10); } #[test] @@ -1630,7 +1630,7 @@ mod tests { ] ); - assert_eq!(linked_chunk.len(), 9); + assert_eq!(linked_chunk.num_items(), 9); } #[test] @@ -1898,7 +1898,7 @@ mod tests { linked_chunk, ['a', 'b', 'c'] ['d', 'w', 'x'] ['y', 'z', 'e'] ['f'] ); - assert_eq!(linked_chunk.len(), 10); + assert_eq!(linked_chunk.num_items(), 10); assert_eq!( linked_chunk.updates().unwrap().take(), &[ @@ -1932,7 +1932,7 @@ mod tests { linked_chunk, ['l', 'm', 'n'] ['o', 'a', 'b'] ['c'] ['d', 'w', 'x'] ['y', 'z', 'e'] ['f'] ); - assert_eq!(linked_chunk.len(), 14); + assert_eq!(linked_chunk.num_items(), 14); assert_eq!( linked_chunk.updates().unwrap().take(), &[ @@ -1966,7 +1966,7 @@ mod tests { linked_chunk, ['l', 'm', 'n'] ['o', 'a', 'b'] ['r', 's', 'c'] ['d', 'w', 'x'] ['y', 'z', 'e'] ['f'] ); - assert_eq!(linked_chunk.len(), 16); + assert_eq!(linked_chunk.num_items(), 16); assert_eq!( linked_chunk.updates().unwrap().take(), &[ @@ -1994,7 +1994,7 @@ mod tests { linked_chunk.updates().unwrap().take(), &[PushItems { at: Position(ChunkIdentifier(3), 1), items: vec!['p', 'q'] }] ); - assert_eq!(linked_chunk.len(), 18); + assert_eq!(linked_chunk.num_items(), 18); } // Insert in a chunk that does not exist. @@ -2039,7 +2039,7 @@ mod tests { ); } - assert_eq!(linked_chunk.len(), 18); + assert_eq!(linked_chunk.num_items(), 18); Ok(()) } @@ -2055,7 +2055,7 @@ mod tests { linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 11); + assert_eq!(linked_chunk.num_items(), 11); // Ignore previous updates. let _ = linked_chunk.updates().unwrap().take(); @@ -2068,21 +2068,21 @@ mod tests { assert_eq!(removed_item, 'f'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 10); + assert_eq!(linked_chunk.num_items(), 10); let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap(); let removed_item = linked_chunk.remove_item_at(position_of_e, EmptyChunk::Remove)?; assert_eq!(removed_item, 'e'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 9); + assert_eq!(linked_chunk.num_items(), 9); let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap(); let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?; assert_eq!(removed_item, 'd'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 8); + assert_eq!(linked_chunk.num_items(), 8); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2103,19 +2103,19 @@ mod tests { assert_eq!(removed_item, 'a'); assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 7); + assert_eq!(linked_chunk.num_items(), 7); let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'b'); assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 6); + assert_eq!(linked_chunk.num_items(), 6); let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'c'); assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 5); + assert_eq!(linked_chunk.num_items(), 5); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2135,19 +2135,19 @@ mod tests { assert_eq!(removed_item, 'g'); assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 4); + assert_eq!(linked_chunk.num_items(), 4); let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'h'); assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']); - assert_eq!(linked_chunk.len(), 3); + assert_eq!(linked_chunk.num_items(), 3); let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'i'); assert_items_eq!(linked_chunk, [] ['j', 'k']); - assert_eq!(linked_chunk.len(), 2); + assert_eq!(linked_chunk.num_items(), 2); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2169,14 +2169,14 @@ mod tests { assert_eq!(removed_item, 'k'); #[rustfmt::skip] assert_items_eq!(linked_chunk, [] ['j']); - assert_eq!(linked_chunk.len(), 1); + assert_eq!(linked_chunk.num_items(), 1); let position_of_j = linked_chunk.item_position(|item| *item == 'j').unwrap(); let removed_item = linked_chunk.remove_item_at(position_of_j, EmptyChunk::Remove)?; assert_eq!(removed_item, 'j'); assert_items_eq!(linked_chunk, []); - assert_eq!(linked_chunk.len(), 0); + assert_eq!(linked_chunk.num_items(), 0); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2194,13 +2194,13 @@ mod tests { #[rustfmt::skip] assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']); - assert_eq!(linked_chunk.len(), 4); + assert_eq!(linked_chunk.num_items(), 4); let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap(); linked_chunk.insert_gap_at((), position_of_c)?; assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] ['d']); - assert_eq!(linked_chunk.len(), 4); + assert_eq!(linked_chunk.num_items(), 4); // Ignore updates. let _ = linked_chunk.updates().unwrap().take(); @@ -2210,27 +2210,27 @@ mod tests { assert_eq!(removed_item, 'c'); assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']); - assert_eq!(linked_chunk.len(), 3); + assert_eq!(linked_chunk.num_items(), 3); let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap(); let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?; assert_eq!(removed_item, 'd'); assert_items_eq!(linked_chunk, ['a', 'b'] [-]); - assert_eq!(linked_chunk.len(), 2); + assert_eq!(linked_chunk.num_items(), 2); let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap(); let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'a'); assert_items_eq!(linked_chunk, ['b'] [-]); - assert_eq!(linked_chunk.len(), 1); + assert_eq!(linked_chunk.num_items(), 1); let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?; assert_eq!(removed_item, 'b'); assert_items_eq!(linked_chunk, [] [-]); - assert_eq!(linked_chunk.len(), 0); + assert_eq!(linked_chunk.num_items(), 0); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2259,7 +2259,7 @@ mod tests { linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h']); - assert_eq!(linked_chunk.len(), 8); + assert_eq!(linked_chunk.num_items(), 8); // Ignore previous updates. let _ = linked_chunk.updates().unwrap().take(); @@ -2272,19 +2272,19 @@ mod tests { assert_eq!(removed_item, 'd'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['e', 'f'] ['g', 'h']); - assert_eq!(linked_chunk.len(), 7); + assert_eq!(linked_chunk.num_items(), 7); let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; assert_eq!(removed_item, 'e'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['f'] ['g', 'h']); - assert_eq!(linked_chunk.len(), 6); + assert_eq!(linked_chunk.num_items(), 6); let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; assert_eq!(removed_item, 'f'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['g', 'h']); - assert_eq!(linked_chunk.len(), 5); + assert_eq!(linked_chunk.num_items(), 5); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2304,13 +2304,13 @@ mod tests { assert_eq!(removed_item, 'g'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['h']); - assert_eq!(linked_chunk.len(), 4); + assert_eq!(linked_chunk.num_items(), 4); let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; assert_eq!(removed_item, 'h'); assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] []); - assert_eq!(linked_chunk.len(), 3); + assert_eq!(linked_chunk.num_items(), 3); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2329,19 +2329,19 @@ mod tests { assert_eq!(removed_item, 'a'); assert_items_eq!(linked_chunk, ['b', 'c'] [] []); - assert_eq!(linked_chunk.len(), 2); + assert_eq!(linked_chunk.num_items(), 2); let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; assert_eq!(removed_item, 'b'); assert_items_eq!(linked_chunk, ['c'] [] []); - assert_eq!(linked_chunk.len(), 1); + assert_eq!(linked_chunk.num_items(), 1); let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?; assert_eq!(removed_item, 'c'); assert_items_eq!(linked_chunk, [] [] []); - assert_eq!(linked_chunk.len(), 0); + assert_eq!(linked_chunk.num_items(), 0); assert_eq!( linked_chunk.updates().unwrap().take(), @@ -2530,7 +2530,7 @@ mod tests { assert!(linked_chunk.updates().unwrap().take().is_empty()); } - assert_eq!(linked_chunk.len(), 6); + assert_eq!(linked_chunk.num_items(), 6); Ok(()) } @@ -2645,7 +2645,7 @@ mod tests { ); } - assert_eq!(linked_chunk.len(), 13); + assert_eq!(linked_chunk.num_items(), 13); Ok(()) } @@ -2732,7 +2732,7 @@ mod tests { assert_eq!(Arc::strong_count(&item), 7); assert_eq!(Arc::strong_count(&gap), 2); - assert_eq!(linked_chunk.len(), 6); + assert_eq!(linked_chunk.num_items(), 6); assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 3); // Now, we can clear the linked chunk and see what happens. @@ -2740,7 +2740,7 @@ mod tests { assert_eq!(Arc::strong_count(&item), 1); assert_eq!(Arc::strong_count(&gap), 1); - assert_eq!(linked_chunk.len(), 0); + assert_eq!(linked_chunk.num_items(), 0); assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 0); } diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index ea53ae23702..a69fcfeca4d 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -78,7 +78,7 @@ impl RoomEvents { /// Returns whether the room has at least one event. pub fn is_empty(&self) -> bool { - self.chunks.len() == 0 + self.chunks.num_items() == 0 } /// Clear all events. From a1a04ee5132b3519a3203d228d428eb1444414e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 9 Dec 2024 15:03:28 +0100 Subject: [PATCH 715/979] chore: Remove MSRV from READMEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It can be found in Cargo.toml. Signed-off-by: Kévin Commaille --- README.md | 4 ---- bindings/matrix-sdk-crypto-ffi/README.md | 4 ---- 2 files changed, 8 deletions(-) diff --git a/README.md b/README.md index 216ad95cf41..825cd803679 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,6 @@ The rust-sdk consists of multiple crates that can be picked at your convenience: - **matrix-sdk-crypto** - No (network) IO encryption state machine that can be used to add Matrix E2EE support to your client or client library. -## Minimum Supported Rust Version (MSRV) - -These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`. - ## Status The library is in an alpha state, things that are implemented generally work but diff --git a/bindings/matrix-sdk-crypto-ffi/README.md b/bindings/matrix-sdk-crypto-ffi/README.md index 8935088532c..6a0c2e9c357 100644 --- a/bindings/matrix-sdk-crypto-ffi/README.md +++ b/bindings/matrix-sdk-crypto-ffi/README.md @@ -71,10 +71,6 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \ /home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so ``` -## Minimum Supported Rust Version (MSRV) - -These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`. - ## License [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) From e402ed4ce8fc773a7e40dfc03a22a8f311a70881 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 3 Dec 2024 17:11:23 +0100 Subject: [PATCH 716/979] refactor(event cache): get the *most recent* pagination token, not the *oldest* one Whenever it needs to back-paginate, the event cache should start with the *most recent* backpagination token, not the oldest one. This isn't a functional change, until the persistent storage is enabled. The reason is that, currently, there is one previous-batch token alive; after it's used, it's replaced with another gap and the events it served to request from the server. When persistent storage will be enabled, we'll have situations like the one shown in the test code, where we can have multiple previous-batch token alive at the same time. In that case, we'll need to back-paginate from the most recent events to the least recent events, and not the other way around, or we'll have holes in the timeline that won't be filled until we got to the start of the timeline. --- .../matrix-sdk/src/event_cache/pagination.rs | 57 +++++++++++++++++-- .../matrix-sdk/src/event_cache/room/events.rs | 16 +++++- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 16b131c1836..793c7d700af 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -245,8 +245,8 @@ impl RoomPagination { /// Otherwise, it will immediately skip. #[doc(hidden)] pub async fn get_or_wait_for_token(&self, wait_time: Option) -> Option { - fn get_oldest(events: &RoomEvents) -> Option { - events.chunks().find_map(|chunk| match chunk.content() { + fn get_latest(events: &RoomEvents) -> Option { + events.rchunks().find_map(|chunk| match chunk.content() { ChunkContent::Gap(gap) => Some(gap.prev_token.clone()), ChunkContent::Items(..) => None, }) @@ -256,7 +256,7 @@ impl RoomPagination { // Scope for the lock guard. let state = self.inner.state.read().await; // Fast-path: we do have a previous-batch token already. - if let Some(found) = get_oldest(state.events()) { + if let Some(found) = get_latest(state.events()) { return Some(found); } // If we've already waited for an initial previous-batch token before, @@ -275,7 +275,7 @@ impl RoomPagination { let _ = timeout(wait_time, self.inner.pagination_batch_token_notifier.notified()).await; let mut state = self.inner.state.write().await; - let token = get_oldest(state.events()); + let token = get_latest(state.events()); state.waited_for_initial_prev_token = true; token } @@ -321,7 +321,9 @@ mod tests { use std::time::{Duration, Instant}; use matrix_sdk_base::RoomState; - use matrix_sdk_test::{async_test, sync_timeline_event}; + use matrix_sdk_test::{ + async_test, event_factory::EventFactory, sync_timeline_event, ALICE, + }; use ruma::room_id; use tokio::{spawn, time::sleep}; @@ -506,5 +508,50 @@ mod tests { // The task succeeded. insert_token_task.await.unwrap(); } + + #[async_test] + async fn test_get_latest_token() { + let client = logged_in_client(None).await; + let room_id = room_id!("!galette:saucisse.bzh"); + client.base_client().get_or_create_room(room_id, RoomState::Joined); + + let event_cache = client.event_cache(); + + event_cache.subscribe().unwrap(); + + let (room_event_cache, _drop_handles) = event_cache.for_room(room_id).await.unwrap(); + + let old_token = "old".to_owned(); + let new_token = "new".to_owned(); + + // Assuming a room event cache that contains both an old and a new pagination + // token, and events in between, + room_event_cache + .inner + .state + .write() + .await + .with_events_mut(|events| { + let f = EventFactory::new().room(room_id).sender(*ALICE); + + // This simulates a valid representation of a room: first group of gap+events + // were e.g. restored from the cache; second group of gap+events was received + // from a subsequent sync. + events.push_gap(Gap { prev_token: old_token }); + events.push_events([f.text_msg("oldest from cache").into()]); + + events.push_gap(Gap { prev_token: new_token.clone() }); + events.push_events([f.text_msg("sync'd gappy timeline").into()]); + }) + .await + .unwrap(); + + let pagination = room_event_cache.pagination(); + + // Retrieving the pagination token will return the most recent one, not the old + // one. + let found = pagination.get_or_wait_for_token(None).await; + assert_eq!(found, Some(new_token)); + } } } diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index a69fcfeca4d..f194349ff35 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -18,10 +18,10 @@ use eyeball_im::VectorDiff; pub use matrix_sdk_base::event_cache::{Event, Gap}; use matrix_sdk_base::{ event_cache::store::DEFAULT_CHUNK_CAPACITY, - linked_chunk::{AsVector, ObservableUpdates}, + linked_chunk::{AsVector, IterBackward, ObservableUpdates}, }; use matrix_sdk_common::linked_chunk::{ - Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position, + Chunk, ChunkIdentifier, EmptyChunk, Error, LinkedChunk, Position, }; use ruma::OwnedEventId; use tracing::{debug, error, warn}; @@ -177,10 +177,20 @@ impl RoomEvents { /// Iterate over the chunks, forward. /// /// The oldest chunk comes first. - pub fn chunks(&self) -> Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> { + #[cfg(test)] + pub fn chunks( + &self, + ) -> matrix_sdk_common::linked_chunk::Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> { self.chunks.chunks() } + /// Iterate over the chunks, backward. + /// + /// The most recent chunk comes first. + pub fn rchunks(&self) -> IterBackward<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> { + self.chunks.rchunks() + } + /// Iterate over the events, backward. /// /// The most recent event comes first. From 8aae16ffd7c7a04b316c725f38ea54bb1009a287 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 29 Nov 2024 14:46:05 +0000 Subject: [PATCH 717/979] feat(crypto) Provide a method to check whether server backup exists without hitting the server every time --- crates/matrix-sdk/CHANGELOG.md | 6 + .../matrix-sdk/src/encryption/backups/mod.rs | 205 ++++++++++++++++-- .../src/encryption/backups/types.rs | 32 +++ crates/matrix-sdk/src/test_utils/mocks.rs | 50 ++++- 4 files changed, 275 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index d2922bc3d82..87862d8672b 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Do not use the encrypted original file's content type as the encrypted thumbnail's content type. ([#ecf4434](https://github.com/matrix-org/matrix-rust-sdk/commit/ecf44348cf6a872b843fb7d7af1a88f724c58c3e)) + ### Features - Enable persistent storage for the `EventCache`. This allows events received @@ -28,6 +29,11 @@ All notable changes to this project will be documented in this file. - [**breaking**] Make all fields of Thumbnail required ([#4324](https://github.com/matrix-org/matrix-rust-sdk/pull/4324)) +- `Backups::exists_on_server`, which always fetches up-to-date information from the + server about whether a key storage backup exists, was renamed to + `fetch_exists_on_the_server`, and a new implementation of `exists_on_server` + which caches the most recent answer is now provided. + ## [0.8.0] - 2024-11-19 ### Bug Fixes diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index d0d0ad43e59..23fa8cb5b3e 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -90,6 +90,7 @@ impl Backups { /// # anyhow::Ok(()) }; /// ``` pub async fn create(&self) -> Result<(), Error> { + self.client.inner.e2ee.backup_state.clear_backup_exists_on_server(); let _guard = self.client.locks().backup_modify_lock.lock().await; self.set_state(BackupState::Creating); @@ -387,7 +388,30 @@ impl Backups { /// This method will request info about the current backup from the /// homeserver and if a backup exits return `true`, otherwise `false`. pub async fn exists_on_server(&self) -> Result { - Ok(self.get_current_version().await?.is_some()) + let exists_on_server = self.get_current_version().await?.is_some(); + self.client.inner.e2ee.backup_state.set_backup_exists_on_server(exists_on_server); + Ok(exists_on_server) + } + + /// Does a backup exist on the server? + /// + /// This method is identical to [`Self::exists_on_server`] except that we + /// cache the latest answer in memory and only empty the cache if the local + /// device adds or deletes a backup itself. + /// + /// Do not use this method if you need an accurate answer about whether a + /// backup exists - instead use [`Self::exists_on_server`]. This method is + /// useful when performance is more important than guaranteed accuracy, + /// such as when classifying UTDs. + pub async fn fast_exists_on_server(&self) -> Result { + // If we have an answer cached, return it immediately + if let Some(cached_value) = self.client.inner.e2ee.backup_state.backup_exists_on_server() { + return Ok(cached_value); + } + + // Otherwise, delegate to exists_on_server. (It will update the cached value for + // us.) + self.exists_on_server().await } /// Subscribe to a stream that notifies when a room key for the specified @@ -621,7 +645,7 @@ impl Backups { async fn delete_backup_from_server(&self, version: String) -> Result<(), Error> { let request = ruma::api::client::backup::delete_backup_version::v3::Request::new(version); - match self.client.send(request, Default::default()).await { + let ret = match self.client.send(request, Default::default()).await { Ok(_) => Ok(()), Err(e) => { if let Some(kind) = e.client_api_error_kind() { @@ -634,7 +658,11 @@ impl Backups { Err(e.into()) } } - } + }; + + self.client.inner.e2ee.backup_state.clear_backup_exists_on_server(); + + ret } #[instrument(skip(self, olm_machine, request))] @@ -1135,6 +1163,23 @@ mod test { assert!(exists, "We should deduce that a backup exists on the server"); } + #[async_test] + async fn test_repeated_calls_to_exists_on_server_makes_repeated_requests() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Expect 2 requests to the server + server.mock_room_keys_version().exists().expect(2).mount().await; + + let backups = client.encryption().backups(); + + // Call exists_on_server twice + backups.exists_on_server().await.unwrap(); + let exists = backups.exists_on_server().await.unwrap(); + + assert!(exists, "We should deduce that a backup exists on the server"); + } + #[async_test] async fn test_when_no_backup_exists_then_exists_on_server_returns_false() { let server = MatrixMockServer::new().await; @@ -1176,21 +1221,149 @@ mod test { } } + #[async_test] + async fn test_when_a_backup_exists_then_fast_exists_on_server_returns_true() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_room_keys_version().exists().expect(1).mount().await; + + let exists = client + .encryption() + .backups() + .fast_exists_on_server() + .await + .expect("We should be able to check if backups exist on the server"); + + assert!(exists, "We should deduce that a backup exists on the server"); + } + + #[async_test] + async fn test_when_no_backup_exists_then_fast_exists_on_server_returns_false() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_room_keys_version().none().expect(1).mount().await; + + let exists = client + .encryption() + .backups() + .fast_exists_on_server() + .await + .expect("We should be able to check if backups exist on the server"); + + assert!(!exists, "We should deduce that no backup exists on the server"); + } + + #[async_test] + async fn test_when_server_returns_an_error_then_fast_exists_on_server_returns_an_error() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + { + let _scope = + server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await; + + client.encryption().backups().fast_exists_on_server().await.expect_err( + "If the /version endpoint returns a non 404 error we should throw an error", + ); + } + + { + let _scope = + server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await; + + client.encryption().backups().fast_exists_on_server().await.expect_err( + "If the /version endpoint returns a non-Matrix 404 error we should throw an error", + ); + } + } + + #[async_test] + async fn test_repeated_calls_to_fast_exists_on_server_do_not_make_additional_requests() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Create a mock stating that the request should only be made once + server.mock_room_keys_version().exists().expect(1).mount().await; + + let backups = client.encryption().backups(); + + // Call fast_exists_on_server several times + backups.fast_exists_on_server().await.unwrap(); + backups.fast_exists_on_server().await.unwrap(); + backups.fast_exists_on_server().await.unwrap(); + + let exists = backups + .fast_exists_on_server() + .await + .expect("We should be able to check if backups exist on the server"); + + assert!(exists, "We should deduce that a backup exists on the server"); + + // We check expectations here, confirming that only one call was made + } + + #[async_test] + async fn test_adding_a_backup_invalidates_fast_exists_on_server_cache() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let backups = client.encryption().backups(); + + { + let _scope = server.mock_room_keys_version().none().expect(1).mount_as_scoped().await; + + // Call fast_exists_on_server to fill the cache + let exists = backups.fast_exists_on_server().await.unwrap(); + assert!(!exists, "No backup exists at this point"); + } + + // Create a new backup. Should invalidate the cache + server.mock_add_room_keys_version().ok().expect(1).mount().await; + backups.create().await.expect("Failed to create a backup"); + + server.mock_room_keys_version().exists().expect(1).mount().await; + let exists = backups + .fast_exists_on_server() + .await + .expect("We should be able to check if backups exist on the server"); + + assert!(exists, "But now a backup does exist"); + } + + #[async_test] + async fn test_removing_a_backup_invalidates_fast_exists_on_server_cache() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let backups = client.encryption().backups(); + + { + let _scope = server.mock_room_keys_version().exists().expect(1).mount_as_scoped().await; + + // Call fast_exists_on_server to fill the cache + let exists = backups.fast_exists_on_server().await.unwrap(); + assert!(exists, "A backup exists at this point"); + } + + // Delete the backup. Should invalidate the cache + server.mock_delete_room_keys_version().ok().expect(1).mount().await; + backups.delete_backup_from_server("1".to_owned()).await.expect("Failed to delete a backup"); + + server.mock_room_keys_version().none().expect(1).mount().await; + let exists = backups + .fast_exists_on_server() + .await + .expect("We should be able to check if backups exist on the server"); + + assert!(!exists, "But now there is no backup"); + } + #[async_test] async fn test_waiting_for_steady_state_resets_the_delay() { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - Mock::given(method("POST")) - .and(path("_matrix/client/unstable/room_keys/version")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "version": "1" - }))) - .expect(1) - .named("POST for the backup creation") - .mount(&server) - .await; + server.mock_add_room_keys_version().ok().expect(1).mount().await; client .encryption() @@ -1247,7 +1420,5 @@ mod test { { client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned() }; assert_eq!(old_duration, current_duration); - - server.verify().await; } } diff --git a/crates/matrix-sdk/src/encryption/backups/types.rs b/crates/matrix-sdk/src/encryption/backups/types.rs index 8585078ad93..9d6c089f503 100644 --- a/crates/matrix-sdk/src/encryption/backups/types.rs +++ b/crates/matrix-sdk/src/encryption/backups/types.rs @@ -53,6 +53,37 @@ pub(crate) struct BackupClientState { pub(crate) upload_progress: ChannelObservable, pub(super) global_state: ChannelObservable, pub(super) room_keys_broadcaster: broadcast::Sender, + + /// Whether a key storage backup exists on the server, as far as we know. + /// + /// This is `None` if we have not asked the server yet, and `Some` + /// otherwise. This value is not always up-to-date: if the backup status + /// on the server was changed by some other client, we will have a old + /// value. + pub(super) backup_exists_on_server: RwLock>, +} + +impl BackupClientState { + /// Update the cached value indicating whether a key storage backup exists + /// on the server + pub(crate) fn set_backup_exists_on_server(&self, exists_on_server: bool) { + *self.backup_exists_on_server.write().unwrap() = Some(exists_on_server); + } + + /// Ask whether the key storage backup exists on the server. Returns `None` + /// if we haven't checked. Note that this value will be out-of-date if + /// some other client changed the state since the last time we checked. + pub(crate) fn backup_exists_on_server(&self) -> Option { + *self.backup_exists_on_server.read().unwrap() + } + + /// Clear out the cached value indicating whether a key storage backup + /// exists on the server, meaning that the code in + /// [`super::Backups`] will repopulate it when needed + /// with an up-to-date value. + pub(crate) fn clear_backup_exists_on_server(&self) { + *self.backup_exists_on_server.write().unwrap() = None; + } } const DEFAULT_BACKUP_UPLOAD_DELAY: Duration = Duration::from_millis(100); @@ -64,6 +95,7 @@ impl Default for BackupClientState { upload_progress: ChannelObservable::new(UploadState::Idle), global_state: Default::default(), room_keys_broadcaster: broadcast::Sender::new(100), + backup_exists_on_server: RwLock::new(None), } } } diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 9c583b95b5e..da51b8388b6 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -591,6 +591,22 @@ impl MatrixMockServer { .and(header("authorization", "Bearer 1234")); MockEndpoint { mock, server: &self.server, endpoint: RoomKeysVersionEndpoint } } + + /// Create a prebuilt mock for adding key storage backups via POST + pub fn mock_add_room_keys_version(&self) -> MockEndpoint<'_, AddRoomKeysVersionEndpoint> { + let mock = Mock::given(method("POST")) + .and(path_regex(r"_matrix/client/v3/room_keys/version")) + .and(header("authorization", "Bearer 1234")); + MockEndpoint { mock, server: &self.server, endpoint: AddRoomKeysVersionEndpoint } + } + + /// Create a prebuilt mock for adding key storage backups via POST + pub fn mock_delete_room_keys_version(&self) -> MockEndpoint<'_, DeleteRoomKeysVersionEndpoint> { + let mock = Mock::given(method("DELETE")) + .and(path_regex(r"_matrix/client/v3/room_keys/version/[^/]*")) + .and(header("authorization", "Bearer 1234")); + MockEndpoint { mock, server: &self.server, endpoint: DeleteRoomKeysVersionEndpoint } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -1669,7 +1685,8 @@ impl<'a> MockEndpoint<'a, PublicRoomsEndpoint> { } } -/// A prebuilt mock for `room_keys/version`: storage ("backup") of room keys. +/// A prebuilt mock for `GET room_keys/version`: storage ("backup") of room +/// keys. pub struct RoomKeysVersionEndpoint; impl<'a> MockEndpoint<'a, RoomKeysVersionEndpoint> { @@ -1713,3 +1730,34 @@ impl<'a> MockEndpoint<'a, RoomKeysVersionEndpoint> { MatrixMock { server: self.server, mock } } } + +/// A prebuilt mock for `POST room_keys/version`: adding room key backups. +pub struct AddRoomKeysVersionEndpoint; + +impl<'a> MockEndpoint<'a, AddRoomKeysVersionEndpoint> { + /// Returns an endpoint that may be used to add room key backups + pub fn ok(self) -> MatrixMock<'a> { + let mock = self + .mock + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "version": "1" + }))) + .named("POST for the backup creation"); + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for `DELETE room_keys/version/xxx`: deleting room key +/// backups. +pub struct DeleteRoomKeysVersionEndpoint; + +impl<'a> MockEndpoint<'a, DeleteRoomKeysVersionEndpoint> { + /// Returns an endpoint that allows deleting room key backups + pub fn ok(self) -> MatrixMock<'a> { + let mock = self + .mock + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .named("DELETE for the backup deletion"); + MatrixMock { server: self.server, mock } + } +} From 50eb46dc821494257df7562546f0b551724280d8 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Dec 2024 11:45:42 +0000 Subject: [PATCH 718/979] refactor(key_backups): Rename exists_on_server to fetch_exists_on_server --- bindings/matrix-sdk-ffi/src/encryption.rs | 2 +- .../matrix-sdk/src/encryption/backups/mod.rs | 44 +++++++++---------- .../src/encryption/backups/types.rs | 3 +- .../src/encryption/recovery/futures.rs | 2 +- .../matrix-sdk/src/encryption/recovery/mod.rs | 4 +- crates/matrix-sdk/src/test_utils/mocks.rs | 2 +- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index ab54911b6c4..1ec9547cd92 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -254,7 +254,7 @@ impl Encryption { /// Therefore it is necessary to poll the server for an answer every time /// you want to differentiate between those two states. pub async fn backup_exists_on_server(&self) -> Result { - Ok(self.inner.backups().exists_on_server().await?) + Ok(self.inner.backups().fetch_exists_on_server().await?) } pub fn recovery_state(&self) -> RecoveryState { diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index 23fa8cb5b3e..0e7800ff9dc 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -386,8 +386,8 @@ impl Backups { /// Does a backup exist on the server? /// /// This method will request info about the current backup from the - /// homeserver and if a backup exits return `true`, otherwise `false`. - pub async fn exists_on_server(&self) -> Result { + /// homeserver and if a backup exists return `true`, otherwise `false`. + pub async fn fetch_exists_on_server(&self) -> Result { let exists_on_server = self.get_current_version().await?.is_some(); self.client.inner.e2ee.backup_state.set_backup_exists_on_server(exists_on_server); Ok(exists_on_server) @@ -395,23 +395,23 @@ impl Backups { /// Does a backup exist on the server? /// - /// This method is identical to [`Self::exists_on_server`] except that we - /// cache the latest answer in memory and only empty the cache if the local - /// device adds or deletes a backup itself. + /// This method is identical to [`Self::fetch_exists_on_server`] except that + /// we cache the latest answer in memory and only empty the cache if the + /// local device adds or deletes a backup itself. /// /// Do not use this method if you need an accurate answer about whether a - /// backup exists - instead use [`Self::exists_on_server`]. This method is - /// useful when performance is more important than guaranteed accuracy, - /// such as when classifying UTDs. + /// backup exists - instead use [`Self::fetch_exists_on_server`]. This + /// method is useful when performance is more important than guaranteed + /// accuracy, such as when classifying UTDs. pub async fn fast_exists_on_server(&self) -> Result { // If we have an answer cached, return it immediately if let Some(cached_value) = self.client.inner.e2ee.backup_state.backup_exists_on_server() { return Ok(cached_value); } - // Otherwise, delegate to exists_on_server. (It will update the cached value for - // us.) - self.exists_on_server().await + // Otherwise, delegate to fetch_exists_on_server. (It will update the cached + // value for us.) + self.fetch_exists_on_server().await } /// Subscribe to a stream that notifies when a room key for the specified @@ -1147,7 +1147,7 @@ mod test { } #[async_test] - async fn test_when_a_backup_exists_then_exists_on_server_returns_true() { + async fn test_when_a_backup_exists_then_fetch_exists_on_server_returns_true() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1156,7 +1156,7 @@ mod test { let exists = client .encryption() .backups() - .exists_on_server() + .fetch_exists_on_server() .await .expect("We should be able to check if backups exist on the server"); @@ -1164,7 +1164,7 @@ mod test { } #[async_test] - async fn test_repeated_calls_to_exists_on_server_makes_repeated_requests() { + async fn test_repeated_calls_to_fetch_exists_on_server_makes_repeated_requests() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1173,15 +1173,15 @@ mod test { let backups = client.encryption().backups(); - // Call exists_on_server twice - backups.exists_on_server().await.unwrap(); - let exists = backups.exists_on_server().await.unwrap(); + // Call fetch_exists_on_server twice + backups.fetch_exists_on_server().await.unwrap(); + let exists = backups.fetch_exists_on_server().await.unwrap(); assert!(exists, "We should deduce that a backup exists on the server"); } #[async_test] - async fn test_when_no_backup_exists_then_exists_on_server_returns_false() { + async fn test_when_no_backup_exists_then_fetch_exists_on_server_returns_false() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1190,7 +1190,7 @@ mod test { let exists = client .encryption() .backups() - .exists_on_server() + .fetch_exists_on_server() .await .expect("We should be able to check if backups exist on the server"); @@ -1198,7 +1198,7 @@ mod test { } #[async_test] - async fn test_when_server_returns_an_error_then_exists_on_server_returns_an_error() { + async fn test_when_server_returns_an_error_then_fetch_exists_on_server_returns_an_error() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1206,7 +1206,7 @@ mod test { let _scope = server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await; - client.encryption().backups().exists_on_server().await.expect_err( + client.encryption().backups().fetch_exists_on_server().await.expect_err( "If the /version endpoint returns a non 404 error we should throw an error", ); } @@ -1215,7 +1215,7 @@ mod test { let _scope = server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await; - client.encryption().backups().exists_on_server().await.expect_err( + client.encryption().backups().fetch_exists_on_server().await.expect_err( "If the /version endpoint returns a non-Matrix 404 error we should throw an error", ); } diff --git a/crates/matrix-sdk/src/encryption/backups/types.rs b/crates/matrix-sdk/src/encryption/backups/types.rs index 9d6c089f503..7f338f4ebfd 100644 --- a/crates/matrix-sdk/src/encryption/backups/types.rs +++ b/crates/matrix-sdk/src/encryption/backups/types.rs @@ -126,7 +126,8 @@ pub enum BackupState { /// The reason we don't know whether a server-side backup exists is that we /// don't get notified by the server about the creation and deletion of /// backups. If we want to know the current state, we need to poll the - /// server, which is done using the [`Backups::exists_on_server()`] method. + /// server, which is done using the [`Backups::fetch_exists_on_server()`] + /// method. #[default] Unknown, /// A new backup is being created by this [`Client`]. This state will be diff --git a/crates/matrix-sdk/src/encryption/recovery/futures.rs b/crates/matrix-sdk/src/encryption/recovery/futures.rs index 61acd9182b4..4317f95f972 100644 --- a/crates/matrix-sdk/src/encryption/recovery/futures.rs +++ b/crates/matrix-sdk/src/encryption/recovery/futures.rs @@ -88,7 +88,7 @@ impl<'a> IntoFuture for Enable<'a> { let future = async move { if !recovery.client.encryption().backups().are_enabled().await { - if recovery.client.encryption().backups().exists_on_server().await? { + if recovery.client.encryption().backups().fetch_exists_on_server().await? { return Err(RecoveryError::BackupExistsOnServer); } else { progress.set(EnableProgress::CreatingBackup); diff --git a/crates/matrix-sdk/src/encryption/recovery/mod.rs b/crates/matrix-sdk/src/encryption/recovery/mod.rs index 214e6cb42b6..18d0a53a3e1 100644 --- a/crates/matrix-sdk/src/encryption/recovery/mod.rs +++ b/crates/matrix-sdk/src/encryption/recovery/mod.rs @@ -243,7 +243,7 @@ impl Recovery { /// ``` #[instrument(skip_all)] pub async fn enable_backup(&self) -> Result<()> { - if !self.client.encryption().backups().exists_on_server().await? { + if !self.client.encryption().backups().fetch_exists_on_server().await? { self.mark_backup_as_enabled().await?; self.client.encryption().backups().create().await?; @@ -503,7 +503,7 @@ impl Recovery { // disabled, then we can automatically enable them. Ok(self.client.inner.e2ee.encryption_settings.auto_enable_backups && !self.client.encryption().backups().are_enabled().await - && !self.client.encryption().backups().exists_on_server().await? + && !self.client.encryption().backups().fetch_exists_on_server().await? && !self.are_backups_marked_as_disabled().await?) } diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index da51b8388b6..74d25a5ac01 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -579,7 +579,7 @@ impl MatrixMockServer { /// mock_server.mock_room_keys_version().exists().expect(1).mount().await; /// /// let exists = - /// client.encryption().backups().exists_on_server().await.unwrap(); + /// client.encryption().backups().fetch_exists_on_server().await.unwrap(); /// /// assert!(exists); /// # }); From 5721c3622df57363518b404fb2da36d84de2e053 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Dec 2024 11:46:55 +0000 Subject: [PATCH 719/979] refactor(key_backups): Rename fast_exists_on_server to exists_on_server --- .../matrix-sdk/src/encryption/backups/mod.rs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index 0e7800ff9dc..f7b6bc921bc 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -403,7 +403,7 @@ impl Backups { /// backup exists - instead use [`Self::fetch_exists_on_server`]. This /// method is useful when performance is more important than guaranteed /// accuracy, such as when classifying UTDs. - pub async fn fast_exists_on_server(&self) -> Result { + pub async fn exists_on_server(&self) -> Result { // If we have an answer cached, return it immediately if let Some(cached_value) = self.client.inner.e2ee.backup_state.backup_exists_on_server() { return Ok(cached_value); @@ -660,6 +660,9 @@ impl Backups { } }; + // If the request succeeded, the backup is gone. If it failed, we are not really + // sure what the backup state is. Either way, clear the cache so we check next + // time we need to know. self.client.inner.e2ee.backup_state.clear_backup_exists_on_server(); ret @@ -1222,7 +1225,7 @@ mod test { } #[async_test] - async fn test_when_a_backup_exists_then_fast_exists_on_server_returns_true() { + async fn test_when_a_backup_exists_then_exists_on_server_returns_true() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1231,7 +1234,7 @@ mod test { let exists = client .encryption() .backups() - .fast_exists_on_server() + .exists_on_server() .await .expect("We should be able to check if backups exist on the server"); @@ -1239,7 +1242,7 @@ mod test { } #[async_test] - async fn test_when_no_backup_exists_then_fast_exists_on_server_returns_false() { + async fn test_when_no_backup_exists_then_exists_on_server_returns_false() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1248,7 +1251,7 @@ mod test { let exists = client .encryption() .backups() - .fast_exists_on_server() + .exists_on_server() .await .expect("We should be able to check if backups exist on the server"); @@ -1256,7 +1259,7 @@ mod test { } #[async_test] - async fn test_when_server_returns_an_error_then_fast_exists_on_server_returns_an_error() { + async fn test_when_server_returns_an_error_then_exists_on_server_returns_an_error() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1264,7 +1267,7 @@ mod test { let _scope = server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await; - client.encryption().backups().fast_exists_on_server().await.expect_err( + client.encryption().backups().exists_on_server().await.expect_err( "If the /version endpoint returns a non 404 error we should throw an error", ); } @@ -1273,14 +1276,14 @@ mod test { let _scope = server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await; - client.encryption().backups().fast_exists_on_server().await.expect_err( + client.encryption().backups().exists_on_server().await.expect_err( "If the /version endpoint returns a non-Matrix 404 error we should throw an error", ); } } #[async_test] - async fn test_repeated_calls_to_fast_exists_on_server_do_not_make_additional_requests() { + async fn test_repeated_calls_to_exists_on_server_do_not_make_additional_requests() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -1289,13 +1292,13 @@ mod test { let backups = client.encryption().backups(); - // Call fast_exists_on_server several times - backups.fast_exists_on_server().await.unwrap(); - backups.fast_exists_on_server().await.unwrap(); - backups.fast_exists_on_server().await.unwrap(); + // Call exists_on_server several times + backups.exists_on_server().await.unwrap(); + backups.exists_on_server().await.unwrap(); + backups.exists_on_server().await.unwrap(); let exists = backups - .fast_exists_on_server() + .exists_on_server() .await .expect("We should be able to check if backups exist on the server"); @@ -1305,7 +1308,7 @@ mod test { } #[async_test] - async fn test_adding_a_backup_invalidates_fast_exists_on_server_cache() { + async fn test_adding_a_backup_invalidates_exists_on_server_cache() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; let backups = client.encryption().backups(); @@ -1313,8 +1316,8 @@ mod test { { let _scope = server.mock_room_keys_version().none().expect(1).mount_as_scoped().await; - // Call fast_exists_on_server to fill the cache - let exists = backups.fast_exists_on_server().await.unwrap(); + // Call exists_on_server to fill the cache + let exists = backups.exists_on_server().await.unwrap(); assert!(!exists, "No backup exists at this point"); } @@ -1324,7 +1327,7 @@ mod test { server.mock_room_keys_version().exists().expect(1).mount().await; let exists = backups - .fast_exists_on_server() + .exists_on_server() .await .expect("We should be able to check if backups exist on the server"); @@ -1332,7 +1335,7 @@ mod test { } #[async_test] - async fn test_removing_a_backup_invalidates_fast_exists_on_server_cache() { + async fn test_removing_a_backup_invalidates_exists_on_server_cache() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; let backups = client.encryption().backups(); @@ -1340,8 +1343,8 @@ mod test { { let _scope = server.mock_room_keys_version().exists().expect(1).mount_as_scoped().await; - // Call fast_exists_on_server to fill the cache - let exists = backups.fast_exists_on_server().await.unwrap(); + // Call exists_on_server to fill the cache + let exists = backups.exists_on_server().await.unwrap(); assert!(exists, "A backup exists at this point"); } @@ -1351,7 +1354,7 @@ mod test { server.mock_room_keys_version().none().expect(1).mount().await; let exists = backups - .fast_exists_on_server() + .exists_on_server() .await .expect("We should be able to check if backups exist on the server"); From 72fcc50f801314684ddf8b42d527b67bf590ce97 Mon Sep 17 00:00:00 2001 From: Jonas Richard Richter Date: Tue, 10 Dec 2024 11:03:31 +0100 Subject: [PATCH 720/979] feat(ffi): Expose the method to send custom events with JSON content (#4390) This patch adds the Room::send_raw method to the bindings, making it usable from e.g. Swift. Signed-off-by: Jonas Richard Richter --- bindings/matrix-sdk-ffi/CHANGELOG.md | 1 + bindings/matrix-sdk-ffi/src/room.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 922f8f305c7..e41d3540d1e 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -33,3 +33,4 @@ Additions: - Add `Encryption::get_user_identity` which returns `UserIdentity` - Add `ClientBuilder::room_key_recipient_strategy` +- Add `Room::send_raw` diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index eeb6eb4af17..47712d0ddf8 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -378,6 +378,22 @@ impl Room { Ok(()) } + /// Send a raw event to the room. + /// + /// # Arguments + /// + /// * `event_type` - The type of the event to send. + /// + /// * `content` - The content of the event to send encoded as JSON string. + pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> { + let content_json: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?; + + self.inner.send_raw(&event_type, content_json).await?; + + Ok(()) + } + /// Redacts an event from the room. /// /// # Arguments From 68cb85a2b2403d14ad7e3b1f3fb08de7ebcb85cb Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 15:45:09 +0100 Subject: [PATCH 721/979] refactor(event cache store): use a single transaction to handle all linked chunk updates at once Instead of one transaction per update. This ensures that if a single update fails, then none is taken into account. --- .../src/event_cache_store.rs | 215 +++++++++--------- 1 file changed, 104 insertions(+), 111 deletions(-) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 0a0cbe30609..2e95141232f 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -416,25 +416,27 @@ impl EventCacheStore for SqliteEventCacheStore { room_id: &RoomId, updates: Vec>, ) -> Result<(), Self::Error> { + // Use a single transaction throughout this function, so that either all updates + // work, or none is taken into account. let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, room_id); + let room_id = room_id.to_owned(); + let this = self.clone(); - for up in updates { - match up { - Update::NewItemsChunk { previous, new, next } => { - let hashed_room_id = hashed_room_id.clone(); - - let previous = previous.as_ref().map(ChunkIdentifier::index); - let new = new.index(); - let next = next.as_ref().map(ChunkIdentifier::index); - - trace!( - %room_id, - "new events chunk (prev={previous:?}, i={new}, next={next:?})", - ); + self.acquire() + .await? + .with_transaction(move |txn| -> Result<_, Self::Error> { + for up in updates { + match up { + Update::NewItemsChunk { previous, new, next } => { + let previous = previous.as_ref().map(ChunkIdentifier::index); + let new = new.index(); + let next = next.as_ref().map(ChunkIdentifier::index); + + trace!( + %room_id, + "new events chunk (prev={previous:?}, i={new}, next={next:?})", + ); - self.acquire() - .await? - .with_transaction(move |txn| { insert_chunk( txn, &hashed_room_id, @@ -442,29 +444,22 @@ impl EventCacheStore for SqliteEventCacheStore { new, next, CHUNK_TYPE_EVENT_TYPE_STRING, - ) - }) - .await?; - } - - Update::NewGapChunk { previous, new, next, gap } => { - let hashed_room_id = hashed_room_id.clone(); + )?; + } - let serialized = serde_json::to_vec(&gap.prev_token)?; - let prev_token = self.encode_value(serialized)?; + Update::NewGapChunk { previous, new, next, gap } => { + let serialized = serde_json::to_vec(&gap.prev_token)?; + let prev_token = this.encode_value(serialized)?; - let previous = previous.as_ref().map(ChunkIdentifier::index); - let new = new.index(); - let next = next.as_ref().map(ChunkIdentifier::index); + let previous = previous.as_ref().map(ChunkIdentifier::index); + let new = new.index(); + let next = next.as_ref().map(ChunkIdentifier::index); - trace!( - %room_id, - "new gap chunk (prev={previous:?}, i={new}, next={next:?})", - ); + trace!( + %room_id, + "new gap chunk (prev={previous:?}, i={new}, next={next:?})", + ); - self.acquire() - .await? - .with_transaction(move |txn| -> rusqlite::Result<()> { // Insert the chunk as a gap. insert_chunk( txn, @@ -481,23 +476,15 @@ impl EventCacheStore for SqliteEventCacheStore { INSERT INTO gaps(chunk_id, room_id, prev_token) VALUES (?, ?, ?) "#, - (new, hashed_room_id, prev_token), + (new, &hashed_room_id, prev_token), )?; + } - Ok(()) - }) - .await?; - } - - Update::RemoveChunk(chunk_identifier) => { - let hashed_room_id = hashed_room_id.clone(); - let chunk_id = chunk_identifier.index(); + Update::RemoveChunk(chunk_identifier) => { + let chunk_id = chunk_identifier.index(); - trace!(%room_id, "removing chunk @ {chunk_id}"); + trace!(%room_id, "removing chunk @ {chunk_id}"); - self.acquire() - .await? - .with_transaction(move |txn| -> rusqlite::Result<()> { // Find chunk to delete. let (previous, next): (Option, Option) = txn.query_row( "SELECT previous, next FROM linked_chunks WHERE id = ? AND room_id = ?", @@ -517,30 +504,19 @@ impl EventCacheStore for SqliteEventCacheStore { // Now delete it, and let cascading delete corresponding entries in the // other data tables. - txn.execute("DELETE FROM linked_chunks WHERE id = ? AND room_id = ?", (chunk_id, hashed_room_id))?; + txn.execute("DELETE FROM linked_chunks WHERE id = ? AND room_id = ?", (chunk_id, &hashed_room_id))?; + } - Ok(()) - }) - .await?; - } + Update::PushItems { at, items } => { + let chunk_id = at.chunk_identifier().index(); - Update::PushItems { at, items } => { - let chunk_id = at.chunk_identifier().index(); - let hashed_room_id = hashed_room_id.clone(); + trace!(%room_id, "pushing items @ {chunk_id}"); - trace!(%room_id, "pushing items @ {chunk_id}"); - - let this = self.clone(); - - self.acquire() - .await? - .with_transaction(move |txn| -> Result<(), Self::Error> { for (i, event) in items.into_iter().enumerate() { let serialized = serde_json::to_vec(&event)?; let content = this.encode_value(serialized)?; - let event_id = - event.event_id().map(|event_id| event_id.to_string()); + let event_id = event.event_id().map(|event_id| event_id.to_string()); let index = at.index() + i; txn.execute( @@ -551,27 +527,18 @@ impl EventCacheStore for SqliteEventCacheStore { (chunk_id, &hashed_room_id, event_id, content, index), )?; } + } - Ok(()) - }) - .await?; - } + Update::RemoveItem { at } => { + let chunk_id = at.chunk_identifier().index(); + let index = at.index(); - Update::RemoveItem { at } => { - let hashed_room_id = hashed_room_id.clone(); - let chunk_id = at.chunk_identifier().index(); - let index = at.index(); + trace!(%room_id, "removing item @ {chunk_id}:{index}"); - trace!(%room_id, "removing item @ {chunk_id}:{index}"); - - self.acquire() - .await? - .with_transaction(move |txn| -> rusqlite::Result<()> { // Remove the entry. txn.execute("DELETE FROM events WHERE room_id = ? AND chunk_id = ? AND position = ?", (&hashed_room_id, chunk_id, index))?; - // Decrement the index of each item after the one we're going to - // remove. + // Decrement the index of each item after the one we're going to remove. txn.execute( r#" UPDATE events @@ -581,50 +548,37 @@ impl EventCacheStore for SqliteEventCacheStore { (&hashed_room_id, chunk_id, index) )?; - Ok(()) - }) - .await?; - } + } - Update::DetachLastItems { at } => { - let hashed_room_id = hashed_room_id.clone(); - let chunk_id = at.chunk_identifier().index(); - let index = at.index(); + Update::DetachLastItems { at } => { + let chunk_id = at.chunk_identifier().index(); + let index = at.index(); - trace!(%room_id, "truncating items >= {chunk_id}:{index}"); + trace!(%room_id, "truncating items >= {chunk_id}:{index}"); - self.acquire() - .await? - .with_transaction(move |txn| -> rusqlite::Result<()> { // Remove these entries. txn.execute("DELETE FROM events WHERE room_id = ? AND chunk_id = ? AND position >= ?", (&hashed_room_id, chunk_id, index))?; - Ok(()) - }) - .await?; - } - - Update::Clear => { - let hashed_room_id = hashed_room_id.clone(); + } - trace!(%room_id, "clearing items"); + Update::Clear => { + trace!(%room_id, "clearing items"); - self.acquire() - .await? - .with_transaction(move |txn| { // Remove chunks, and let cascading do its job. txn.execute( "DELETE FROM linked_chunks WHERE room_id = ?", (&hashed_room_id,), - ) - }) - .await?; - } + )?; + } - Update::StartReattachItems | Update::EndReattachItems => { - // Nothing. + Update::StartReattachItems | Update::EndReattachItems => { + // Nothing. + } + } } - } - } + + Ok(()) + }) + .await?; Ok(()) } @@ -1414,6 +1368,45 @@ mod tests { check_test_event(&events[0], "beaufort is the best"); }); } + + #[async_test] + async fn test_linked_chunk_update_is_a_transaction() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = *DEFAULT_TEST_ROOM_ID; + + // Trigger a violation of the unique constraint on the (room id, chunk id) + // couple. + let err = store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + ], + ) + .await + .unwrap_err(); + + // The operation fails with a constraint violation error. + assert_matches!(err, crate::error::Error::Sqlite(err) => { + assert_matches!(err.sqlite_error_code(), Some(rusqlite::ErrorCode::ConstraintViolation)); + }); + + // If the updates have been handled transactionally, then no new chunks should + // have been added; failure of the second update leads to the first one being + // rolled back. + let chunks = store.load_chunks(room_id).await.unwrap(); + assert!(chunks.is_empty()); + } } #[cfg(test)] From a2210bce4887ed37e964c4adb5a31a68d0eed74a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 10:45:48 +0100 Subject: [PATCH 722/979] refactor(ui): Add `EventMeta::timeline_item_index`. This is the foundation for the mapping between remote events and timeline items. --- .../src/timeline/controller/state.rs | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 1800bebdab7..96283554832 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -1199,7 +1199,11 @@ pub(crate) struct FullEventMeta<'a> { impl FullEventMeta<'_> { fn base_meta(&self) -> EventMeta { - EventMeta { event_id: self.event_id.to_owned(), visible: self.visible } + EventMeta { + event_id: self.event_id.to_owned(), + visible: self.visible, + timeline_item_index: None, + } } } @@ -1208,6 +1212,70 @@ impl FullEventMeta<'_> { pub(crate) struct EventMeta { /// The ID of the event. pub event_id: OwnedEventId, + /// Whether the event is among the timeline items. pub visible: bool, + + /// Foundation for the mapping between remote events to timeline items. + /// + /// Let's explain it. The events represent the first set and are stored in + /// [`TimelineMetadata::all_remote_events`], and the timeline + /// items represent the second set and are stored in + /// [`TimelineState::items`]. + /// + /// Each event is mapped to at most one timeline item: + /// + /// - `None` if the event isn't rendered in the timeline (e.g. some state + /// events, or malformed events) or is rendered as a timeline item that + /// attaches to or groups with another item, like reactions, + /// - `Some(_)` if the event is rendered in the timeline. + /// + /// This is neither a surjection nor an injection. Every timeline item may + /// not be attached to an event, for example with a virtual timeline item. + /// We can formulate other rules: + /// + /// - a timeline item that doesn't _move_ and that is represented by an + /// event has a mapping to an event, + /// - a virtual timeline item has no mapping to an event. + /// + /// Imagine the following remote events: + /// + /// | index | remote events | + /// +-------+---------------+ + /// | 0 | `$ev0` | + /// | 1 | `$ev1` | + /// | 2 | `$ev2` | + /// | 3 | `$ev3` | + /// | 4 | `$ev4` | + /// | 5 | `$ev5` | + /// + /// Once rendered in a timeline, it for example produces: + /// + /// | index | item | aside items | + /// +-------+-------------------+----------------------+ + /// | 0 | content of `$ev0` | | + /// | 1 | content of `$ev2` | reaction with `$ev4` | + /// | 2 | day divider | | + /// | 3 | content of `$ev3` | | + /// | 4 | content of `$ev5` | | + /// + /// Note the day divider that is a virtual item. Also note ``$ev4`` which is + /// a reaction to `$ev2`. Finally note that `$ev1` is not rendered in + /// the timeline. + /// + /// The mapping between remove event index to timeline item index will look + /// like this: + /// + /// | remove event index | timeline item index | comment | + /// +--------------------+---------------------+--------------------------------------------+ + /// | 0 | `Some(0)` | `$ev0` is rendered as the #0 timeline item | + /// | 1 | `None` | `$ev1` isn't rendered in the timeline | + /// | 2 | `Some(1)` | `$ev2` is rendered as the #1 timeline item | + /// | 3 | `Some(3)` | `$ev3` is rendered as the #3 timeline item | + /// | 4 | `None` | `$ev4` is a reaction to item #1 | + /// | 5 | `Some(4)` | `$ev5` is rendered as the #4 timeline item | + /// + /// Note that the #2 timeline item (the day divider) doesn't map to any + /// remote event, but if it moves, it has an impact on this mapping. + pub timeline_item_index: Option, } From 14d0f6877a6fd43f3226691c360c31a4dc3c4209 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 10:53:20 +0100 Subject: [PATCH 723/979] refactor(ui): Maintain `timeline_item_index` when remote events are manipulated. This patch maintains the `timeline_item_index` when a new remote events is added or removed. --- .../src/timeline/controller/state.rs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 96283554832..1db916cff31 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -1157,17 +1157,40 @@ impl AllRemoteEvents { /// Insert a new remote event at the front of all the others. pub fn push_front(&mut self, event_meta: EventMeta) { + // If there is an associated `timeline_item_index`, shift all the + // `timeline_item_index` that come after this one. + if let Some(new_timeline_item_index) = event_meta.timeline_item_index { + self.increment_all_timeline_item_index_after(new_timeline_item_index); + } + + // Push the event. self.0.push_front(event_meta) } /// Insert a new remote event at the back of all the others. pub fn push_back(&mut self, event_meta: EventMeta) { + // If there is an associated `timeline_item_index`, shift all the + // `timeline_item_index` that come after this one. + if let Some(new_timeline_item_index) = event_meta.timeline_item_index { + self.increment_all_timeline_item_index_after(new_timeline_item_index); + } + + // Push the event. self.0.push_back(event_meta) } /// Remove one remote event at a specific index, and return it if it exists. pub fn remove(&mut self, event_index: usize) -> Option { - self.0.remove(event_index) + // Remove the event. + let event_meta = self.0.remove(event_index)?; + + // If there is an associated `timeline_item_index`, shift all the + // `timeline_item_index` that come after this one. + if let Some(removed_timeline_item_index) = event_meta.timeline_item_index { + self.decrement_all_timeline_item_index_after(removed_timeline_item_index); + }; + + Some(event_meta) } /// Return a reference to the last remote event if it exists. @@ -1179,6 +1202,30 @@ impl AllRemoteEvents { pub fn get_by_event_id_mut(&mut self, event_id: &EventId) -> Option<&mut EventMeta> { self.0.iter_mut().rev().find(|event_meta| event_meta.event_id == event_id) } + + /// Shift to the right all timeline item indexes that are equal to or + /// greater than `new_timeline_item_index`. + fn increment_all_timeline_item_index_after(&mut self, new_timeline_item_index: usize) { + for event_meta in self.0.iter_mut() { + if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { + if *timeline_item_index >= new_timeline_item_index { + *timeline_item_index += 1; + } + } + } + } + + /// Shift to the left all timeline item indexes that are greater than + /// `removed_wtimeline_item_index`. + fn decrement_all_timeline_item_index_after(&mut self, removed_timeline_item_index: usize) { + for event_meta in self.0.iter_mut() { + if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { + if *timeline_item_index > removed_timeline_item_index { + *timeline_item_index -= 1; + } + } + } + } } /// Full metadata about an event. From 91b73a2b16b1a250e373c24e54660811cfd35145 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 11:17:07 +0100 Subject: [PATCH 724/979] refactor(ui): Maintain `timeline_item_index` when timeline items are inserted or removed. This patch maintains the `timeline_item_index` when timeline items are inserted or removed. --- .../src/timeline/controller/state.rs | 55 +++++++++++++- .../src/timeline/event_handler.rs | 72 ++++++++++++++----- 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 1db916cff31..92fb2f07500 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::{ + cmp::Ordering, collections::{vec_deque::Iter, HashMap, VecDeque}, future::Future, num::NonZeroUsize, @@ -1092,7 +1093,9 @@ impl TimelineMetadata { (None, Some(idx)) => { // Only insert the read marker if it is not at the end of the timeline. if idx + 1 < items.len() { - items.insert(idx + 1, TimelineItem::read_marker()); + let idx = idx + 1; + items.insert(idx, TimelineItem::read_marker()); + self.all_remote_events.timeline_item_has_been_inserted_at(idx, None); self.has_up_to_date_read_marker_item = true; } else { // The next event might require a read marker to be inserted at the current @@ -1113,6 +1116,7 @@ impl TimelineMetadata { if from + 1 == items.len() { // The read marker has nothing after it. An item disappeared; remove it. items.remove(from); + self.all_remote_events.timeline_item_has_been_removed_at(from); } self.has_up_to_date_read_marker_item = true; return; @@ -1120,6 +1124,7 @@ impl TimelineMetadata { let prev_len = items.len(); let read_marker = items.remove(from); + self.all_remote_events.timeline_item_has_been_removed_at(from); // Only insert the read marker if it is not at the end of the timeline. if to + 1 < prev_len { @@ -1127,6 +1132,7 @@ impl TimelineMetadata { // by one position by the remove call above, insert the fully- // read marker at its previous position, rather than that + 1 items.insert(to, read_marker); + self.all_remote_events.timeline_item_has_been_inserted_at(to, None); self.has_up_to_date_read_marker_item = true; } else { self.has_up_to_date_read_marker_item = false; @@ -1198,6 +1204,11 @@ impl AllRemoteEvents { self.0.back() } + /// Return the index of the last remote event if it exists. + pub fn last_index(&self) -> Option { + self.0.len().checked_sub(1) + } + /// Get a mutable reference to a specific remote event by its ID. pub fn get_by_event_id_mut(&mut self, event_id: &EventId) -> Option<&mut EventMeta> { self.0.iter_mut().rev().find(|event_meta| event_meta.event_id == event_id) @@ -1226,6 +1237,48 @@ impl AllRemoteEvents { } } } + + pub fn timeline_item_has_been_inserted_at( + &mut self, + new_timeline_item_index: usize, + event_index: Option, + ) { + self.increment_all_timeline_item_index_after(new_timeline_item_index); + + if let Some(event_index) = event_index { + if let Some(event_meta) = self.0.get_mut(event_index) { + event_meta.timeline_item_index = Some(new_timeline_item_index); + } + } + } + + pub fn timeline_item_has_been_removed_at(&mut self, timeline_item_index_to_remove: usize) { + for event_meta in self.0.iter_mut() { + let mut remove_timeline_item_index = false; + + // A `timeline_item_index` is removed. Let's shift all indexes that come + // after the removed one. + if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { + match (*timeline_item_index).cmp(&timeline_item_index_to_remove) { + Ordering::Equal => { + remove_timeline_item_index = true; + } + + Ordering::Greater => { + *timeline_item_index -= 1; + } + + Ordering::Less => {} + } + } + + // This is the `event_meta` that holds the `timeline_item_index` that is being + // removed. So let's clean it. + if remove_timeline_item_index { + event_meta.timeline_item_index = None; + } + } + } } /// Full metadata about an event. diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index cc01448714b..bcb312f0274 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -504,14 +504,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("No new item added"); if let Flow::Remote { - position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx }, + position: TimelineItemPosition::UpdateDecrypted { timeline_item_index }, .. } = self.ctx.flow { // If add was not called, that means the UTD event is one that // wouldn't normally be visible. Remove it. trace!("Removing UTD that was successfully retried"); - self.items.remove(idx); + self.items.remove(timeline_item_index); + self.meta.all_remote_events.timeline_item_has_been_removed_at(timeline_item_index); self.result.item_removed = true; } @@ -1010,6 +1011,16 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } /// Add a new event item in the timeline. + /// + /// # Safety + /// + /// This method is not marked as unsafe **but** it manipulates + /// [`TimelineMetadata::all_remote_events`]. 2 rules **must** be respected: + /// + /// 1. the remote event of the item being added **must** be present in + /// `all_remote_events`, + /// 2. the lastly added or updated remote event must be associated to the + /// timeline item being added here. fn add_item( &mut self, content: TimelineItemContent, @@ -1100,6 +1111,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let item = self.meta.new_timeline_item(item); self.items.push_front(item); + self.meta.all_remote_events.timeline_item_has_been_inserted_at(0, Some(0)); } Flow::Remote { @@ -1153,19 +1165,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // will run to re-add the removed item } - // Local echoes that are pending should stick to the bottom, - // find the latest event that isn't that. - let latest_event_idx = self - .items - .iter() - .enumerate() - .rev() - .find_map(|(idx, item)| (!item.as_event()?.is_local_echo()).then_some(idx)); - - // Insert the next item after the latest event item that's not a - // pending local echo, or at the start if there is no such item. - let insert_idx = latest_event_idx.map_or(0, |idx| idx + 1); - trace!("Adding new remote timeline item after all non-pending events"); let new_item = match removed_event_item_id { // If a previous version of the same item (usually a local @@ -1175,14 +1174,49 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { None => self.meta.new_timeline_item(item), }; - // Keep push semantics, if we're inserting at the front or the back. - if insert_idx == self.items.len() { + // Local events are always at the bottom. Let's find the latest remote event + // and insert after it, otherwise, if there is no remote event, insert at 0. + let timeline_item_index = self + .items + .iter() + .enumerate() + .rev() + .find_map(|(timeline_item_index, timeline_item)| { + (!timeline_item.as_event()?.is_local_echo()) + .then_some(timeline_item_index + 1) + }) + .unwrap_or(0); + + // Try to keep precise insertion semantics here, in this exact order: + // + // * _push back_ when the new item is inserted after all items (the assumption + // being that this is the hot path, because most of the time new events + // come from the sync), + // * _push front_ when the new item is inserted at index 0, + // * _insert_ otherwise. + + if timeline_item_index == self.items.len() { + trace!("Adding new remote timeline item at the back"); self.items.push_back(new_item); - } else if insert_idx == 0 { + } else if timeline_item_index == 0 { + trace!("Adding new remote timeline item at the front"); self.items.push_front(new_item); } else { - self.items.insert(insert_idx, new_item); + trace!( + timeline_item_index, + "Adding new remote timeline item at specific index" + ); + self.items.insert(timeline_item_index, new_item); } + + self.meta.all_remote_events.timeline_item_has_been_inserted_at( + timeline_item_index, + Some(self.meta.all_remote_events.last_index() + // The last remote event is necessarily associated to this + // timeline item, see the contract of this method. + .expect("A timeline item is being added but its associated remote event is missing") + ), + ); } Flow::Remote { From b069b20e187f65744fd8ba255ffae282054f8163 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 15:01:44 +0100 Subject: [PATCH 725/979] refactor(ui): Create `ObservableItems(Transaction)`. --- .../src/timeline/controller/mod.rs | 12 +- .../timeline/controller/observable_items.rs | 142 ++++++++++++++++++ .../src/timeline/controller/state.rs | 24 ++- .../src/timeline/day_dividers.rs | 39 ++--- .../src/timeline/event_handler.rs | 10 +- .../src/timeline/read_receipts.rs | 18 +-- 6 files changed, 190 insertions(+), 55 deletions(-) create mode 100644 crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 13638ed6230..d6b55a4abf9 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -51,9 +51,14 @@ use tracing::{ debug, error, field, field::debug, info, info_span, instrument, trace, warn, Instrument as _, }; -pub(super) use self::state::{ - AllRemoteEvents, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, - TimelineNewItemPosition, TimelineState, TimelineStateTransaction, +#[cfg(test)] +pub(super) use self::observable_items::ObservableItems; +pub(super) use self::{ + observable_items::ObservableItemsTransaction, + state::{ + AllRemoteEvents, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, + TimelineNewItemPosition, TimelineState, TimelineStateTransaction, + }, }; use super::{ event_handler::TimelineEventKind, @@ -77,6 +82,7 @@ use crate::{ unable_to_decrypt_hook::UtdHookManager, }; +mod observable_items; mod state; /// Data associated to the current timeline focus. diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs new file mode 100644 index 00000000000..7c5c2e4482f --- /dev/null +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -0,0 +1,142 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ops::Deref, sync::Arc}; + +use eyeball_im::{ + ObservableVector, ObservableVectorEntries, ObservableVectorEntry, ObservableVectorTransaction, + ObservableVectorTransactionEntry, VectorSubscriber, +}; +use imbl::Vector; + +use super::TimelineItem; + +#[derive(Debug)] +pub struct ObservableItems { + items: ObservableVector>, +} + +impl ObservableItems { + pub fn new() -> Self { + Self { + // Upstream default capacity is currently 16, which is making + // sliding-sync tests with 20 events lag. This should still be + // small enough. + items: ObservableVector::with_capacity(32), + } + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn subscribe(&self) -> VectorSubscriber> { + self.items.subscribe() + } + + pub fn clone(&self) -> Vector> { + self.items.clone() + } + + pub fn transaction(&mut self) -> ObservableItemsTransaction<'_> { + ObservableItemsTransaction { items: self.items.transaction() } + } + + pub fn set( + &mut self, + timeline_item_index: usize, + timeline_item: Arc, + ) -> Arc { + self.items.set(timeline_item_index, timeline_item) + } + + pub fn entries(&mut self) -> ObservableVectorEntries<'_, Arc> { + self.items.entries() + } + + pub fn for_each(&mut self, f: F) + where + F: FnMut(ObservableVectorEntry<'_, Arc>), + { + self.items.for_each(f) + } +} + +// It's fine to deref to an immutable reference to `Vector`. +impl Deref for ObservableItems { + type Target = Vector>; + + fn deref(&self) -> &Self::Target { + &self.items + } +} + +#[derive(Debug)] +pub struct ObservableItemsTransaction<'observable_items> { + items: ObservableVectorTransaction<'observable_items, Arc>, +} + +impl<'observable_items> ObservableItemsTransaction<'observable_items> { + pub fn get(&self, timeline_item_index: usize) -> Option<&Arc> { + self.items.get(timeline_item_index) + } + + pub fn set( + &mut self, + timeline_item_index: usize, + timeline_item: Arc, + ) -> Arc { + self.items.set(timeline_item_index, timeline_item) + } + + pub fn remove(&mut self, timeline_item_index: usize) -> Arc { + self.items.remove(timeline_item_index) + } + + pub fn insert(&mut self, timeline_item_index: usize, timeline_item: Arc) { + self.items.insert(timeline_item_index, timeline_item); + } + + pub fn push_front(&mut self, timeline_item: Arc) { + self.items.push_front(timeline_item); + } + + pub fn push_back(&mut self, timeline_item: Arc) { + self.items.push_back(timeline_item); + } + + pub fn clear(&mut self) { + self.items.clear(); + } + + pub fn for_each(&mut self, f: F) + where + F: FnMut(ObservableVectorTransactionEntry<'_, 'observable_items, Arc>), + { + self.items.for_each(f) + } + + pub fn commit(self) { + self.items.commit() + } +} + +// It's fine to deref to an immutable reference to `Vector`. +impl Deref for ObservableItemsTransaction<'_> { + type Target = Vector>; + + fn deref(&self) -> &Self::Target { + &self.items + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 92fb2f07500..72916c1c7bc 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -20,7 +20,7 @@ use std::{ sync::{Arc, RwLock}, }; -use eyeball_im::{ObservableVector, ObservableVectorTransaction, ObservableVectorTransactionEntry}; +use eyeball_im::ObservableVectorTransactionEntry; use itertools::Itertools as _; use matrix_sdk::{ deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer, send_queue::SendHandle, @@ -45,7 +45,10 @@ use ruma::{ }; use tracing::{debug, instrument, trace, warn}; -use super::{HandleManyEventsResult, TimelineFocusKind, TimelineSettings}; +use super::{ + observable_items::{ObservableItems, ObservableItemsTransaction}, + HandleManyEventsResult, TimelineFocusKind, TimelineSettings, +}; use crate::{ events::SyncTimelineEventWithoutContent, timeline::{ @@ -90,7 +93,7 @@ impl From for TimelineItemPosition { #[derive(Debug)] pub(in crate::timeline) struct TimelineState { - pub items: ObservableVector>, + pub items: ObservableItems, pub meta: TimelineMetadata, /// The kind of focus of this timeline. @@ -107,10 +110,7 @@ impl TimelineState { is_room_encrypted: Option, ) -> Self { Self { - // Upstream default capacity is currently 16, which is making - // sliding-sync tests with 20 events lag. This should still be - // small enough. - items: ObservableVector::with_capacity(32), + items: ObservableItems::new(), meta: TimelineMetadata::new( own_user_id, room_version, @@ -330,6 +330,7 @@ impl TimelineState { pub(super) fn transaction(&mut self) -> TimelineStateTransaction<'_> { let items = self.items.transaction(); let meta = self.meta.clone(); + TimelineStateTransaction { items, previous_meta: &mut self.meta, @@ -342,7 +343,7 @@ impl TimelineState { pub(in crate::timeline) struct TimelineStateTransaction<'a> { /// A vector transaction over the items themselves. Holds temporary state /// until committed. - pub items: ObservableVectorTransaction<'a, Arc>, + pub items: ObservableItemsTransaction<'a>, /// A clone of the previous meta, that we're operating on during the /// transaction, and that will be committed to the previous meta location in @@ -1041,10 +1042,7 @@ impl TimelineMetadata { } /// Try to update the read marker item in the timeline. - pub(crate) fn update_read_marker( - &mut self, - items: &mut ObservableVectorTransaction<'_, Arc>, - ) { + pub(crate) fn update_read_marker(&mut self, items: &mut ObservableItemsTransaction<'_>) { let Some(fully_read_event) = &self.fully_read_event else { return }; trace!(?fully_read_event, "Updating read marker"); @@ -1359,7 +1357,7 @@ pub(crate) struct EventMeta { /// | 3 | content of `$ev3` | | /// | 4 | content of `$ev5` | | /// - /// Note the day divider that is a virtual item. Also note ``$ev4`` which is + /// Note the day divider that is a virtual item. Also note `$ev4` which is /// a reaction to `$ev2`. Finally note that `$ev1` is not rendered in /// the timeline. /// diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index cbd76e8cce7..6f02196c10a 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -17,13 +17,13 @@ use std::{fmt::Display, sync::Arc}; -use eyeball_im::ObservableVectorTransaction; use ruma::MilliSecondsSinceUnixEpoch; use tracing::{error, event_enabled, instrument, trace, warn, Level}; use super::{ - controller::TimelineMetadata, util::timestamp_to_date, TimelineItem, TimelineItemKind, - VirtualTimelineItem, + controller::{ObservableItemsTransaction, TimelineMetadata}, + util::timestamp_to_date, + TimelineItem, TimelineItemKind, VirtualTimelineItem, }; /// Algorithm ensuring that day dividers are adjusted correctly, according to @@ -81,11 +81,7 @@ impl DayDividerAdjuster { /// Ensures that date separators are properly inserted/removed when needs /// be. #[instrument(skip_all)] - pub fn run( - &mut self, - items: &mut ObservableVectorTransaction<'_, Arc>, - meta: &mut TimelineMetadata, - ) { + pub fn run(&mut self, items: &mut ObservableItemsTransaction<'_>, meta: &mut TimelineMetadata) { // We're going to record vector operations like inserting, replacing and // removing day dividers. Since we may remove or insert new items, // recorded offsets will change as we're iterating over the array. The @@ -284,11 +280,7 @@ impl DayDividerAdjuster { } } - fn process_ops( - &self, - items: &mut ObservableVectorTransaction<'_, Arc>, - meta: &mut TimelineMetadata, - ) { + fn process_ops(&self, items: &mut ObservableItemsTransaction<'_>, meta: &mut TimelineMetadata) { // Record the deletion offset. let mut offset = 0i64; // Remember what the maximum index was, so we can assert that it's @@ -366,7 +358,7 @@ impl DayDividerAdjuster { /// Returns a report if and only if there was at least one error. fn check_invariants<'a, 'o>( &mut self, - items: &'a ObservableVectorTransaction<'o, Arc>, + items: &'a ObservableItemsTransaction<'o>, initial_state: Option>>, ) -> Option> { let mut report = DayDividerInvariantsReport { @@ -512,7 +504,7 @@ struct DayDividerInvariantsReport<'a, 'o> { /// The operations that have been applied on the list. operations: Vec, /// Final state after inserting the day dividers. - final_state: &'a ObservableVectorTransaction<'o, Arc>, + final_state: &'a ObservableItemsTransaction<'o>, /// Errors encountered in the algorithm. errors: Vec, } @@ -608,10 +600,9 @@ enum DayDividerInsertError { #[cfg(test)] mod tests { use assert_matches2::assert_let; - use eyeball_im::ObservableVector; use ruma::{owned_event_id, owned_user_id, uint, MilliSecondsSinceUnixEpoch}; - use super::DayDividerAdjuster; + use super::{super::controller::ObservableItems, DayDividerAdjuster}; use crate::timeline::{ controller::TimelineMetadata, event_item::{EventTimelineItemKind, RemoteEventTimelineItem}, @@ -654,7 +645,7 @@ mod tests { #[test] fn test_no_trailing_day_divider() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); @@ -688,7 +679,7 @@ mod tests { #[test] fn test_read_marker_in_between_event_and_day_divider() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); @@ -720,7 +711,7 @@ mod tests { #[test] fn test_read_marker_in_between_day_dividers() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); @@ -754,7 +745,7 @@ mod tests { #[test] fn test_remove_all_day_dividers() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); @@ -784,7 +775,7 @@ mod tests { #[test] fn test_event_read_marker_spurious_day_divider() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); @@ -810,7 +801,7 @@ mod tests { #[test] fn test_multiple_trailing_day_dividers() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); @@ -834,7 +825,7 @@ mod tests { #[test] fn test_start_with_read_marker() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index bcb312f0274..e04b0399719 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use as_variant::as_variant; -use eyeball_im::{ObservableVectorTransaction, ObservableVectorTransactionEntry}; +use eyeball_im::ObservableVectorTransactionEntry; use indexmap::IndexMap; use matrix_sdk::{ crypto::types::events::UtdCause, @@ -51,7 +51,9 @@ use ruma::{ use tracing::{debug, error, field::debug, info, instrument, trace, warn}; use super::{ - controller::{PendingEditKind, TimelineMetadata, TimelineStateTransaction}, + controller::{ + ObservableItemsTransaction, PendingEditKind, TimelineMetadata, TimelineStateTransaction, + }, day_dividers::DayDividerAdjuster, event_item::{ extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content, @@ -330,7 +332,7 @@ pub(super) struct HandleEventResult { /// existing timeline item, transforming that item or creating a new one, /// updating the reactive Vec). pub(super) struct TimelineEventHandler<'a, 'o> { - items: &'a mut ObservableVectorTransaction<'o, Arc>, + items: &'a mut ObservableItemsTransaction<'o>, meta: &'a mut TimelineMetadata, ctx: TimelineEventContext, result: HandleEventResult, @@ -1243,7 +1245,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { /// After updating the timeline item `new_item` which id is /// `target_event_id`, update other items that are responses to this item. fn maybe_update_responses( - items: &mut ObservableVectorTransaction<'_, Arc>, + items: &mut ObservableItemsTransaction<'_>, target_event_id: &EventId, new_item: &EventTimelineItem, ) { diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs index a12ebbb0967..10040230011 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{cmp::Ordering, collections::HashMap, sync::Arc}; +use std::{cmp::Ordering, collections::HashMap}; -use eyeball_im::ObservableVectorTransaction; use futures_core::Stream; use indexmap::IndexMap; use ruma::{ @@ -27,7 +26,8 @@ use tracing::{debug, error, warn}; use super::{ controller::{ - AllRemoteEvents, FullEventMeta, TimelineMetadata, TimelineState, TimelineStateTransaction, + AllRemoteEvents, FullEventMeta, ObservableItemsTransaction, TimelineMetadata, + TimelineState, TimelineStateTransaction, }, traits::RoomDataProvider, util::{rfind_event_by_id, RelativePosition}, @@ -100,7 +100,7 @@ impl ReadReceipts { new_receipt: FullReceipt<'_>, is_own_user_id: bool, all_events: &AllRemoteEvents, - timeline_items: &mut ObservableVectorTransaction<'_, Arc>, + timeline_items: &mut ObservableItemsTransaction<'_>, ) { // Get old receipt. let old_receipt = self.get_latest(new_receipt.user_id, &new_receipt.receipt_type); @@ -284,11 +284,7 @@ struct ReadReceiptTimelineUpdate { impl ReadReceiptTimelineUpdate { /// Remove the old receipt from the corresponding timeline item. - fn remove_old_receipt( - &self, - items: &mut ObservableVectorTransaction<'_, Arc>, - user_id: &UserId, - ) { + fn remove_old_receipt(&self, items: &mut ObservableItemsTransaction<'_>, user_id: &UserId) { let Some(event_id) = &self.old_event_id else { // Nothing to do. return; @@ -319,7 +315,7 @@ impl ReadReceiptTimelineUpdate { /// Add the new receipt to the corresponding timeline item. fn add_new_receipt( self, - items: &mut ObservableVectorTransaction<'_, Arc>, + items: &mut ObservableItemsTransaction<'_>, user_id: OwnedUserId, receipt: Receipt, ) { @@ -348,7 +344,7 @@ impl ReadReceiptTimelineUpdate { /// Apply this update to the timeline. fn apply( self, - items: &mut ObservableVectorTransaction<'_, Arc>, + items: &mut ObservableItemsTransaction<'_>, user_id: OwnedUserId, receipt: Receipt, ) { From 0647be1bc3fa211ab08f720d5a9d2f15c6a4eb92 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 16:46:14 +0100 Subject: [PATCH 726/979] refactor(ui): Move `AllRemoteEvents` inside `observable_items`. This patch moves `AllRemoteEvents` inside `observable_items` so that more methods can be made private, which reduces the risk of misuses of this API. In particular, the following methods are now strictly private: - `clear` - `push_front` - `push_back` - `remove` - `timeline_item_has_been_inserted_at` - `timeline_item_has_been_removed_at` In fact, now, all `&mut self` method (except `get_by_event_id_mut`) are now strictly private! --- .../src/timeline/controller/mod.rs | 43 ++-- .../timeline/controller/observable_items.rs | 215 +++++++++++++++++- .../src/timeline/controller/state.rs | 198 ++-------------- .../src/timeline/day_dividers.rs | 62 ++--- .../src/timeline/event_handler.rs | 28 +-- .../src/timeline/read_receipts.rs | 60 +++-- 6 files changed, 341 insertions(+), 265 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index d6b55a4abf9..0a58951b6a2 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -54,10 +54,10 @@ use tracing::{ #[cfg(test)] pub(super) use self::observable_items::ObservableItems; pub(super) use self::{ - observable_items::ObservableItemsTransaction, + observable_items::{AllRemoteEvents, ObservableItemsTransaction}, state::{ - AllRemoteEvents, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, - TimelineNewItemPosition, TimelineState, TimelineStateTransaction, + FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, TimelineNewItemPosition, + TimelineState, TimelineStateTransaction, }, }; use super::{ @@ -1487,13 +1487,22 @@ impl TimelineController { match receipt_type { SendReceiptType::Read => { - if let Some((old_pub_read, _)) = - state.meta.user_receipt(own_user_id, ReceiptType::Read, room).await + if let Some((old_pub_read, _)) = state + .meta + .user_receipt( + own_user_id, + ReceiptType::Read, + room, + state.items.all_remote_events(), + ) + .await { trace!(%old_pub_read, "found a previous public receipt"); - if let Some(relative_pos) = - state.meta.compare_events_positions(&old_pub_read, event_id) - { + if let Some(relative_pos) = state.meta.compare_events_positions( + &old_pub_read, + event_id, + state.items.all_remote_events(), + ) { trace!("event referred to new receipt is {relative_pos:?} the previous receipt"); return relative_pos == RelativePosition::After; } @@ -1506,9 +1515,11 @@ impl TimelineController { state.latest_user_read_receipt(own_user_id, room).await { trace!(%old_priv_read, "found a previous private receipt"); - if let Some(relative_pos) = - state.meta.compare_events_positions(&old_priv_read, event_id) - { + if let Some(relative_pos) = state.meta.compare_events_positions( + &old_priv_read, + event_id, + state.items.all_remote_events(), + ) { trace!("event referred to new receipt is {relative_pos:?} the previous receipt"); return relative_pos == RelativePosition::After; } @@ -1517,9 +1528,11 @@ impl TimelineController { SendReceiptType::FullyRead => { if let Some(prev_event_id) = self.room_data_provider.load_fully_read_marker().await { - if let Some(relative_pos) = - state.meta.compare_events_positions(&prev_event_id, event_id) - { + if let Some(relative_pos) = state.meta.compare_events_positions( + &prev_event_id, + event_id, + state.items.all_remote_events(), + ) { return relative_pos == RelativePosition::After; } } @@ -1535,7 +1548,7 @@ impl TimelineController { /// it's folded into another timeline item. pub(crate) async fn latest_event_id(&self) -> Option { let state = self.state.read().await; - state.meta.all_remote_events.last().map(|event_meta| &event_meta.event_id).cloned() + state.items.all_remote_events().last().map(|event_meta| &event_meta.event_id).cloned() } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 7c5c2e4482f..fc0a8aa6fbb 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -12,19 +12,36 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ops::Deref, sync::Arc}; +use std::{ + cmp::Ordering, + collections::{vec_deque::Iter, VecDeque}, + ops::Deref, + sync::Arc, +}; use eyeball_im::{ ObservableVector, ObservableVectorEntries, ObservableVectorEntry, ObservableVectorTransaction, ObservableVectorTransactionEntry, VectorSubscriber, }; use imbl::Vector; +use ruma::EventId; -use super::TimelineItem; +use super::{state::EventMeta, TimelineItem}; #[derive(Debug)] pub struct ObservableItems { + /// All timeline items. + /// + /// Yeah, there are here! items: ObservableVector>, + + /// List of all the remote events as received in the timeline, even the ones + /// that are discarded in the timeline items. + /// + /// This is useful to get this for the moment as it helps the `Timeline` to + /// compute read receipts and read markers. It also helps to map event to + /// timeline item, see [`EventMeta::timeline_item_index`] to learn more. + all_remote_events: AllRemoteEvents, } impl ObservableItems { @@ -34,9 +51,14 @@ impl ObservableItems { // sliding-sync tests with 20 events lag. This should still be // small enough. items: ObservableVector::with_capacity(32), + all_remote_events: AllRemoteEvents::default(), } } + pub fn all_remote_events(&self) -> &AllRemoteEvents { + &self.all_remote_events + } + pub fn is_empty(&self) -> bool { self.items.is_empty() } @@ -50,7 +72,10 @@ impl ObservableItems { } pub fn transaction(&mut self) -> ObservableItemsTransaction<'_> { - ObservableItemsTransaction { items: self.items.transaction() } + ObservableItemsTransaction { + items: self.items.transaction(), + all_remote_events: &mut self.all_remote_events, + } } pub fn set( @@ -85,6 +110,7 @@ impl Deref for ObservableItems { #[derive(Debug)] pub struct ObservableItemsTransaction<'observable_items> { items: ObservableVectorTransaction<'observable_items, Arc>, + all_remote_events: &'observable_items mut AllRemoteEvents, } impl<'observable_items> ObservableItemsTransaction<'observable_items> { @@ -92,6 +118,29 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.items.get(timeline_item_index) } + pub fn all_remote_events(&self) -> &AllRemoteEvents { + &self.all_remote_events + } + + pub fn remove_remote_event(&mut self, event_index: usize) -> Option { + self.all_remote_events.remove(event_index) + } + + pub fn push_front_remote_event(&mut self, event_meta: EventMeta) { + self.all_remote_events.push_front(event_meta); + } + + pub fn push_back_remote_event(&mut self, event_meta: EventMeta) { + self.all_remote_events.push_back(event_meta); + } + + pub fn get_remote_event_by_event_id_mut( + &mut self, + event_id: &EventId, + ) -> Option<&mut EventMeta> { + self.all_remote_events.get_by_event_id_mut(event_id) + } + pub fn set( &mut self, timeline_item_index: usize, @@ -101,23 +150,36 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { } pub fn remove(&mut self, timeline_item_index: usize) -> Arc { - self.items.remove(timeline_item_index) + let removed_timeline_item = self.items.remove(timeline_item_index); + self.all_remote_events.timeline_item_has_been_removed_at(timeline_item_index); + + removed_timeline_item } - pub fn insert(&mut self, timeline_item_index: usize, timeline_item: Arc) { + pub fn insert( + &mut self, + timeline_item_index: usize, + timeline_item: Arc, + event_index: Option, + ) { self.items.insert(timeline_item_index, timeline_item); + self.all_remote_events.timeline_item_has_been_inserted_at(timeline_item_index, event_index); } - pub fn push_front(&mut self, timeline_item: Arc) { + pub fn push_front(&mut self, timeline_item: Arc, event_index: Option) { self.items.push_front(timeline_item); + self.all_remote_events.timeline_item_has_been_inserted_at(0, event_index); } - pub fn push_back(&mut self, timeline_item: Arc) { + pub fn push_back(&mut self, timeline_item: Arc, event_index: Option) { self.items.push_back(timeline_item); + self.all_remote_events + .timeline_item_has_been_inserted_at(self.items.len().saturating_sub(1), event_index); } pub fn clear(&mut self) { self.items.clear(); + self.all_remote_events.clear(); } pub fn for_each(&mut self, f: F) @@ -140,3 +202,142 @@ impl Deref for ObservableItemsTransaction<'_> { &self.items } } + +/// A type for all remote events. +/// +/// Having this type helps to know exactly which parts of the code and how they +/// use all remote events. It also helps to give a bit of semantics on top of +/// them. +#[derive(Clone, Debug, Default)] +pub struct AllRemoteEvents(VecDeque); + +impl AllRemoteEvents { + /// Return a front-to-back iterator over all remote events. + pub fn iter(&self) -> Iter<'_, EventMeta> { + self.0.iter() + } + + /// Remove all remote events. + fn clear(&mut self) { + self.0.clear(); + } + + /// Insert a new remote event at the front of all the others. + fn push_front(&mut self, event_meta: EventMeta) { + // If there is an associated `timeline_item_index`, shift all the + // `timeline_item_index` that come after this one. + if let Some(new_timeline_item_index) = event_meta.timeline_item_index { + self.increment_all_timeline_item_index_after(new_timeline_item_index); + } + + // Push the event. + self.0.push_front(event_meta) + } + + /// Insert a new remote event at the back of all the others. + fn push_back(&mut self, event_meta: EventMeta) { + // If there is an associated `timeline_item_index`, shift all the + // `timeline_item_index` that come after this one. + if let Some(new_timeline_item_index) = event_meta.timeline_item_index { + self.increment_all_timeline_item_index_after(new_timeline_item_index); + } + + // Push the event. + self.0.push_back(event_meta) + } + + /// Remove one remote event at a specific index, and return it if it exists. + fn remove(&mut self, event_index: usize) -> Option { + // Remove the event. + let event_meta = self.0.remove(event_index)?; + + // If there is an associated `timeline_item_index`, shift all the + // `timeline_item_index` that come after this one. + if let Some(removed_timeline_item_index) = event_meta.timeline_item_index { + self.decrement_all_timeline_item_index_after(removed_timeline_item_index); + }; + + Some(event_meta) + } + + /// Return a reference to the last remote event if it exists. + pub fn last(&self) -> Option<&EventMeta> { + self.0.back() + } + + /// Return the index of the last remote event if it exists. + pub fn last_index(&self) -> Option { + self.0.len().checked_sub(1) + } + + /// Get a mutable reference to a specific remote event by its ID. + pub fn get_by_event_id_mut(&mut self, event_id: &EventId) -> Option<&mut EventMeta> { + self.0.iter_mut().rev().find(|event_meta| event_meta.event_id == event_id) + } + + /// Shift to the right all timeline item indexes that are equal to or + /// greater than `new_timeline_item_index`. + fn increment_all_timeline_item_index_after(&mut self, new_timeline_item_index: usize) { + for event_meta in self.0.iter_mut() { + if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { + if *timeline_item_index >= new_timeline_item_index { + *timeline_item_index += 1; + } + } + } + } + + /// Shift to the left all timeline item indexes that are greater than + /// `removed_wtimeline_item_index`. + fn decrement_all_timeline_item_index_after(&mut self, removed_timeline_item_index: usize) { + for event_meta in self.0.iter_mut() { + if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { + if *timeline_item_index > removed_timeline_item_index { + *timeline_item_index -= 1; + } + } + } + } + + fn timeline_item_has_been_inserted_at( + &mut self, + new_timeline_item_index: usize, + event_index: Option, + ) { + self.increment_all_timeline_item_index_after(new_timeline_item_index); + + if let Some(event_index) = event_index { + if let Some(event_meta) = self.0.get_mut(event_index) { + event_meta.timeline_item_index = Some(new_timeline_item_index); + } + } + } + + fn timeline_item_has_been_removed_at(&mut self, timeline_item_index_to_remove: usize) { + for event_meta in self.0.iter_mut() { + let mut remove_timeline_item_index = false; + + // A `timeline_item_index` is removed. Let's shift all indexes that come + // after the removed one. + if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { + match (*timeline_item_index).cmp(&timeline_item_index_to_remove) { + Ordering::Equal => { + remove_timeline_item_index = true; + } + + Ordering::Greater => { + *timeline_item_index -= 1; + } + + Ordering::Less => {} + } + } + + // This is the `event_meta` that holds the `timeline_item_index` that is being + // removed. So let's clean it. + if remove_timeline_item_index { + event_meta.timeline_item_index = None; + } + } + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 72916c1c7bc..925e1d45d36 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -13,8 +13,7 @@ // limitations under the License. use std::{ - cmp::Ordering, - collections::{vec_deque::Iter, HashMap, VecDeque}, + collections::HashMap, future::Future, num::NonZeroUsize, sync::{Arc, RwLock}, @@ -46,7 +45,7 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use super::{ - observable_items::{ObservableItems, ObservableItemsTransaction}, + observable_items::{AllRemoteEvents, ObservableItems, ObservableItemsTransaction}, HandleManyEventsResult, TimelineFocusKind, TimelineSettings, }; use crate::{ @@ -546,7 +545,7 @@ impl TimelineStateTransaction<'_> { }; // Remember the event before returning prematurely. - // See [`TimelineMetadata::all_remote_events`]. + // See [`ObservableItems::all_remote_events`]. self.add_or_update_remote_event( event_meta, position, @@ -585,7 +584,7 @@ impl TimelineStateTransaction<'_> { }; // Remember the event before returning prematurely. - // See [`TimelineMetadata::all_remote_events`]. + // See [`ObservableItems::all_remote_events`]. self.add_or_update_remote_event( event_meta, position, @@ -611,7 +610,7 @@ impl TimelineStateTransaction<'_> { }; // Remember the event. - // See [`TimelineMetadata::all_remote_events`]. + // See [`ObservableItems::all_remote_events`]. self.add_or_update_remote_event(event_meta, position, room_data_provider, settings).await; let sender_profile = room_data_provider.profile_from_user_id(&sender).await; @@ -623,7 +622,7 @@ impl TimelineStateTransaction<'_> { read_receipts: if settings.track_read_receipts && should_add { self.meta.read_receipts.compute_event_receipts( &event_id, - &self.meta.all_remote_events, + self.items.all_remote_events(), matches!(position, TimelineItemPosition::End { .. }), ) } else { @@ -702,7 +701,7 @@ impl TimelineStateTransaction<'_> { } /// Add or update a remote event in the - /// [`TimelineMetadata::all_remote_events`] collection. + /// [`ObservableItems::all_remote_events`] collection. /// /// This method also adjusts read receipt if needed. async fn add_or_update_remote_event( @@ -712,7 +711,7 @@ impl TimelineStateTransaction<'_> { room_data_provider: &P, settings: &TimelineSettings, ) { - // Detect if an event already exists in [`TimelineMetadata::all_remote_events`]. + // Detect if an event already exists in [`ObservableItems::all_remote_events`]. // // Returns its position, in this case. fn event_already_exists( @@ -725,27 +724,27 @@ impl TimelineStateTransaction<'_> { match position { TimelineItemPosition::Start { .. } => { if let Some(pos) = - event_already_exists(event_meta.event_id, &self.meta.all_remote_events) + event_already_exists(event_meta.event_id, &self.items.all_remote_events()) { - self.meta.all_remote_events.remove(pos); + self.items.remove_remote_event(pos); } - self.meta.all_remote_events.push_front(event_meta.base_meta()) + self.items.push_front_remote_event(event_meta.base_meta()) } TimelineItemPosition::End { .. } => { if let Some(pos) = - event_already_exists(event_meta.event_id, &self.meta.all_remote_events) + event_already_exists(event_meta.event_id, &self.items.all_remote_events()) { - self.meta.all_remote_events.remove(pos); + self.items.remove_remote_event(pos); } - self.meta.all_remote_events.push_back(event_meta.base_meta()); + self.items.push_back_remote_event(event_meta.base_meta()); } TimelineItemPosition::UpdateDecrypted { .. } => { if let Some(event) = - self.meta.all_remote_events.get_by_event_id_mut(event_meta.event_id) + self.items.get_remote_event_by_event_id_mut(event_meta.event_id) { if event.visible != event_meta.visible { event.visible = event_meta.visible; @@ -918,13 +917,6 @@ pub(in crate::timeline) struct TimelineMetadata { /// the device has terabytes of RAM. next_internal_id: u64, - /// List of all the remote events as received in the timeline, even the ones - /// that are discarded in the timeline items. - /// - /// This is useful to get this for the moment as it helps the `Timeline` to - /// compute read receipts and read markers. - pub all_remote_events: AllRemoteEvents, - /// State helping matching reactions to their associated events, and /// stashing pending reactions. pub reactions: Reactions, @@ -967,7 +959,6 @@ impl TimelineMetadata { ) -> Self { Self { own_user_id, - all_remote_events: Default::default(), next_internal_id: Default::default(), reactions: Default::default(), pending_poll_events: Default::default(), @@ -987,7 +978,6 @@ impl TimelineMetadata { pub(crate) fn clear(&mut self) { // Note: we don't clear the next internal id to avoid bad cases of stale unique // ids across timeline clears. - self.all_remote_events.clear(); self.reactions.clear(); self.pending_poll_events.clear(); self.pending_edits.clear(); @@ -1008,6 +998,7 @@ impl TimelineMetadata { &self, event_a: &EventId, event_b: &EventId, + all_remote_events: &AllRemoteEvents, ) -> Option { if event_a == event_b { return Some(RelativePosition::Same); @@ -1015,11 +1006,11 @@ impl TimelineMetadata { // We can make early returns here because we know all events since the end of // the timeline, so the first event encountered is the oldest one. - for meta in self.all_remote_events.iter().rev() { - if meta.event_id == event_a { + for event_meta in all_remote_events.iter().rev() { + if event_meta.event_id == event_a { return Some(RelativePosition::Before); } - if meta.event_id == event_b { + if event_meta.event_id == event_b { return Some(RelativePosition::After); } } @@ -1092,8 +1083,7 @@ impl TimelineMetadata { // Only insert the read marker if it is not at the end of the timeline. if idx + 1 < items.len() { let idx = idx + 1; - items.insert(idx, TimelineItem::read_marker()); - self.all_remote_events.timeline_item_has_been_inserted_at(idx, None); + items.insert(idx, TimelineItem::read_marker(), None); self.has_up_to_date_read_marker_item = true; } else { // The next event might require a read marker to be inserted at the current @@ -1114,7 +1104,6 @@ impl TimelineMetadata { if from + 1 == items.len() { // The read marker has nothing after it. An item disappeared; remove it. items.remove(from); - self.all_remote_events.timeline_item_has_been_removed_at(from); } self.has_up_to_date_read_marker_item = true; return; @@ -1122,15 +1111,13 @@ impl TimelineMetadata { let prev_len = items.len(); let read_marker = items.remove(from); - self.all_remote_events.timeline_item_has_been_removed_at(from); // Only insert the read marker if it is not at the end of the timeline. if to + 1 < prev_len { // Since the fully-read event's index was shifted to the left // by one position by the remove call above, insert the fully- // read marker at its previous position, rather than that + 1 - items.insert(to, read_marker); - self.all_remote_events.timeline_item_has_been_inserted_at(to, None); + items.insert(to, read_marker, None); self.has_up_to_date_read_marker_item = true; } else { self.has_up_to_date_read_marker_item = false; @@ -1140,145 +1127,6 @@ impl TimelineMetadata { } } -/// A type for all remote events. -/// -/// Having this type helps to know exactly which parts of the code and how they -/// use all remote events. It also helps to give a bit of semantics on top of -/// them. -#[derive(Clone, Debug, Default)] -pub(crate) struct AllRemoteEvents(VecDeque); - -impl AllRemoteEvents { - /// Return a front-to-back iterator over all remote events. - pub fn iter(&self) -> Iter<'_, EventMeta> { - self.0.iter() - } - - /// Remove all remote events. - pub fn clear(&mut self) { - self.0.clear(); - } - - /// Insert a new remote event at the front of all the others. - pub fn push_front(&mut self, event_meta: EventMeta) { - // If there is an associated `timeline_item_index`, shift all the - // `timeline_item_index` that come after this one. - if let Some(new_timeline_item_index) = event_meta.timeline_item_index { - self.increment_all_timeline_item_index_after(new_timeline_item_index); - } - - // Push the event. - self.0.push_front(event_meta) - } - - /// Insert a new remote event at the back of all the others. - pub fn push_back(&mut self, event_meta: EventMeta) { - // If there is an associated `timeline_item_index`, shift all the - // `timeline_item_index` that come after this one. - if let Some(new_timeline_item_index) = event_meta.timeline_item_index { - self.increment_all_timeline_item_index_after(new_timeline_item_index); - } - - // Push the event. - self.0.push_back(event_meta) - } - - /// Remove one remote event at a specific index, and return it if it exists. - pub fn remove(&mut self, event_index: usize) -> Option { - // Remove the event. - let event_meta = self.0.remove(event_index)?; - - // If there is an associated `timeline_item_index`, shift all the - // `timeline_item_index` that come after this one. - if let Some(removed_timeline_item_index) = event_meta.timeline_item_index { - self.decrement_all_timeline_item_index_after(removed_timeline_item_index); - }; - - Some(event_meta) - } - - /// Return a reference to the last remote event if it exists. - pub fn last(&self) -> Option<&EventMeta> { - self.0.back() - } - - /// Return the index of the last remote event if it exists. - pub fn last_index(&self) -> Option { - self.0.len().checked_sub(1) - } - - /// Get a mutable reference to a specific remote event by its ID. - pub fn get_by_event_id_mut(&mut self, event_id: &EventId) -> Option<&mut EventMeta> { - self.0.iter_mut().rev().find(|event_meta| event_meta.event_id == event_id) - } - - /// Shift to the right all timeline item indexes that are equal to or - /// greater than `new_timeline_item_index`. - fn increment_all_timeline_item_index_after(&mut self, new_timeline_item_index: usize) { - for event_meta in self.0.iter_mut() { - if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { - if *timeline_item_index >= new_timeline_item_index { - *timeline_item_index += 1; - } - } - } - } - - /// Shift to the left all timeline item indexes that are greater than - /// `removed_wtimeline_item_index`. - fn decrement_all_timeline_item_index_after(&mut self, removed_timeline_item_index: usize) { - for event_meta in self.0.iter_mut() { - if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { - if *timeline_item_index > removed_timeline_item_index { - *timeline_item_index -= 1; - } - } - } - } - - pub fn timeline_item_has_been_inserted_at( - &mut self, - new_timeline_item_index: usize, - event_index: Option, - ) { - self.increment_all_timeline_item_index_after(new_timeline_item_index); - - if let Some(event_index) = event_index { - if let Some(event_meta) = self.0.get_mut(event_index) { - event_meta.timeline_item_index = Some(new_timeline_item_index); - } - } - } - - pub fn timeline_item_has_been_removed_at(&mut self, timeline_item_index_to_remove: usize) { - for event_meta in self.0.iter_mut() { - let mut remove_timeline_item_index = false; - - // A `timeline_item_index` is removed. Let's shift all indexes that come - // after the removed one. - if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() { - match (*timeline_item_index).cmp(&timeline_item_index_to_remove) { - Ordering::Equal => { - remove_timeline_item_index = true; - } - - Ordering::Greater => { - *timeline_item_index -= 1; - } - - Ordering::Less => {} - } - } - - // This is the `event_meta` that holds the `timeline_item_index` that is being - // removed. So let's clean it. - if remove_timeline_item_index { - event_meta.timeline_item_index = None; - } - } - } -} - /// Full metadata about an event. /// /// Only used to group function parameters. @@ -1317,9 +1165,9 @@ pub(crate) struct EventMeta { /// Foundation for the mapping between remote events to timeline items. /// /// Let's explain it. The events represent the first set and are stored in - /// [`TimelineMetadata::all_remote_events`], and the timeline + /// [`ObservableItems::all_remote_events`], and the timeline /// items represent the second set and are stored in - /// [`TimelineState::items`]. + /// [`ObservableItems::items`]. /// /// Each event is mapped to at most one timeline item: /// diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index 6f02196c10a..05385723fb0 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -301,11 +301,11 @@ impl DayDividerAdjuster { // Keep push semantics, if we're inserting at the front or the back. if at == items.len() { - items.push_back(item); + items.push_back(item, None); } else if at == 0 { - items.push_front(item); + items.push_front(item, None); } else { - items.insert(at, item); + items.insert(at, item, None); } offset += 1; @@ -654,9 +654,12 @@ mod tests { let timestamp_next_day = MilliSecondsSinceUnixEpoch((42 + 3600 * 24 * 1000).try_into().unwrap()); - txn.push_back(meta.new_timeline_item(event_with_ts(timestamp))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp_next_day))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker)); + txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); + txn.push_back( + meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp_next_day)), + None, + ); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); let mut adjuster = DayDividerAdjuster::default(); adjuster.run(&mut txn, &mut meta); @@ -690,10 +693,13 @@ mod tests { assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day)); let event = event_with_ts(timestamp); - txn.push_back(meta.new_timeline_item(event.clone())); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp_next_day))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker)); - txn.push_back(meta.new_timeline_item(event)); + txn.push_back(meta.new_timeline_item(event.clone()), None); + txn.push_back( + meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp_next_day)), + None, + ); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); + txn.push_back(meta.new_timeline_item(event), None); let mut adjuster = DayDividerAdjuster::default(); adjuster.run(&mut txn, &mut meta); @@ -721,12 +727,12 @@ mod tests { MilliSecondsSinceUnixEpoch((42 + 3600 * 24 * 1000).try_into().unwrap()); assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day)); - txn.push_back(meta.new_timeline_item(event_with_ts(timestamp))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker)); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); - txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day))); + txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); let mut adjuster = DayDividerAdjuster::default(); adjuster.run(&mut txn, &mut meta); @@ -755,10 +761,10 @@ mod tests { MilliSecondsSinceUnixEpoch((42 + 3600 * 24 * 1000).try_into().unwrap()); assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day)); - txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); - txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day))); + txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); let mut adjuster = DayDividerAdjuster::default(); adjuster.run(&mut txn, &mut meta); @@ -782,9 +788,9 @@ mod tests { let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); - txn.push_back(meta.new_timeline_item(event_with_ts(timestamp))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker)); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); + txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); let mut adjuster = DayDividerAdjuster::default(); adjuster.run(&mut txn, &mut meta); @@ -808,9 +814,9 @@ mod tests { let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker)); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp))); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); let mut adjuster = DayDividerAdjuster::default(); adjuster.run(&mut txn, &mut meta); @@ -831,8 +837,8 @@ mod tests { let mut meta = test_metadata(); let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker)); - txn.push_back(meta.new_timeline_item(event_with_ts(timestamp))); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); + txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); let mut adjuster = DayDividerAdjuster::default(); adjuster.run(&mut txn, &mut meta); diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index e04b0399719..b8e6457dd98 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -514,7 +514,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // wouldn't normally be visible. Remove it. trace!("Removing UTD that was successfully retried"); self.items.remove(timeline_item_index); - self.meta.all_remote_events.timeline_item_has_been_removed_at(timeline_item_index); self.result.item_removed = true; } @@ -1095,7 +1094,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("Adding new local timeline item"); let item = self.meta.new_timeline_item(item); - self.items.push_back(item); + self.items.push_back(item, None); } Flow::Remote { position: TimelineItemPosition::Start { .. }, event_id, .. } => { @@ -1112,8 +1111,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("Adding new remote timeline item at the start"); let item = self.meta.new_timeline_item(item); - self.items.push_front(item); - self.meta.all_remote_events.timeline_item_has_been_inserted_at(0, Some(0)); + self.items.push_front(item, Some(0)); } Flow::Remote { @@ -1189,6 +1187,13 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { }) .unwrap_or(0); + let event_index = + Some(self.items.all_remote_events().last_index() + // The last remote event is necessarily associated to this + // timeline item, see the contract of this method. + .expect("A timeline item is being added but its associated remote event is missing") + ); + // Try to keep precise insertion semantics here, in this exact order: // // * _push back_ when the new item is inserted after all items (the assumption @@ -1199,26 +1204,17 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { if timeline_item_index == self.items.len() { trace!("Adding new remote timeline item at the back"); - self.items.push_back(new_item); + self.items.push_back(new_item, event_index); } else if timeline_item_index == 0 { trace!("Adding new remote timeline item at the front"); - self.items.push_front(new_item); + self.items.push_front(new_item, event_index); } else { trace!( timeline_item_index, "Adding new remote timeline item at specific index" ); - self.items.insert(timeline_item_index, new_item); + self.items.insert(timeline_item_index, new_item, event_index); } - - self.meta.all_remote_events.timeline_item_has_been_inserted_at( - timeline_item_index, - Some(self.meta.all_remote_events.last_index() - // The last remote event is necessarily associated to this - // timeline item, see the contract of this method. - .expect("A timeline item is being added but its associated remote event is missing") - ), - ); } Flow::Remote { diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs index 10040230011..8cc0c8c2147 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs @@ -99,9 +99,10 @@ impl ReadReceipts { &mut self, new_receipt: FullReceipt<'_>, is_own_user_id: bool, - all_events: &AllRemoteEvents, timeline_items: &mut ObservableItemsTransaction<'_>, ) { + let all_events = timeline_items.all_remote_events(); + // Get old receipt. let old_receipt = self.get_latest(new_receipt.user_id, &new_receipt.receipt_type); if old_receipt @@ -382,7 +383,6 @@ impl TimelineStateTransaction<'_> { self.meta.read_receipts.maybe_update_read_receipt( full_receipt, is_own_user_id, - &self.meta.all_remote_events, &mut self.items, ); } @@ -417,7 +417,6 @@ impl TimelineStateTransaction<'_> { self.meta.read_receipts.maybe_update_read_receipt( full_receipt, user_id == own_user_id, - &self.meta.all_remote_events, &mut self.items, ); } @@ -446,7 +445,6 @@ impl TimelineStateTransaction<'_> { self.meta.read_receipts.maybe_update_read_receipt( full_receipt, is_own_event, - &self.meta.all_remote_events, &mut self.items, ); } @@ -456,8 +454,8 @@ impl TimelineStateTransaction<'_> { pub(super) fn maybe_update_read_receipts_of_prev_event(&mut self, event_id: &EventId) { // Find the previous visible event, if there is one. let Some(prev_event_meta) = self - .meta - .all_remote_events + .items + .all_remote_events() .iter() .rev() // Find the event item. @@ -487,7 +485,7 @@ impl TimelineStateTransaction<'_> { let read_receipts = self.meta.read_receipts.compute_event_receipts( &remote_prev_event_item.event_id, - &self.meta.all_remote_events, + &self.items.all_remote_events(), false, ); @@ -535,18 +533,24 @@ impl TimelineState { user_id: &UserId, room_data_provider: &P, ) -> Option<(OwnedEventId, Receipt)> { - let public_read_receipt = - self.meta.user_receipt(user_id, ReceiptType::Read, room_data_provider).await; - let private_read_receipt = - self.meta.user_receipt(user_id, ReceiptType::ReadPrivate, room_data_provider).await; + let all_remote_events = self.items.all_remote_events(); + let public_read_receipt = self + .meta + .user_receipt(user_id, ReceiptType::Read, room_data_provider, all_remote_events) + .await; + let private_read_receipt = self + .meta + .user_receipt(user_id, ReceiptType::ReadPrivate, room_data_provider, all_remote_events) + .await; // Let's assume that a private read receipt should be more recent than a public // read receipt, otherwise there's no point in the private read receipt, // and use it as default. - match self - .meta - .compare_optional_receipts(public_read_receipt.as_ref(), private_read_receipt.as_ref()) - { + match self.meta.compare_optional_receipts( + public_read_receipt.as_ref(), + private_read_receipt.as_ref(), + self.items.all_remote_events(), + ) { Ordering::Greater => public_read_receipt, Ordering::Less => private_read_receipt, _ => unreachable!(), @@ -568,16 +572,19 @@ impl TimelineState { // Let's assume that a private read receipt should be more recent than a public // read receipt, otherwise there's no point in the private read receipt, // and use it as default. - let (latest_receipt_id, _) = - match self.meta.compare_optional_receipts(public_read_receipt, private_read_receipt) { - Ordering::Greater => public_read_receipt?, - Ordering::Less => private_read_receipt?, - _ => unreachable!(), - }; + let (latest_receipt_id, _) = match self.meta.compare_optional_receipts( + public_read_receipt, + private_read_receipt, + self.items.all_remote_events(), + ) { + Ordering::Greater => public_read_receipt?, + Ordering::Less => private_read_receipt?, + _ => unreachable!(), + }; // Find the corresponding visible event. - self.meta - .all_remote_events + self.items + .all_remote_events() .iter() .rev() .skip_while(|ev| ev.event_id != *latest_receipt_id) @@ -597,6 +604,7 @@ impl TimelineMetadata { user_id: &UserId, receipt_type: ReceiptType, room_data_provider: &P, + all_remote_events: &AllRemoteEvents, ) -> Option<(OwnedEventId, Receipt)> { if let Some(receipt) = self.read_receipts.get_latest(user_id, &receipt_type) { // Since it is in the timeline, it should be the most recent. @@ -616,6 +624,7 @@ impl TimelineMetadata { match self.compare_optional_receipts( main_thread_read_receipt.as_ref(), unthreaded_read_receipt.as_ref(), + all_remote_events, ) { Ordering::Greater => main_thread_read_receipt, Ordering::Less => unthreaded_read_receipt, @@ -633,6 +642,7 @@ impl TimelineMetadata { &self, lhs: Option<&(OwnedEventId, Receipt)>, rhs_or_default: Option<&(OwnedEventId, Receipt)>, + all_remote_events: &AllRemoteEvents, ) -> Ordering { // If we only have one, use it. let Some((lhs_event_id, lhs_receipt)) = lhs else { @@ -643,7 +653,9 @@ impl TimelineMetadata { }; // Compare by position in the timeline. - if let Some(relative_pos) = self.compare_events_positions(lhs_event_id, rhs_event_id) { + if let Some(relative_pos) = + self.compare_events_positions(lhs_event_id, rhs_event_id, all_remote_events) + { if relative_pos == RelativePosition::Before { return Ordering::Greater; } From 40ff880597b59309f9486fa3af506f973bc93b39 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 16:54:53 +0100 Subject: [PATCH 727/979] doc(ui): Add more documentation. --- .../timeline/controller/observable_items.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index fc0a8aa6fbb..5133bc798ba 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -28,6 +28,9 @@ use ruma::EventId; use super::{state::EventMeta, TimelineItem}; +/// An `ObservableItems` is a type similar to +/// [`ObservableVector>`] except the API is limited and, +/// internally, maintains the mapping between remote events and timeline items. #[derive(Debug)] pub struct ObservableItems { /// All timeline items. @@ -45,6 +48,7 @@ pub struct ObservableItems { } impl ObservableItems { + /// Create an empty `ObservableItems`. pub fn new() -> Self { Self { // Upstream default capacity is currently 16, which is making @@ -55,22 +59,29 @@ impl ObservableItems { } } + /// Get a reference to all remote events. pub fn all_remote_events(&self) -> &AllRemoteEvents { &self.all_remote_events } + /// Check whether there is timeline items. pub fn is_empty(&self) -> bool { self.items.is_empty() } + /// Subscribe to timeline item updates. pub fn subscribe(&self) -> VectorSubscriber> { self.items.subscribe() } + /// Get a clone of all timeline items. + /// + /// Note that it doesn't clone `Self`, only the inner timeline items. pub fn clone(&self) -> Vector> { self.items.clone() } + /// Start a new transaction to make multiple updates as one unit. pub fn transaction(&mut self) -> ObservableItemsTransaction<'_> { ObservableItemsTransaction { items: self.items.transaction(), @@ -78,6 +89,12 @@ impl ObservableItems { } } + /// Replace the timeline item at position `timeline_item_index` by + /// `timeline_item`. + /// + /// # Panics + /// + /// Panics if `timeline_item_index > total_number_of_timeline_items`. pub fn set( &mut self, timeline_item_index: usize, @@ -86,10 +103,13 @@ impl ObservableItems { self.items.set(timeline_item_index, timeline_item) } + /// Get an iterator over all the entries in this `ObservableItems`. pub fn entries(&mut self) -> ObservableVectorEntries<'_, Arc> { self.items.entries() } + /// Call the given closure for every element in this `ObservableItems`, + /// with an entry struct that allows updating or removing that element. pub fn for_each(&mut self, f: F) where F: FnMut(ObservableVectorEntry<'_, Arc>), From 943b3fbd917eb8f9688c3023b675674d7267fd62 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 16:56:35 +0100 Subject: [PATCH 728/979] refactor(ui): Rename `ObservableItems::set` to `replace`. This patch renames `ObservableItems(Transaction)::set` to `replace`, it conveys the semantics a bit better for new comers. --- .../src/timeline/controller/mod.rs | 18 +++++++++--------- .../timeline/controller/observable_items.rs | 4 ++-- .../src/timeline/controller/state.rs | 2 +- .../src/timeline/day_dividers.rs | 2 +- .../src/timeline/event_handler.rs | 19 ++++++++++--------- .../src/timeline/read_receipts.rs | 6 +++--- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 0a58951b6a2..d75aae1b3a9 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -597,7 +597,7 @@ impl TimelineController

{ if reaction_info.is_some() { let new_item = item.with_reactions(reactions); - state.items.set(item_pos, new_item); + state.items.replace(item_pos, new_item); } else { warn!("reaction is missing on the item, not removing it locally, but sending redaction."); } @@ -621,7 +621,7 @@ impl TimelineController

{ .or_default() .insert(user_id.to_owned(), reaction_info); let new_item = item.with_reactions(reactions); - state.items.set(item_pos, new_item); + state.items.replace(item_pos, new_item); } else { warn!("couldn't find item to re-add reaction anymore; maybe it's been redacted?"); } @@ -817,7 +817,7 @@ impl TimelineController

{ { trace!("updated reaction status to sent"); entry.status = ReactionStatus::RemoteToRemote(event_id.to_owned()); - txn.items.set(item_pos, event_item.with_reactions(reactions)); + txn.items.replace(item_pos, event_item.with_reactions(reactions)); txn.commit(); return; } @@ -863,7 +863,7 @@ impl TimelineController

{ } let new_item = item.with_inner_kind(local_item.with_send_state(send_state)); - txn.items.set(idx, new_item); + txn.items.replace(idx, new_item); txn.commit(); } @@ -910,7 +910,7 @@ impl TimelineController

{ let mut reactions = item.reactions().clone(); if reactions.remove_reaction(&full_key.sender, &full_key.key).is_some() { let updated_item = item.with_reactions(reactions); - state.items.set(idx, updated_item); + state.items.replace(idx, updated_item); } else { warn!( "missing reaction {} for sender {} on timeline item", @@ -967,7 +967,7 @@ impl TimelineController

{ prev_item.internal_id.to_owned(), ); - txn.items.set(idx, new_item); + txn.items.replace(idx, new_item); // This doesn't change the original sending time, so there's no need to adjust // day dividers. @@ -1322,7 +1322,7 @@ impl TimelineController

{ trace!("Adding local reaction to local echo"); let new_item = item.with_reactions(reactions); - state.items.set(item_pos, new_item); + state.items.replace(item_pos, new_item); // Add it to the reaction map, so we can discard it later if needs be. state.meta.reactions.map.insert( @@ -1462,7 +1462,7 @@ impl TimelineController { event, }), )); - state.items.set(index, TimelineItem::new(item, internal_id)); + state.items.replace(index, TimelineItem::new(item, internal_id)); Ok(()) } @@ -1594,7 +1594,7 @@ async fn fetch_replied_to_event( let event_item = item.with_content(TimelineItemContent::Message(reply), None); let new_timeline_item = TimelineItem::new(event_item, internal_id); - state.items.set(index, new_timeline_item); + state.items.replace(index, new_timeline_item); // Don't hold the state lock while the network request is made drop(state); diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 5133bc798ba..804a35cca71 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -95,7 +95,7 @@ impl ObservableItems { /// # Panics /// /// Panics if `timeline_item_index > total_number_of_timeline_items`. - pub fn set( + pub fn replace( &mut self, timeline_item_index: usize, timeline_item: Arc, @@ -161,7 +161,7 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.all_remote_events.get_by_event_id_mut(event_id) } - pub fn set( + pub fn replace( &mut self, timeline_item_index: usize, timeline_item: Arc, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 925e1d45d36..b66e29579f6 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -788,7 +788,7 @@ impl TimelineStateTransaction<'_> { // Replace the existing item with a new version with the right encryption flag let item = item.with_kind(cloned_event); - self.items.set(idx, item); + self.items.replace(idx, item); } } } diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index 05385723fb0..ba9cd9175b3 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -330,7 +330,7 @@ impl DayDividerAdjuster { unique_id.to_owned(), ); - items.set(at, item); + items.replace(at, item); max_i = i; } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index b8e6457dd98..edc3d8c9c84 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -578,7 +578,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Self::maybe_update_responses(self.items, &replacement.event_id, &new_item); // Update the event itself. - self.items.set(item_pos, TimelineItem::new(new_item, internal_id)); + self.items.replace(item_pos, TimelineItem::new(new_item, internal_id)); self.result.items_updated += 1; } } else if let Flow::Remote { position, raw_event, .. } = &self.ctx.flow { @@ -732,7 +732,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { }, ); - self.items.set(idx, event_item.with_reactions(reactions)); + self.items.replace(idx, event_item.with_reactions(reactions)); self.result.items_updated += 1; } else { @@ -796,7 +796,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { }; trace!("Applying poll start edit."); - self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); + self.items.replace(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); self.result.items_updated += 1; } @@ -900,7 +900,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { ); trace!("Adding poll response."); - self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); + self.items.replace(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); self.result.items_updated += 1; } @@ -919,7 +919,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let new_item = item.with_content(TimelineItemContent::Poll(poll_state), None); trace!("Ending poll."); - self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); + self.items + .replace(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned())); self.result.items_updated += 1; } Err(_) => { @@ -961,7 +962,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // the replied-to event there as well. Self::maybe_update_responses(self.items, &redacted, &new_item); - self.items.set(idx, TimelineItem::new(new_item, internal_id)); + self.items.replace(idx, TimelineItem::new(new_item, internal_id)); self.result.items_updated += 1; } } else { @@ -1002,7 +1003,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let mut reactions = item.reactions.clone(); if reactions.remove_reaction(&sender, &key).is_some() { trace!("Removing reaction"); - self.items.set(item_pos, item.with_reactions(reactions)); + self.items.replace(item_pos, item.with_reactions(reactions)); self.result.items_updated += 1; return true; } @@ -1153,7 +1154,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // If the old item is the last one and no day divider // changes need to happen, replace and return early. trace!(idx, "Replacing existing event"); - self.items.set(idx, TimelineItem::new(item, old_item_id.to_owned())); + self.items.replace(idx, TimelineItem::new(item, old_item_id.to_owned())); return; } @@ -1228,7 +1229,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Self::maybe_update_responses(self.items, decrypted_event_id, &item); let internal_id = self.items[*idx].internal_id.clone(); - self.items.set(*idx, TimelineItem::new(item, internal_id)); + self.items.replace(*idx, TimelineItem::new(item, internal_id)); } } diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs index 8cc0c8c2147..30226de15ca 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs @@ -307,7 +307,7 @@ impl ReadReceiptTimelineUpdate { receipt doesn't have a receipt for the user" ); } - items.set(receipt_pos, TimelineItem::new(event_item, event_item_id)); + items.replace(receipt_pos, TimelineItem::new(event_item, event_item_id)); } else { warn!("received a read receipt for a local item, this should not be possible"); } @@ -336,7 +336,7 @@ impl ReadReceiptTimelineUpdate { if let Some(remote_event_item) = event_item.as_remote_mut() { remote_event_item.read_receipts.insert(user_id, receipt); - items.set(receipt_pos, TimelineItem::new(event_item, event_item_id)); + items.replace(receipt_pos, TimelineItem::new(event_item, event_item_id)); } else { warn!("received a read receipt for a local item, this should not be possible"); } @@ -495,7 +495,7 @@ impl TimelineStateTransaction<'_> { } remote_prev_event_item.read_receipts = read_receipts; - self.items.set(prev_item_pos, TimelineItem::new(prev_event_item, prev_event_item_id)); + self.items.replace(prev_item_pos, TimelineItem::new(prev_event_item, prev_event_item_id)); } } From 6f231523b364e1b6bdeb0a7b6ba44d6e1803fced Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 17:10:52 +0100 Subject: [PATCH 729/979] refactor(ui): Create `ObservableItemsEntries` and `ObservableItemsEntry`. This patch creates `ObservableItemsEntries` and particularly `ObservableItemsEntry` that wraps the equivalent `ObservableVectorEntries` and `ObservableVectorEntry` with the noticeable difference that `ObservableItemsEntry` does **not** expose the `remove` method. It only exposes `replace` (which is a renaming of `set`). --- .../src/timeline/controller/mod.rs | 14 +++---- .../timeline/controller/observable_items.rs | 39 ++++++++++++++++--- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index d75aae1b3a9..31c638befed 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -15,7 +15,7 @@ use std::{collections::BTreeSet, fmt, sync::Arc}; use as_variant::as_variant; -use eyeball_im::{ObservableVectorEntry, VectorDiff}; +use eyeball_im::VectorDiff; use eyeball_im_util::vector::VectorObserverExt; use futures_core::Stream; use imbl::Vector; @@ -54,7 +54,7 @@ use tracing::{ #[cfg(test)] pub(super) use self::observable_items::ObservableItems; pub(super) use self::{ - observable_items::{AllRemoteEvents, ObservableItemsTransaction}, + observable_items::{AllRemoteEvents, ObservableItemsEntry, ObservableItemsTransaction}, state::{ FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, TimelineNewItemPosition, TimelineState, TimelineStateTransaction, @@ -1134,7 +1134,7 @@ impl TimelineController

{ let new_item = entry.with_kind(TimelineItemKind::Event( event_item.with_sender_profile(profile_state.clone()), )); - ObservableVectorEntry::set(&mut entry, new_item); + ObservableItemsEntry::replace(&mut entry, new_item); } }); } @@ -1160,7 +1160,7 @@ impl TimelineController

{ let updated_item = event_item.with_sender_profile(TimelineDetails::Ready(profile)); let new_item = entry.with_kind(updated_item); - ObservableVectorEntry::set(&mut entry, new_item); + ObservableItemsEntry::replace(&mut entry, new_item); } None => { if !event_item.sender_profile().is_unavailable() { @@ -1168,7 +1168,7 @@ impl TimelineController

{ let updated_item = event_item.with_sender_profile(TimelineDetails::Unavailable); let new_item = entry.with_kind(updated_item); - ObservableVectorEntry::set(&mut entry, new_item); + ObservableItemsEntry::replace(&mut entry, new_item); } else { debug!(event_id, transaction_id, "Profile already marked unavailable"); } @@ -1204,7 +1204,7 @@ impl TimelineController

{ let updated_item = event_item.with_sender_profile(TimelineDetails::Ready(profile)); let new_item = entry.with_kind(updated_item); - ObservableVectorEntry::set(&mut entry, new_item); + ObservableItemsEntry::replace(&mut entry, new_item); } } None => { @@ -1213,7 +1213,7 @@ impl TimelineController

{ let updated_item = event_item.with_sender_profile(TimelineDetails::Unavailable); let new_item = entry.with_kind(updated_item); - ObservableVectorEntry::set(&mut entry, new_item); + ObservableItemsEntry::replace(&mut entry, new_item); } else { debug!(event_id, transaction_id, "Profile already marked unavailable"); } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 804a35cca71..9b7d8ee216f 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -104,17 +104,17 @@ impl ObservableItems { } /// Get an iterator over all the entries in this `ObservableItems`. - pub fn entries(&mut self) -> ObservableVectorEntries<'_, Arc> { - self.items.entries() + pub fn entries(&mut self) -> ObservableItemsEntries<'_> { + ObservableItemsEntries(self.items.entries()) } /// Call the given closure for every element in this `ObservableItems`, /// with an entry struct that allows updating or removing that element. - pub fn for_each(&mut self, f: F) + pub fn for_each(&mut self, mut f: F) where - F: FnMut(ObservableVectorEntry<'_, Arc>), + F: FnMut(ObservableItemsEntry<'_>), { - self.items.for_each(f) + self.items.for_each(|entry| f(ObservableItemsEntry(entry))) } } @@ -127,6 +127,35 @@ impl Deref for ObservableItems { } } +/// An “iterator“ that yields entries into an `ObservableItems`. +pub struct ObservableItemsEntries<'a>(ObservableVectorEntries<'a, Arc>); + +impl<'a> ObservableItemsEntries<'a> { + /// Advance this iterator, yielding an `ObservableItemsEntry` for the next + /// item in the timeline, or `None` if all items have been visited. + pub fn next(&mut self) -> Option> { + self.0.next().map(ObservableItemsEntry) + } +} + +/// A handle to a single timeline item in an `ObservableItems`. +pub struct ObservableItemsEntry<'a>(ObservableVectorEntry<'a, Arc>); + +impl<'a> ObservableItemsEntry<'a> { + /// Replace the timeline item by `timeline_item`. + pub fn replace(this: &mut Self, timeline_item: Arc) -> Arc { + ObservableVectorEntry::set(&mut this.0, timeline_item) + } +} + +impl Deref for ObservableItemsEntry<'_> { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Debug)] pub struct ObservableItemsTransaction<'observable_items> { items: ObservableVectorTransaction<'observable_items, Arc>, From aa9138b281a990e1502ce02b03ca0eed755502cc Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 17:20:32 +0100 Subject: [PATCH 730/979] doc(ui): Add more documentation for `ObservableItemsTransaction`. --- .../timeline/controller/observable_items.rs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 9b7d8ee216f..42c0d4dede6 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -109,7 +109,7 @@ impl ObservableItems { } /// Call the given closure for every element in this `ObservableItems`, - /// with an entry struct that allows updating or removing that element. + /// with an entry struct that allows updating that element. pub fn for_each(&mut self, mut f: F) where F: FnMut(ObservableItemsEntry<'_>), @@ -156,6 +156,12 @@ impl Deref for ObservableItemsEntry<'_> { } } +/// A transaction that allows making multiple updates to an `ObservableItems` as +/// an atomic unit. +/// +/// For updates from the transaction to have affect, it has to be finalized with +/// [`Self::commit`]. If the transaction is dropped without that method being +/// called, the updates will be discarded. #[derive(Debug)] pub struct ObservableItemsTransaction<'observable_items> { items: ObservableVectorTransaction<'observable_items, Arc>, @@ -163,26 +169,38 @@ pub struct ObservableItemsTransaction<'observable_items> { } impl<'observable_items> ObservableItemsTransaction<'observable_items> { + /// Get a referene to the timeline index at position `timeline_item_index`. pub fn get(&self, timeline_item_index: usize) -> Option<&Arc> { self.items.get(timeline_item_index) } + /// Get a reference to all remote events. pub fn all_remote_events(&self) -> &AllRemoteEvents { &self.all_remote_events } + /// Remove a remote event at position `event_index`. + /// + /// Not to be confused with removing a timeline item! pub fn remove_remote_event(&mut self, event_index: usize) -> Option { self.all_remote_events.remove(event_index) } + /// Push a new remote event at the front of all remote events. + /// + /// Not to be confused with pushing front a timeline item! pub fn push_front_remote_event(&mut self, event_meta: EventMeta) { self.all_remote_events.push_front(event_meta); } + /// Push a new remote event at the back of all remote events. + /// + /// Not to be confused with pushing back a timeline item! pub fn push_back_remote_event(&mut self, event_meta: EventMeta) { self.all_remote_events.push_back(event_meta); } + /// Get a remote event by using an event ID. pub fn get_remote_event_by_event_id_mut( &mut self, event_id: &EventId, @@ -190,6 +208,8 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.all_remote_events.get_by_event_id_mut(event_id) } + /// Replace a timeline item at position `timeline_item_index` by + /// `timeline_item`. pub fn replace( &mut self, timeline_item_index: usize, @@ -198,6 +218,7 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.items.set(timeline_item_index, timeline_item) } + /// Remove a timeline item at position `timeline_item_index`. pub fn remove(&mut self, timeline_item_index: usize) -> Arc { let removed_timeline_item = self.items.remove(timeline_item_index); self.all_remote_events.timeline_item_has_been_removed_at(timeline_item_index); @@ -205,6 +226,14 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { removed_timeline_item } + /// Insert a new `timeline_item` at position `timeline_item_index`, with an + /// optionally associated `event_index`. + /// + /// If `event_index` is `Some(_)`, it means `timeline_item_index` has an + /// associated remote event (at position `event_index`) that maps to it. + /// Otherwise, if it is `None`, it means there is no remote event associated + /// to it; that's the case for virtual timeline item for example. See + /// [`EventMeta::timeline_item_index`] to learn more. pub fn insert( &mut self, timeline_item_index: usize, @@ -215,22 +244,41 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.all_remote_events.timeline_item_has_been_inserted_at(timeline_item_index, event_index); } + /// Push a new `timeline_item` at position 0, with an optionally associated + /// `event_index`. + /// + /// If `event_index` is `Some(_)`, it means `timeline_item_index` has an + /// associated remote event (at position `event_index`) that maps to it. + /// Otherwise, if it is `None`, it means there is no remote event associated + /// to it; that's the case for virtual timeline item for example. See + /// [`EventMeta::timeline_item_index`] to learn more. pub fn push_front(&mut self, timeline_item: Arc, event_index: Option) { self.items.push_front(timeline_item); self.all_remote_events.timeline_item_has_been_inserted_at(0, event_index); } + /// Push a new `timeline_item` at position `len() - 1`, with an optionally + /// associated `event_index`. + /// + /// If `event_index` is `Some(_)`, it means `timeline_item_index` has an + /// associated remote event (at position `event_index`) that maps to it. + /// Otherwise, if it is `None`, it means there is no remote event associated + /// to it; that's the case for virtual timeline item for example. See + /// [`EventMeta::timeline_item_index`] to learn more. pub fn push_back(&mut self, timeline_item: Arc, event_index: Option) { self.items.push_back(timeline_item); self.all_remote_events .timeline_item_has_been_inserted_at(self.items.len().saturating_sub(1), event_index); } + /// Clear all timeline items and all remote events. pub fn clear(&mut self) { self.items.clear(); self.all_remote_events.clear(); } + /// Call the given closure for every element in this `ObservableItems`, + /// with an entry struct that allows updating that element. pub fn for_each(&mut self, f: F) where F: FnMut(ObservableVectorTransactionEntry<'_, 'observable_items, Arc>), @@ -238,6 +286,8 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.items.for_each(f) } + /// Commit this transaction, persisting the changes and notifying + /// subscribers. pub fn commit(self) { self.items.commit() } From 56218ee5d755dc1f4f694a4d885a15ac7147c0ef Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 17:39:38 +0100 Subject: [PATCH 731/979] refactor(ui): Create `ObservableItemsTransactionEntry`. This patch creates `ObservableItemsTransactionEntry` that mimics `ObservableVectorTransactionEntry`. The differences are `set` is renamed `replace`, and `remove` is unsafe (because I failed to update `AllRemoteEvents` in this method due to the borrow checker). --- .../src/timeline/controller/mod.rs | 5 ++- .../timeline/controller/observable_items.rs | 37 +++++++++++++++++-- .../src/timeline/controller/state.rs | 13 +++++-- .../src/timeline/event_handler.rs | 28 +++++--------- 4 files changed, 58 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 31c638befed..9a22bb91753 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -54,7 +54,10 @@ use tracing::{ #[cfg(test)] pub(super) use self::observable_items::ObservableItems; pub(super) use self::{ - observable_items::{AllRemoteEvents, ObservableItemsEntry, ObservableItemsTransaction}, + observable_items::{ + AllRemoteEvents, ObservableItemsEntry, ObservableItemsTransaction, + ObservableItemsTransactionEntry, + }, state::{ FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata, TimelineNewItemPosition, TimelineState, TimelineStateTransaction, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 42c0d4dede6..799e88785b4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -279,11 +279,11 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { /// Call the given closure for every element in this `ObservableItems`, /// with an entry struct that allows updating that element. - pub fn for_each(&mut self, f: F) + pub fn for_each(&mut self, mut f: F) where - F: FnMut(ObservableVectorTransactionEntry<'_, 'observable_items, Arc>), + F: FnMut(ObservableItemsTransactionEntry<'_, 'observable_items>), { - self.items.for_each(f) + self.items.for_each(|entry| f(ObservableItemsTransactionEntry(entry))) } /// Commit this transaction, persisting the changes and notifying @@ -302,6 +302,37 @@ impl Deref for ObservableItemsTransaction<'_> { } } +/// A handle to a single timeline item in an `ObservableItemsTransaction`. +pub struct ObservableItemsTransactionEntry<'a, 'observable_items>( + ObservableVectorTransactionEntry<'a, 'observable_items, Arc>, +); + +impl<'a, 'o> ObservableItemsTransactionEntry<'a, 'o> { + /// Replace the timeline item by `timeline_item`. + pub fn replace(this: &mut Self, timeline_item: Arc) -> Arc { + ObservableVectorTransactionEntry::set(&mut this.0, timeline_item) + } + + /// Remove this timeline item. + /// + /// # Safety + /// + /// This method doesn't update `AllRemoteEvents`. Be sure that the caller + /// doesn't break the mapping between remote events and timeline items. See + /// [`EventMeta::timeline_item_index`] to learn more. + pub unsafe fn remove(this: Self) { + ObservableVectorTransactionEntry::remove(this.0); + } +} + +impl Deref for ObservableItemsTransactionEntry<'_, '_> { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// A type for all remote events. /// /// Having this type helps to know exactly which parts of the code and how they diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index b66e29579f6..9df9ffb8e2f 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -19,7 +19,6 @@ use std::{ sync::{Arc, RwLock}, }; -use eyeball_im::ObservableVectorTransactionEntry; use itertools::Itertools as _; use matrix_sdk::{ deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer, send_queue::SendHandle, @@ -45,7 +44,10 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use super::{ - observable_items::{AllRemoteEvents, ObservableItems, ObservableItemsTransaction}, + observable_items::{ + AllRemoteEvents, ObservableItems, ObservableItemsTransaction, + ObservableItemsTransactionEntry, + }, HandleManyEventsResult, TimelineFocusKind, TimelineSettings, }; use crate::{ @@ -655,7 +657,12 @@ impl TimelineStateTransaction<'_> { // Remove all remote events and the read marker self.items.for_each(|entry| { if entry.is_remote_event() || entry.is_read_marker() { - ObservableVectorTransactionEntry::remove(entry); + // SAFETY: this method removes all events except local events. Local events + // don't have a mapping from remote events to timeline items because… well… they + // are local events, not remove events. + unsafe { + ObservableItemsTransactionEntry::remove(entry); + } } }); diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index edc3d8c9c84..9c8a63f203b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -15,7 +15,6 @@ use std::sync::Arc; use as_variant::as_variant; -use eyeball_im::ObservableVectorTransactionEntry; use indexmap::IndexMap; use matrix_sdk::{ crypto::types::events::UtdCause, @@ -52,30 +51,23 @@ use tracing::{debug, error, field::debug, info, instrument, trace, warn}; use super::{ controller::{ - ObservableItemsTransaction, PendingEditKind, TimelineMetadata, TimelineStateTransaction, + ObservableItemsTransaction, ObservableItemsTransactionEntry, PendingEdit, PendingEditKind, + TimelineMetadata, TimelineStateTransaction, }, day_dividers::DayDividerAdjuster, event_item::{ extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content, AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, - LocalEventTimelineItem, PollState, Profile, ReactionsByKeyBySender, RemoteEventOrigin, - RemoteEventTimelineItem, TimelineEventItemId, + LocalEventTimelineItem, PollState, Profile, ReactionInfo, ReactionStatus, + ReactionsByKeyBySender, RemoteEventOrigin, RemoteEventTimelineItem, TimelineEventItemId, }, - reactions::FullReactionKey, + reactions::{FullReactionKey, PendingReaction}, + traits::RoomDataProvider, util::{rfind_event_by_id, rfind_event_item}, - EventTimelineItem, InReplyToDetails, OtherState, Sticker, TimelineDetails, TimelineItem, - TimelineItemContent, -}; -use crate::{ - events::SyncTimelineEventWithoutContent, - timeline::{ - controller::PendingEdit, - event_item::{ReactionInfo, ReactionStatus}, - reactions::PendingReaction, - traits::RoomDataProvider, - RepliedToEvent, - }, + EventTimelineItem, InReplyToDetails, OtherState, RepliedToEvent, Sticker, TimelineDetails, + TimelineItem, TimelineItemContent, }; +use crate::events::SyncTimelineEventWithoutContent; /// When adding an event, useful information related to the source of the event. pub(super) enum Flow { @@ -1262,7 +1254,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { TimelineItemContent::Message(message.with_in_reply_to(in_reply_to)); let new_reply_item = entry.with_kind(event_item.with_content(new_reply_content, None)); - ObservableVectorTransactionEntry::set(&mut entry, new_reply_item); + ObservableItemsTransactionEntry::replace(&mut entry, new_reply_item); } }); } From 81c962238a25f0b04dba97ddececea7450732f0e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 17:42:32 +0100 Subject: [PATCH 732/979] doc(ui): Add more documentation for `AllRemoteEvents`. --- .../src/timeline/controller/observable_items.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 799e88785b4..a78106f8337 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -169,7 +169,7 @@ pub struct ObservableItemsTransaction<'observable_items> { } impl<'observable_items> ObservableItemsTransaction<'observable_items> { - /// Get a referene to the timeline index at position `timeline_item_index`. + /// Get a reference to the timeline index at position `timeline_item_index`. pub fn get(&self, timeline_item_index: usize) -> Option<&Arc> { self.items.get(timeline_item_index) } @@ -429,6 +429,10 @@ impl AllRemoteEvents { } } + /// Notify that a timeline item has been inserted at + /// `new_timeline_item_index`. If `event_index` is `Some(_)`, it means the + /// remote event at `event_index` must be mapped to + /// `new_timeline_item_index`. fn timeline_item_has_been_inserted_at( &mut self, new_timeline_item_index: usize, @@ -443,6 +447,9 @@ impl AllRemoteEvents { } } + /// Notify that a timeline item has been removed at + /// `new_timeline_item_index`. If `event_index` is `Some(_)`, it means the + /// remote event at `event_index` must be unmapped. fn timeline_item_has_been_removed_at(&mut self, timeline_item_index_to_remove: usize) { for event_meta in self.0.iter_mut() { let mut remove_timeline_item_index = false; From 05969fefdeadba5ae8135ccb8981ab9fafd38fb9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 19:43:54 +0100 Subject: [PATCH 733/979] chore: Make Clippy happy. --- .../src/timeline/controller/observable_items.rs | 8 ++++---- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 4 ++-- crates/matrix-sdk-ui/src/timeline/read_receipts.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index a78106f8337..6f258edf545 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -130,7 +130,7 @@ impl Deref for ObservableItems { /// An “iterator“ that yields entries into an `ObservableItems`. pub struct ObservableItemsEntries<'a>(ObservableVectorEntries<'a, Arc>); -impl<'a> ObservableItemsEntries<'a> { +impl ObservableItemsEntries<'_> { /// Advance this iterator, yielding an `ObservableItemsEntry` for the next /// item in the timeline, or `None` if all items have been visited. pub fn next(&mut self) -> Option> { @@ -141,7 +141,7 @@ impl<'a> ObservableItemsEntries<'a> { /// A handle to a single timeline item in an `ObservableItems`. pub struct ObservableItemsEntry<'a>(ObservableVectorEntry<'a, Arc>); -impl<'a> ObservableItemsEntry<'a> { +impl ObservableItemsEntry<'_> { /// Replace the timeline item by `timeline_item`. pub fn replace(this: &mut Self, timeline_item: Arc) -> Arc { ObservableVectorEntry::set(&mut this.0, timeline_item) @@ -176,7 +176,7 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { /// Get a reference to all remote events. pub fn all_remote_events(&self) -> &AllRemoteEvents { - &self.all_remote_events + self.all_remote_events } /// Remove a remote event at position `event_index`. @@ -307,7 +307,7 @@ pub struct ObservableItemsTransactionEntry<'a, 'observable_items>( ObservableVectorTransactionEntry<'a, 'observable_items, Arc>, ); -impl<'a, 'o> ObservableItemsTransactionEntry<'a, 'o> { +impl ObservableItemsTransactionEntry<'_, '_> { /// Replace the timeline item by `timeline_item`. pub fn replace(this: &mut Self, timeline_item: Arc) -> Arc { ObservableVectorTransactionEntry::set(&mut this.0, timeline_item) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 9df9ffb8e2f..520f3ac82e4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -731,7 +731,7 @@ impl TimelineStateTransaction<'_> { match position { TimelineItemPosition::Start { .. } => { if let Some(pos) = - event_already_exists(event_meta.event_id, &self.items.all_remote_events()) + event_already_exists(event_meta.event_id, self.items.all_remote_events()) { self.items.remove_remote_event(pos); } @@ -741,7 +741,7 @@ impl TimelineStateTransaction<'_> { TimelineItemPosition::End { .. } => { if let Some(pos) = - event_already_exists(event_meta.event_id, &self.items.all_remote_events()) + event_already_exists(event_meta.event_id, self.items.all_remote_events()) { self.items.remove_remote_event(pos); } diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs index 30226de15ca..00964de20be 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/read_receipts.rs @@ -485,7 +485,7 @@ impl TimelineStateTransaction<'_> { let read_receipts = self.meta.read_receipts.compute_event_receipts( &remote_prev_event_item.event_id, - &self.items.all_remote_events(), + self.items.all_remote_events(), false, ); From 80f6b8d2cde3710621e91c15b121b4ab36594027 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 20:51:50 +0100 Subject: [PATCH 734/979] test(ui): Write test suite for `AllRemoteEvents`. This patch adds test suite for `AllRemoteEvents`. --- .../timeline/controller/observable_items.rs | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 6f258edf545..e5a2f4134c2 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -478,3 +478,286 @@ impl AllRemoteEvents { } } } + +#[cfg(test)] +mod all_remote_events_tests { + use assert_matches::assert_matches; + use ruma::event_id; + + use super::{AllRemoteEvents, EventMeta}; + + fn event_meta(event_id: &str, timeline_item_index: Option) -> EventMeta { + EventMeta { event_id: event_id.parse().unwrap(), timeline_item_index, visible: false } + } + + macro_rules! assert_events { + ( $events:ident, [ $( ( $event_id:literal, $timeline_item_index:expr ) ),* $(,)? ] ) => { + let mut iter = $events .iter(); + + $( + assert_matches!(iter.next(), Some(EventMeta { event_id, timeline_item_index, .. }) => { + assert_eq!(event_id.as_str(), $event_id ); + assert_eq!(*timeline_item_index, $timeline_item_index ); + }); + )* + + assert!(iter.next().is_none(), "Not all events have been asserted"); + } + } + + #[test] + fn test_clear() { + let mut events = AllRemoteEvents::default(); + + events.push_back(event_meta("$ev0", None)); + events.push_back(event_meta("$ev1", None)); + events.push_back(event_meta("$ev2", None)); + + assert_eq!(events.iter().count(), 3); + + events.clear(); + + assert_eq!(events.iter().count(), 0); + } + + #[test] + fn test_push_front() { + let mut events = AllRemoteEvents::default(); + + // Push front on an empty set, nothing particular. + events.push_front(event_meta("$ev0", Some(1))); + + // Push front with no `timeline_item_index`. + events.push_front(event_meta("$ev1", None)); + + // Push front with a `timeline_item_index`. + events.push_front(event_meta("$ev2", Some(0))); + + // Push front with the same `timeline_item_index`. + events.push_front(event_meta("$ev3", Some(0))); + + assert_events!( + events, + [ + // `timeline_item_index` is untouched + ("$ev3", Some(0)), + // `timeline_item_index` has been shifted once + ("$ev2", Some(1)), + // no `timeline_item_index` + ("$ev1", None), + // `timeline_item_index` has been shifted twice + ("$ev0", Some(3)), + ] + ); + } + + #[test] + fn test_push_back() { + let mut events = AllRemoteEvents::default(); + + // Push back on an empty set, nothing particular. + events.push_back(event_meta("$ev0", Some(0))); + + // Push back with no `timeline_item_index`. + events.push_back(event_meta("$ev1", None)); + + // Push back with a `timeline_item_index`. + events.push_back(event_meta("$ev2", Some(1))); + + // Push back with a `timeline_item_index` pointing to a timeline item that is + // not the last one. Is it possible in practise? Normally not, but let's test + // it anyway. + events.push_back(event_meta("$ev3", Some(1))); + + assert_events!( + events, + [ + // `timeline_item_index` is untouched + ("$ev0", Some(0)), + // no `timeline_item_index` + ("$ev1", None), + // `timeline_item_index` has been shifted once + ("$ev2", Some(2)), + // `timeline_item_index` is untouched + ("$ev3", Some(1)), + ] + ); + } + + #[test] + fn test_remove() { + let mut events = AllRemoteEvents::default(); + + // Push some events. + events.push_back(event_meta("$ev0", Some(0))); + events.push_back(event_meta("$ev1", Some(1))); + events.push_back(event_meta("$ev2", None)); + events.push_back(event_meta("$ev3", Some(2))); + + // Assert initial state. + assert_events!( + events, + [("$ev0", Some(0)), ("$ev1", Some(1)), ("$ev2", None), ("$ev3", Some(2))] + ); + + // Remove two events. + events.remove(2); // $ev2 has no `timeline_item_index` + events.remove(1); // $ev1 has a `timeline_item_index` + + assert_events!( + events, + [ + ("$ev0", Some(0)), + // `timeline_item_index` has shifted once + ("$ev3", Some(1)), + ] + ); + } + + #[test] + fn test_last() { + let mut events = AllRemoteEvents::default(); + + assert!(events.last().is_none()); + assert!(events.last_index().is_none()); + + // Push some events. + events.push_back(event_meta("$ev0", Some(0))); + events.push_back(event_meta("$ev1", Some(1))); + + assert_matches!(events.last(), Some(EventMeta { event_id, .. }) => { + assert_eq!(event_id.as_str(), "$ev1"); + }); + assert_eq!(events.last_index(), Some(1)); + } + + #[test] + fn test_get_by_event_by_mut() { + let mut events = AllRemoteEvents::default(); + + // Push some events. + events.push_back(event_meta("$ev0", Some(0))); + events.push_back(event_meta("$ev1", Some(1))); + + assert!(events.get_by_event_id_mut(event_id!("$ev0")).is_some()); + assert!(events.get_by_event_id_mut(event_id!("$ev42")).is_none()); + } + + #[test] + fn test_timeline_item_has_been_inserted_at() { + let mut events = AllRemoteEvents::default(); + + // Push some events. + events.push_back(event_meta("$ev0", Some(0))); + events.push_back(event_meta("$ev1", Some(1))); + events.push_back(event_meta("$ev2", None)); + events.push_back(event_meta("$ev3", None)); + events.push_back(event_meta("$ev4", Some(2))); + events.push_back(event_meta("$ev5", Some(3))); + events.push_back(event_meta("$ev6", None)); + + // A timeline item has been inserted at index 2, and maps to no event. + events.timeline_item_has_been_inserted_at(2, None); + + assert_events!( + events, + [ + ("$ev0", Some(0)), + ("$ev1", Some(1)), + ("$ev2", None), + ("$ev3", None), + // `timeline_item_index` is shifted once + ("$ev4", Some(3)), + // `timeline_item_index` is shifted once + ("$ev5", Some(4)), + ("$ev6", None), + ] + ); + + // A timeline item has been inserted at the back, and maps to `$ev6`. + events.timeline_item_has_been_inserted_at(5, Some(6)); + + assert_events!( + events, + [ + ("$ev0", Some(0)), + ("$ev1", Some(1)), + ("$ev2", None), + ("$ev3", None), + ("$ev4", Some(3)), + ("$ev5", Some(4)), + // `timeline_item_index` has been updated + ("$ev6", Some(5)), + ] + ); + } + + #[test] + fn test_timeline_item_has_been_removed_at() { + let mut events = AllRemoteEvents::default(); + + // Push some events. + events.push_back(event_meta("$ev0", Some(0))); + events.push_back(event_meta("$ev1", Some(1))); + events.push_back(event_meta("$ev2", None)); + events.push_back(event_meta("$ev3", None)); + events.push_back(event_meta("$ev4", Some(3))); + events.push_back(event_meta("$ev5", Some(4))); + events.push_back(event_meta("$ev6", None)); + + // A timeline item has been removed at index 2, which maps to no event. + events.timeline_item_has_been_removed_at(2); + + assert_events!( + events, + [ + ("$ev0", Some(0)), + ("$ev1", Some(1)), + ("$ev2", None), + ("$ev3", None), + // `timeline_item_index` is shifted once + ("$ev4", Some(2)), + // `timeline_item_index` is shifted once + ("$ev5", Some(3)), + ("$ev6", None), + ] + ); + + // A timeline item has been removed at index 2, which maps to `$ev4`. + events.timeline_item_has_been_removed_at(2); + + assert_events!( + events, + [ + ("$ev0", Some(0)), + ("$ev1", Some(1)), + ("$ev2", None), + ("$ev3", None), + // `timeline_item_index` has been updated + ("$ev4", None), + // `timeline_item_index` has shifted once + ("$ev5", Some(2)), + ("$ev6", None), + ] + ); + + // A timeline item has been removed at index 0, which maps to `$ev0`. + events.timeline_item_has_been_removed_at(0); + + assert_events!( + events, + [ + // `timeline_item_index` has been updated + ("$ev0", None), + // `timeline_item_index` has shifted once + ("$ev1", Some(0)), + ("$ev2", None), + ("$ev3", None), + ("$ev4", None), + // `timeline_item_index` has shifted once + ("$ev5", Some(1)), + ("$ev6", None), + ] + ); + } +} From 92cb18207e50b4dfd0fd0108ca0b184ce3b65b58 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 4 Dec 2024 21:16:48 +0100 Subject: [PATCH 735/979] test(ui): Write test suite for `ObservableItems`. --- .../timeline/controller/observable_items.rs | 737 ++++++++++++++++++ 1 file changed, 737 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index e5a2f4134c2..f11301059db 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -139,6 +139,7 @@ impl ObservableItemsEntries<'_> { } /// A handle to a single timeline item in an `ObservableItems`. +#[derive(Debug)] pub struct ObservableItemsEntry<'a>(ObservableVectorEntry<'a, Arc>); impl ObservableItemsEntry<'_> { @@ -333,6 +334,740 @@ impl Deref for ObservableItemsTransactionEntry<'_, '_> { } } +#[cfg(test)] +mod observable_items_tests { + use std::ops::Not; + + use assert_matches::assert_matches; + use eyeball_im::VectorDiff; + use ruma::{ + events::room::message::{MessageType, TextMessageEventContent}, + owned_user_id, MilliSecondsSinceUnixEpoch, + }; + use stream_assert::assert_next_matches; + + use super::*; + use crate::timeline::{ + controller::{EventTimelineItemKind, RemoteEventOrigin}, + event_item::RemoteEventTimelineItem, + EventTimelineItem, Message, TimelineDetails, TimelineItemContent, TimelineUniqueId, + }; + + fn item(event_id: &str) -> Arc { + TimelineItem::new( + EventTimelineItem::new( + owned_user_id!("@ivan:mnt.io"), + TimelineDetails::Unavailable, + MilliSecondsSinceUnixEpoch(0u32.into()), + TimelineItemContent::Message(Message { + msgtype: MessageType::Text(TextMessageEventContent::plain("hello")), + in_reply_to: None, + thread_root: None, + edited: false, + mentions: None, + }), + EventTimelineItemKind::Remote(RemoteEventTimelineItem { + event_id: event_id.parse().unwrap(), + transaction_id: None, + read_receipts: Default::default(), + is_own: false, + is_highlighted: false, + encryption_info: None, + original_json: None, + latest_edit_json: None, + origin: RemoteEventOrigin::Sync, + }), + Default::default(), + false, + ), + TimelineUniqueId(format!("__id_{event_id}")), + ) + } + + fn read_marker() -> Arc { + TimelineItem::read_marker() + } + + fn event_meta(event_id: &str) -> EventMeta { + EventMeta { event_id: event_id.parse().unwrap(), timeline_item_index: None, visible: false } + } + + macro_rules! assert_event_id { + ( $timeline_item:expr, $event_id:literal $( , $message:expr )? $(,)? ) => { + assert_eq!($timeline_item.as_event().unwrap().event_id().unwrap().as_str(), $event_id $( , $message)? ); + }; + } + + macro_rules! assert_mapping { + ( on $transaction:ident: + | event_id | event_index | timeline_item_index | + | $( - )+ | $( - )+ | $( - )+ | + $( + | $event_id:literal | $event_index:literal | $( $timeline_item_index:literal )? | + )+ + ) => { + let all_remote_events = $transaction .all_remote_events(); + + $( + // Remote event exists at this index… + assert_matches!(all_remote_events.0.get( $event_index ), Some(EventMeta { event_id, timeline_item_index, .. }) => { + // … this is the remote event with the expected event ID + assert_eq!( + event_id.as_str(), + $event_id , + concat!("event #", $event_index, " should have ID ", $event_id) + ); + + + // (tiny hack to handle the case where `$timeline_item_index` is absent) + #[allow(unused_variables)] + let timeline_item_index_is_expected = false; + $( + let timeline_item_index_is_expected = true; + let _ = $timeline_item_index; + )? + + if timeline_item_index_is_expected.not() { + // … this remote event does NOT map to a timeline item index + assert!( + timeline_item_index.is_none(), + concat!("event #", $event_index, " with ID ", $event_id, " should NOT map to a timeline item index" ) + ); + } + + $( + // … this remote event maps to a timeline item index + assert_eq!( + *timeline_item_index, + Some( $timeline_item_index ), + concat!("event #", $event_index, " with ID ", $event_id, " should map to timeline item #", $timeline_item_index ) + ); + + // … this timeline index exists + assert_matches!( $transaction .get( $timeline_item_index ), Some(timeline_item) => { + // … this timelime item has the expected event ID + assert_event_id!( + timeline_item, + $event_id , + concat!("timeline item #", $timeline_item_index, " should map to event ID ", $event_id ) + ); + }); + )? + }); + )* + } +} + + #[test] + fn test_is_empty() { + let mut items = ObservableItems::new(); + + assert!(items.is_empty()); + + // Push one event to check if `is_empty` returns false. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.commit(); + + assert!(items.is_empty().not()); + } + + #[test] + fn test_subscribe() { + let mut items = ObservableItems::new(); + let mut subscriber = items.subscribe().into_stream(); + + // Push one event to check the subscriber is emitting something. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.commit(); + + // It does! + assert_next_matches!(subscriber, VectorDiff::PushBack { value: event } => { + assert_event_id!(event, "$ev0"); + }); + } + + #[test] + fn test_clone() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.push_back(item("$ev1"), Some(1)); + transaction.commit(); + + let events = items.clone(); + assert_eq!(events.len(), 2); + assert_event_id!(events[0], "$ev0"); + assert_event_id!(events[1], "$ev1"); + } + + #[test] + fn test_replace() { + let mut items = ObservableItems::new(); + + // Push one event that will be replaced. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.commit(); + + // That's time to replace it! + items.replace(0, item("$ev1")); + + let events = items.clone(); + assert_eq!(events.len(), 1); + assert_event_id!(events[0], "$ev1"); + } + + #[test] + fn test_entries() { + let mut items = ObservableItems::new(); + + // Push events to iterate on. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.push_back(item("$ev1"), Some(1)); + transaction.push_back(item("$ev2"), Some(2)); + transaction.commit(); + + let mut entries = items.entries(); + + assert_matches!(entries.next(), Some(entry) => { + assert_event_id!(entry, "$ev0"); + }); + assert_matches!(entries.next(), Some(entry) => { + assert_event_id!(entry, "$ev1"); + }); + assert_matches!(entries.next(), Some(entry) => { + assert_event_id!(entry, "$ev2"); + }); + assert_matches!(entries.next(), None); + } + + #[test] + fn test_entry_replace() { + let mut items = ObservableItems::new(); + + // Push events to iterate on. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.commit(); + + let mut entries = items.entries(); + + // Replace one event by another one. + assert_matches!(entries.next(), Some(mut entry) => { + assert_event_id!(entry, "$ev0"); + ObservableItemsEntry::replace(&mut entry, item("$ev1")); + }); + assert_matches!(entries.next(), None); + + // Check the new event. + let mut entries = items.entries(); + + assert_matches!(entries.next(), Some(entry) => { + assert_event_id!(entry, "$ev1"); + }); + assert_matches!(entries.next(), None); + } + + #[test] + fn test_for_each() { + let mut items = ObservableItems::new(); + + // Push events to iterate on. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.push_back(item("$ev1"), Some(1)); + transaction.push_back(item("$ev2"), Some(2)); + transaction.commit(); + + let mut nth = 0; + + // Iterate over events. + items.for_each(|entry| { + match nth { + 0 => { + assert_event_id!(entry, "$ev0"); + } + 1 => { + assert_event_id!(entry, "$ev1"); + } + 2 => { + assert_event_id!(entry, "$ev2"); + } + _ => unreachable!(), + } + + nth += 1; + }); + } + + #[test] + fn test_transaction_commit() { + let mut items = ObservableItems::new(); + + // Don't commit the transaction. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + drop(transaction); + + assert!(items.is_empty()); + + // Commit the transaction. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.commit(); + + assert!(items.is_empty().not()); + } + + #[test] + fn test_transaction_get() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + + assert_matches!(transaction.get(0), Some(event) => { + assert_event_id!(event, "$ev0"); + }); + } + + #[test] + fn test_transaction_replace() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.replace(0, item("$ev1")); + + assert_matches!(transaction.get(0), Some(event) => { + assert_event_id!(event, "$ev1"); + }); + } + + #[test] + fn test_transaction_insert() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev0")); + transaction.insert(0, item("$ev0"), Some(0)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | // new + } + + // Timeline item without a remote event (for example a read marker). + transaction.insert(0, read_marker(), None); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 1 | // has shifted + } + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev1")); + transaction.insert(2, item("$ev1"), Some(1)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 1 | + | "$ev1" | 1 | 2 | // new + } + + // Remote event without a timeline item (for example a state event). + transaction.push_back_remote_event(event_meta("$ev2")); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 1 | + | "$ev1" | 1 | 2 | + | "$ev2" | 2 | | // new + } + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev3")); + transaction.insert(3, item("$ev3"), Some(3)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 1 | + | "$ev1" | 1 | 2 | + | "$ev2" | 2 | | + | "$ev3" | 3 | 3 | // new + } + + // Timeline item with a remote event, but late. + // I don't know if this case is possible in reality, but let's be robust. + transaction.insert(3, item("$ev2"), Some(2)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 1 | + | "$ev1" | 1 | 2 | + | "$ev2" | 2 | 3 | // updated + | "$ev3" | 3 | 4 | // has shifted + } + + // Let's move the read marker for the fun. + transaction.remove(0); + transaction.insert(2, read_marker(), None); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | // has shifted + | "$ev1" | 1 | 1 | // has shifted + | "$ev2" | 2 | 3 | + | "$ev3" | 3 | 4 | + } + + assert_eq!(transaction.len(), 5); + } + + #[test] + fn test_transaction_push_front() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + + // Remote event with its timeline item. + transaction.push_front_remote_event(event_meta("$ev0")); + transaction.push_front(item("$ev0"), Some(0)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | // new + } + + // Timeline item without a remote event (for example a read marker). + transaction.push_front(read_marker(), None); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 1 | // has shifted + } + + // Remote event with its timeline item. + transaction.push_front_remote_event(event_meta("$ev1")); + transaction.push_front(item("$ev1"), Some(0)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev1" | 0 | 0 | // new + | "$ev0" | 1 | 2 | // has shifted + } + + // Remote event without a timeline item (for example a state event). + transaction.push_front_remote_event(event_meta("$ev2")); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev2" | 0 | | + | "$ev1" | 1 | 0 | // has shifted + | "$ev0" | 2 | 2 | // has shifted + } + + // Remote event with its timeline item. + transaction.push_front_remote_event(event_meta("$ev3")); + transaction.push_front(item("$ev3"), Some(0)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev3" | 0 | 0 | // new + | "$ev2" | 1 | | + | "$ev1" | 2 | 1 | // has shifted + | "$ev0" | 3 | 3 | // has shifted + } + + assert_eq!(transaction.len(), 4); + } + + #[test] + fn test_transaction_push_back() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev0")); + transaction.push_back(item("$ev0"), Some(0)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | // new + } + + // Timeline item without a remote event (for example a read marker). + transaction.push_back(read_marker(), None); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + } + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev1")); + transaction.push_back(item("$ev1"), Some(1)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | 2 | // new + } + + // Remote event without a timeline item (for example a state event). + transaction.push_back_remote_event(event_meta("$ev2")); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | 2 | + | "$ev2" | 2 | | // new + } + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev3")); + transaction.push_back(item("$ev3"), Some(3)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | 2 | + | "$ev2" | 2 | | + | "$ev3" | 3 | 3 | // new + } + + assert_eq!(transaction.len(), 4); + } + + #[test] + fn test_transaction_remove() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev0")); + transaction.push_back(item("$ev0"), Some(0)); + + // Timeline item without a remote event (for example a read marker). + transaction.push_back(read_marker(), None); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev1")); + transaction.push_back(item("$ev1"), Some(1)); + + // Remote event without a timeline item (for example a state event). + transaction.push_back_remote_event(event_meta("$ev2")); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev3")); + transaction.push_back(item("$ev3"), Some(3)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | 2 | + | "$ev2" | 2 | | + | "$ev3" | 3 | 3 | + } + + // Remove the timeline item that has no event. + transaction.remove(1); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | 1 | // has shifted + | "$ev2" | 2 | | + | "$ev3" | 3 | 2 | // has shifted + } + + // Remove an timeline item that has an event. + transaction.remove(1); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | | // has been removed + | "$ev2" | 2 | | + | "$ev3" | 3 | 1 | // has shifted + } + + // Remove the last timeline item to test off by 1 error. + transaction.remove(1); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | | + | "$ev2" | 2 | | + | "$ev3" | 3 | | // has been removed + } + + // Remove all the items \o/ + transaction.remove(0); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | | // has been removed + | "$ev1" | 1 | | + | "$ev2" | 2 | | + | "$ev3" | 3 | | + } + + assert!(transaction.is_empty()); + } + + #[test] + fn test_transaction_clear() { + let mut items = ObservableItems::new(); + + let mut transaction = items.transaction(); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev0")); + transaction.push_back(item("$ev0"), Some(0)); + + // Timeline item without a remote event (for example a read marker). + transaction.push_back(read_marker(), None); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev1")); + transaction.push_back(item("$ev1"), Some(1)); + + // Remote event without a timeline item (for example a state event). + transaction.push_back_remote_event(event_meta("$ev2")); + + // Remote event with its timeline item. + transaction.push_back_remote_event(event_meta("$ev3")); + transaction.push_back(item("$ev3"), Some(3)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | 2 | + | "$ev2" | 2 | | + | "$ev3" | 3 | 3 | + } + + assert_eq!(transaction.all_remote_events().0.len(), 4); + assert_eq!(transaction.len(), 4); + + // Let's clear everything. + transaction.clear(); + + assert!(transaction.all_remote_events().0.is_empty()); + assert!(transaction.is_empty()); + } + + #[test] + fn test_transaction_for_each() { + let mut items = ObservableItems::new(); + + // Push events to iterate on. + let mut transaction = items.transaction(); + transaction.push_back(item("$ev0"), Some(0)); + transaction.push_back(item("$ev1"), Some(1)); + transaction.push_back(item("$ev2"), Some(2)); + + let mut nth = 0; + + // Iterate over events. + transaction.for_each(|entry| { + match nth { + 0 => { + assert_event_id!(entry, "$ev0"); + } + 1 => { + assert_event_id!(entry, "$ev1"); + } + 2 => { + assert_event_id!(entry, "$ev2"); + } + _ => unreachable!(), + } + + nth += 1; + }); + } +} + /// A type for all remote events. /// /// Having this type helps to know exactly which parts of the code and how they @@ -509,12 +1244,14 @@ mod all_remote_events_tests { fn test_clear() { let mut events = AllRemoteEvents::default(); + // Push some events. events.push_back(event_meta("$ev0", None)); events.push_back(event_meta("$ev1", None)); events.push_back(event_meta("$ev2", None)); assert_eq!(events.iter().count(), 3); + // And clear them! events.clear(); assert_eq!(events.iter().count(), 0); From ed1f2e29ed95ade544006553e1823fddf57d88ba Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 15:28:11 +0100 Subject: [PATCH 736/979] refactor(ui): `ObservableItemsTransactionEntry::remove` is no longer unsafe --- .../timeline/controller/observable_items.rs | 78 +++++++++++++++---- .../src/timeline/controller/state.rs | 7 +- 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index f11301059db..6c5adde7b31 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -284,7 +284,9 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { where F: FnMut(ObservableItemsTransactionEntry<'_, 'observable_items>), { - self.items.for_each(|entry| f(ObservableItemsTransactionEntry(entry))) + self.items.for_each(|entry| { + f(ObservableItemsTransactionEntry { entry, all_remote_events: self.all_remote_events }) + }) } /// Commit this transaction, persisting the changes and notifying @@ -304,25 +306,27 @@ impl Deref for ObservableItemsTransaction<'_> { } /// A handle to a single timeline item in an `ObservableItemsTransaction`. -pub struct ObservableItemsTransactionEntry<'a, 'observable_items>( - ObservableVectorTransactionEntry<'a, 'observable_items, Arc>, -); +pub struct ObservableItemsTransactionEntry<'observable_transaction_items, 'observable_items> { + entry: ObservableVectorTransactionEntry< + 'observable_transaction_items, + 'observable_items, + Arc, + >, + all_remote_events: &'observable_transaction_items mut AllRemoteEvents, +} impl ObservableItemsTransactionEntry<'_, '_> { /// Replace the timeline item by `timeline_item`. pub fn replace(this: &mut Self, timeline_item: Arc) -> Arc { - ObservableVectorTransactionEntry::set(&mut this.0, timeline_item) + ObservableVectorTransactionEntry::set(&mut this.entry, timeline_item) } /// Remove this timeline item. - /// - /// # Safety - /// - /// This method doesn't update `AllRemoteEvents`. Be sure that the caller - /// doesn't break the mapping between remote events and timeline items. See - /// [`EventMeta::timeline_item_index`] to learn more. - pub unsafe fn remove(this: Self) { - ObservableVectorTransactionEntry::remove(this.0); + pub fn remove(this: Self) { + let entry_index = ObservableVectorTransactionEntry::index(&this.entry); + + ObservableVectorTransactionEntry::remove(this.entry); + this.all_remote_events.timeline_item_has_been_removed_at(entry_index); } } @@ -330,7 +334,7 @@ impl Deref for ObservableItemsTransactionEntry<'_, '_> { type Target = Arc; fn deref(&self) -> &Self::Target { - &self.0 + &self.entry } } @@ -1066,6 +1070,52 @@ mod observable_items_tests { nth += 1; }); } + + #[test] + fn test_transaction_for_each_remove() { + let mut items = ObservableItems::new(); + + // Push events to iterate on. + let mut transaction = items.transaction(); + + transaction.push_back_remote_event(event_meta("$ev0")); + transaction.push_back(item("$ev0"), Some(0)); + + transaction.push_back_remote_event(event_meta("$ev1")); + transaction.push_back(item("$ev1"), Some(1)); + + transaction.push_back_remote_event(event_meta("$ev2")); + transaction.push_back(item("$ev2"), Some(2)); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev1" | 1 | 1 | + | "$ev2" | 2 | 2 | + } + + // Iterate over events, and remove one. + transaction.for_each(|entry| { + if entry.as_event().unwrap().event_id().unwrap().as_str() == "$ev1" { + ObservableItemsTransactionEntry::remove(entry); + } + }); + + assert_mapping! { + on transaction: + + | event_id | event_index | timeline_item_index | + |----------|-------------|---------------------| + | "$ev0" | 0 | 0 | + | "$ev2" | 2 | 1 | // has shifted + } + + assert_eq!(transaction.all_remote_events().0.len(), 3); + assert_eq!(transaction.len(), 2); + } } /// A type for all remote events. diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 520f3ac82e4..4264f3442f4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -657,12 +657,7 @@ impl TimelineStateTransaction<'_> { // Remove all remote events and the read marker self.items.for_each(|entry| { if entry.is_remote_event() || entry.is_read_marker() { - // SAFETY: this method removes all events except local events. Local events - // don't have a mapping from remote events to timeline items because… well… they - // are local events, not remove events. - unsafe { - ObservableItemsTransactionEntry::remove(entry); - } + ObservableItemsTransactionEntry::remove(entry); } }); From e32ea1627e320f998572c4d51e248e53ac0c46ab Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 15:58:48 +0100 Subject: [PATCH 737/979] doc(ui): Fix typos. --- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4264f3442f4..878573af3ea 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -1199,7 +1199,7 @@ pub(crate) struct EventMeta { /// /// Once rendered in a timeline, it for example produces: /// - /// | index | item | aside items | + /// | index | item | related items | /// +-------+-------------------+----------------------+ /// | 0 | content of `$ev0` | | /// | 1 | content of `$ev2` | reaction with `$ev4` | @@ -1211,10 +1211,10 @@ pub(crate) struct EventMeta { /// a reaction to `$ev2`. Finally note that `$ev1` is not rendered in /// the timeline. /// - /// The mapping between remove event index to timeline item index will look + /// The mapping between remote event index to timeline item index will look /// like this: /// - /// | remove event index | timeline item index | comment | + /// | remote event index | timeline item index | comment | /// +--------------------+---------------------+--------------------------------------------+ /// | 0 | `Some(0)` | `$ev0` is rendered as the #0 timeline item | /// | 1 | `None` | `$ev1` isn't rendered in the timeline | From 72f1bd618056ca61ec44bd55b1dd413b56a5a0f8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 16:04:31 +0100 Subject: [PATCH 738/979] doc(ui): Document `ObservableItems::items` more and `TimelineItemKind`. --- .../src/timeline/controller/observable_items.rs | 6 +++++- crates/matrix-sdk-ui/src/timeline/item.rs | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 6c5adde7b31..bc02b8b50d6 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -35,7 +35,11 @@ use super::{state::EventMeta, TimelineItem}; pub struct ObservableItems { /// All timeline items. /// - /// Yeah, there are here! + /// Yeah, there are here! This [`ObservableVector`] contains all the + /// timeline items that are rendered in your magnificent Matrix client. + /// + /// These items are the _core_ of the timeline, see [`TimelineItem`] to + /// learn more. items: ObservableVector>, /// List of all the remote events as received in the timeline, even the ones diff --git a/crates/matrix-sdk-ui/src/timeline/item.rs b/crates/matrix-sdk-ui/src/timeline/item.rs index 8096da48c1a..6487d28f6d7 100644 --- a/crates/matrix-sdk-ui/src/timeline/item.rs +++ b/crates/matrix-sdk-ui/src/timeline/item.rs @@ -26,13 +26,14 @@ use super::{EventTimelineItem, VirtualTimelineItem}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct TimelineUniqueId(pub String); +/// The type of timeline item. #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum TimelineItemKind { /// An event or aggregation of multiple events. Event(EventTimelineItem), /// An item that doesn't correspond to an event, for example the user's - /// own read marker. + /// own read marker, or a day divider. Virtual(VirtualTimelineItem), } From 13e26b13e76f2728e9c4c276d2c56f37e53b0e0e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 16:11:16 +0100 Subject: [PATCH 739/979] doc(ui): Rephrase the documentation of `ObservableItems::all_remote_events`. --- .../src/timeline/controller/observable_items.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index bc02b8b50d6..74dac26dbae 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -45,9 +45,10 @@ pub struct ObservableItems { /// List of all the remote events as received in the timeline, even the ones /// that are discarded in the timeline items. /// - /// This is useful to get this for the moment as it helps the `Timeline` to - /// compute read receipts and read markers. It also helps to map event to - /// timeline item, see [`EventMeta::timeline_item_index`] to learn more. + /// The list of all remote events is used to compute the read receipts and + /// read markers; additionally it's used to map events to timeline items, + /// for more info about that, take a look at the documentation for + /// [`EventMeta::timeline_item_index`]. all_remote_events: AllRemoteEvents, } From 0d17ea353fb89a63b5797443c6b577116938f6ef Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 16:34:04 +0100 Subject: [PATCH 740/979] refactor(ui): Replace a panic by a sensible value + `error!`. --- .../src/timeline/event_handler.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 9c8a63f203b..1eba4147626 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -1180,12 +1180,18 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { }) .unwrap_or(0); - let event_index = - Some(self.items.all_remote_events().last_index() - // The last remote event is necessarily associated to this - // timeline item, see the contract of this method. - .expect("A timeline item is being added but its associated remote event is missing") - ); + let event_index = self + .items + .all_remote_events() + .last_index() + // The last remote event is necessarily associated to this + // timeline item, see the contract of this method. Let's fallback to a similar + // value as `timeline_item_index` instead of panicking. + .or_else(|| { + error!(?event_id, "Failed to read the last event index from `AllRemoteEvents`: at least one event must be present"); + + Some(0) + }); // Try to keep precise insertion semantics here, in this exact order: // From 0f2ada09582c841e0b2a7aa3b0815b70c33ebc90 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 16:37:07 +0100 Subject: [PATCH 741/979] refactor(ui): Rename `ObservableItems::clone` to `clone_items`. --- .../src/timeline/controller/mod.rs | 6 +++--- .../timeline/controller/observable_items.rs | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 9a22bb91753..5d82572470c 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -467,7 +467,7 @@ impl TimelineController

{ /// /// Cheap because `im::Vector` is cheap to clone. pub(super) async fn items(&self) -> Vector> { - self.state.read().await.items.clone() + self.state.read().await.items.clone_items() } pub(super) async fn subscribe( @@ -475,7 +475,7 @@ impl TimelineController

{ ) -> (Vector>, impl Stream>> + Send) { trace!("Creating timeline items signal"); let state = self.state.read().await; - (state.items.clone(), state.items.subscribe().into_stream()) + (state.items.clone_items(), state.items.subscribe().into_stream()) } pub(super) async fn subscribe_batched( @@ -483,7 +483,7 @@ impl TimelineController

{ ) -> (Vector>, impl Stream>>>) { trace!("Creating timeline items signal"); let state = self.state.read().await; - (state.items.clone(), state.items.subscribe().into_batched_stream()) + (state.items.clone_items(), state.items.subscribe().into_batched_stream()) } pub(super) async fn subscribe_filter_map( diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 74dac26dbae..a1b6697ae98 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -82,7 +82,7 @@ impl ObservableItems { /// Get a clone of all timeline items. /// /// Note that it doesn't clone `Self`, only the inner timeline items. - pub fn clone(&self) -> Vector> { + pub fn clone_items(&self) -> Vector> { self.items.clone() } @@ -498,7 +498,7 @@ mod observable_items_tests { } #[test] - fn test_clone() { + fn test_clone_items() { let mut items = ObservableItems::new(); let mut transaction = items.transaction(); @@ -506,10 +506,10 @@ mod observable_items_tests { transaction.push_back(item("$ev1"), Some(1)); transaction.commit(); - let events = items.clone(); - assert_eq!(events.len(), 2); - assert_event_id!(events[0], "$ev0"); - assert_event_id!(events[1], "$ev1"); + let items = items.clone_items(); + assert_eq!(items.len(), 2); + assert_event_id!(items[0], "$ev0"); + assert_event_id!(items[1], "$ev1"); } #[test] @@ -524,9 +524,9 @@ mod observable_items_tests { // That's time to replace it! items.replace(0, item("$ev1")); - let events = items.clone(); - assert_eq!(events.len(), 1); - assert_event_id!(events[0], "$ev1"); + let items = items.clone_items(); + assert_eq!(items.len(), 1); + assert_event_id!(items[0], "$ev1"); } #[test] From 6b56c9efd83deb1121bb2f1f9eec0a73d6073a07 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 16:39:40 +0100 Subject: [PATCH 742/979] doc(ui): Explain why `Deref` is fine, but `DerefMut` is not. --- .../timeline/controller/observable_items.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index a1b6697ae98..ad2af19c0f7 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -124,6 +124,10 @@ impl ObservableItems { } // It's fine to deref to an immutable reference to `Vector`. +// +// We don't want, however, to deref to a mutable reference: it should be done +// via proper methods to control precisely the mapping between remote events and +// timeline items. impl Deref for ObservableItems { type Target = Vector>; @@ -154,6 +158,11 @@ impl ObservableItemsEntry<'_> { } } +// It's fine to deref to an immutable reference to `Arc`. +// +// We don't want, however, to deref to a mutable reference: it should be done +// via proper methods to control precisely the mapping between remote events and +// timeline items. impl Deref for ObservableItemsEntry<'_> { type Target = Arc; @@ -302,6 +311,10 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { } // It's fine to deref to an immutable reference to `Vector`. +// +// We don't want, however, to deref to a mutable reference: it should be done +// via proper methods to control precisely the mapping between remote events and +// timeline items. impl Deref for ObservableItemsTransaction<'_> { type Target = Vector>; @@ -335,6 +348,11 @@ impl ObservableItemsTransactionEntry<'_, '_> { } } +// It's fine to deref to an immutable reference to `Arc`. +// +// We don't want, however, to deref to a mutable reference: it should be done +// via proper methods to control precisely the mapping between remote events and +// timeline items. impl Deref for ObservableItemsTransactionEntry<'_, '_> { type Target = Arc; From 9a08975c8e9b060602c4ab42471efae916b76418 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 16:50:25 +0100 Subject: [PATCH 743/979] doc(ui): Explain why `ObservableItemsEntries` does not implement `Iterator`. --- .../src/timeline/controller/observable_items.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index ad2af19c0f7..7d36c1adee0 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -136,7 +136,12 @@ impl Deref for ObservableItems { } } -/// An “iterator“ that yields entries into an `ObservableItems`. +/// An iterator that yields entries into an `ObservableItems`. +/// +/// It doesn't implement [`Iterator`] though because of a lifetime conflict: the +/// returned `Iterator::Item` could live longer than the `Iterator` itself. +/// Ideally, `Iterator::next` should take a `&'a mut self`, but this is not +/// possible. pub struct ObservableItemsEntries<'a>(ObservableVectorEntries<'a, Arc>); impl ObservableItemsEntries<'_> { From 352676158098cab5e93fc4a525bc752a4ddfa2aa Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 17:10:02 +0100 Subject: [PATCH 744/979] doc(ui): Unfold a `Self` in the doc. --- .../matrix-sdk-ui/src/timeline/controller/observable_items.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 7d36c1adee0..ab6d992953a 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -180,8 +180,8 @@ impl Deref for ObservableItemsEntry<'_> { /// an atomic unit. /// /// For updates from the transaction to have affect, it has to be finalized with -/// [`Self::commit`]. If the transaction is dropped without that method being -/// called, the updates will be discarded. +/// [`ObservableItemsTransaction::commit`]. If the transaction is dropped +/// without that method being called, the updates will be discarded. #[derive(Debug)] pub struct ObservableItemsTransaction<'observable_items> { items: ObservableVectorTransaction<'observable_items, Arc>, From ee94c86164430bf0c51233d60326caacc09c303c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 17:10:57 +0100 Subject: [PATCH 745/979] doc(ui): Fix a typo. --- .../matrix-sdk-ui/src/timeline/controller/observable_items.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index ab6d992953a..5ff68c3f513 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -189,7 +189,7 @@ pub struct ObservableItemsTransaction<'observable_items> { } impl<'observable_items> ObservableItemsTransaction<'observable_items> { - /// Get a reference to the timeline index at position `timeline_item_index`. + /// Get a reference to the timeline item at position `timeline_item_index`. pub fn get(&self, timeline_item_index: usize) -> Option<&Arc> { self.items.get(timeline_item_index) } From cf178d603c13814f255bd4fa899b372fe33ed5f0 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 17:14:44 +0100 Subject: [PATCH 746/979] doc(ui): Fix typos. --- .../src/timeline/controller/observable_items.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 5ff68c3f513..e5f62d70716 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -199,7 +199,7 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.all_remote_events } - /// Remove a remote event at position `event_index`. + /// Remove a remote event at the `event_index` position. /// /// Not to be confused with removing a timeline item! pub fn remove_remote_event(&mut self, event_index: usize) -> Option { @@ -208,14 +208,14 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { /// Push a new remote event at the front of all remote events. /// - /// Not to be confused with pushing front a timeline item! + /// Not to be confused with pushing a timeline item to the front! pub fn push_front_remote_event(&mut self, event_meta: EventMeta) { self.all_remote_events.push_front(event_meta); } /// Push a new remote event at the back of all remote events. /// - /// Not to be confused with pushing back a timeline item! + /// Not to be confused with pushing a timeline item to the back! pub fn push_back_remote_event(&mut self, event_meta: EventMeta) { self.all_remote_events.push_back(event_meta); } From cf02e694f20a1ab9db08dd199c3faab54c1c8d49 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 14:54:13 +0100 Subject: [PATCH 747/979] feat(event cache store): add a method to clear all rooms' linked chunks --- .../event_cache/store/integration_tests.rs | 69 +++++++++++++++++++ .../src/event_cache/store/memory_store.rs | 5 ++ .../src/event_cache/store/traits.rs | 10 +++ .../src/linked_chunk/relational.rs | 6 ++ .../src/event_cache_store.rs | 11 +++ 5 files changed, 101 insertions(+) diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index ad484c56985..0c4f36bbb7f 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -114,6 +114,9 @@ pub trait EventCacheStoreIntegrationTests { /// Test that rebuilding a linked chunk from an empty store doesn't return /// anything. async fn test_rebuild_empty_linked_chunk(&self); + + /// Test that clear all the rooms' linked chunks works. + async fn test_clear_all_rooms_chunks(&self); } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -372,6 +375,65 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { // When I rebuild a linked chunk from an empty store, it's empty. assert!(self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap().is_none()); } + + async fn test_clear_all_rooms_chunks(&self) { + use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId; + + let r0 = room_id!("!r0:matrix.org"); + let r1 = room_id!("!r1:matrix.org"); + + // Add updates for the first room. + self.handle_linked_chunk_updates( + r0, + vec![ + // new chunk + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")], + }, + ], + ) + .await + .unwrap(); + + // Add updates for the second room. + self.handle_linked_chunk_updates( + r1, + vec![ + // Empty items chunk. + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // a gap chunk + Update::NewGapChunk { + previous: Some(CId::new(0)), + new: CId::new(1), + next: None, + gap: Gap { prev_token: "bleu d'auvergne".to_owned() }, + }, + // another items chunk + Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(2), 0), + items: vec![make_test_event(r0, "yummy")], + }, + ], + ) + .await + .unwrap(); + + // Sanity check: both linked chunks can be reloaded. + assert!(self.reload_linked_chunk(r0).await.unwrap().is_some()); + assert!(self.reload_linked_chunk(r1).await.unwrap().is_some()); + + // Clear the chunks. + self.clear_all_rooms_chunks().await.unwrap(); + + // Both rooms now have no linked chunk. + assert!(self.reload_linked_chunk(r0).await.unwrap().is_none()); + assert!(self.reload_linked_chunk(r1).await.unwrap().is_none()); + } } /// Macro building to allow your `EventCacheStore` implementation to run the @@ -440,6 +502,13 @@ macro_rules! event_cache_store_integration_tests { get_event_cache_store().await.unwrap().into_event_cache_store(); event_cache_store.test_rebuild_empty_linked_chunk().await; } + + #[async_test] + async fn test_clear_all_rooms_chunks() { + let event_cache_store = + get_event_cache_store().await.unwrap().into_event_cache_store(); + event_cache_store.test_clear_all_rooms_chunks().await; + } } }; } diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 78fc8cfa567..8f921b5808d 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -115,6 +115,11 @@ impl EventCacheStore for MemoryStore { Ok(result) } + async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> { + self.inner.write().unwrap().events.clear(); + Ok(()) + } + async fn add_media_content( &self, request: &MediaRequestParameters, diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 94bc4a94f1e..50707a6dd50 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -62,6 +62,12 @@ pub trait EventCacheStore: AsyncTraitDeps { room_id: &RoomId, ) -> Result>, Self::Error>; + /// Clear persisted events for all the rooms. + /// + /// This will empty and remove all the linked chunks stored previously, + /// using the above [`Self::handle_linked_chunk_updates`] methods. + async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>; + /// Add a media file's content in the media store. /// /// # Arguments @@ -188,6 +194,10 @@ impl EventCacheStore for EraseEventCacheStoreError { self.0.reload_linked_chunk(room_id).await.map_err(Into::into) } + async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> { + self.0.clear_all_rooms_chunks().await.map_err(Into::into) + } + async fn add_media_content( &self, request: &MediaRequestParameters, diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index 972e3cd8efb..27f13740616 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -80,6 +80,12 @@ impl RelationalLinkedChunk { Self { chunks: Vec::new(), items: Vec::new() } } + /// Removes all the chunks and items from this relational linked chunk. + pub fn clear(&mut self) { + self.chunks.clear(); + self.items.clear(); + } + /// Apply [`Update`]s. That's the only way to write data inside this /// relational linked chunk. pub fn apply_updates(&mut self, room_id: &RoomId, updates: Vec>) { diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 2e95141232f..08c8eb58435 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -609,6 +609,17 @@ impl EventCacheStore for SqliteEventCacheStore { }) } + async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> { + self.acquire() + .await? + .with_transaction(move |txn| { + // Remove all the chunks, and let cascading do its job. + txn.execute("DELETE FROM linked_chunks", ()) + }) + .await?; + Ok(()) + } + async fn add_media_content( &self, request: &MediaRequestParameters, From 0783cf89ba4946b7df5de1510f1ed99f3e6331fb Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 15:10:08 +0100 Subject: [PATCH 748/979] feat(ffi): add a feature flag to enable persistent storage for the event cache --- bindings/matrix-sdk-ffi/src/client_builder.rs | 36 +++++++++++++++++++ crates/matrix-sdk/src/client/mod.rs | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 0ac13e41798..189b9e48b52 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -8,6 +8,7 @@ use matrix_sdk::{ CollectStrategy, TrustRequirement, }, encryption::{BackupDownloadStrategy, EncryptionSettings}, + event_cache::EventCacheError, reqwest::Certificate, ruma::{ServerName, UserId}, sliding_sync::{ @@ -202,6 +203,8 @@ pub enum ClientBuildError { SlidingSyncVersion(VersionBuilderError), #[error(transparent)] Sdk(MatrixClientBuildError), + #[error(transparent)] + EventCache(#[from] EventCacheError), #[error("Failed to build the client: {message}")] Generic { message: String }, } @@ -269,6 +272,8 @@ pub struct ClientBuilder { room_key_recipient_strategy: CollectStrategy, decryption_trust_requirement: TrustRequirement, request_config: Option, + + use_event_cache_persistent_storage: Option, } #[matrix_sdk_ffi_macros::export] @@ -299,9 +304,25 @@ impl ClientBuilder { room_key_recipient_strategy: Default::default(), decryption_trust_requirement: TrustRequirement::Untrusted, request_config: Default::default(), + use_event_cache_persistent_storage: None, }) } + /// Whether to use the event cache persistent storage or not. + /// + /// This is a temporary feature flag, for testing the event cache's + /// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280. + /// + /// To enable the persistent storage: call this with `true`. + /// + /// To explicitly disable a previously-enabled persistent storage, call this + /// with `false` to clear the previous content in storage. + pub fn use_event_cache_persistent_storage(self: Arc, value: bool) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.use_event_cache_persistent_storage = Some(value); + Arc::new(builder) + } + pub fn cross_process_store_locks_holder_name( self: Arc, holder_name: String, @@ -624,6 +645,21 @@ impl ClientBuilder { let sdk_client = inner_builder.build().await?; + if let Some(val) = builder.use_event_cache_persistent_storage { + if val { + // Enable the persistent storage \o/ + sdk_client.event_cache().enable_storage()?; + } else { + // Get rid of the previous events, if any. + let store = sdk_client + .event_cache_store() + .lock() + .await + .map_err(EventCacheError::LockingStorage)?; + store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?; + } + } + Ok(Arc::new( Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate) .await?, diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index b288af97133..2f70e6b8822 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -615,7 +615,7 @@ impl Client { } /// Get a reference to the event cache store. - pub(crate) fn event_cache_store(&self) -> &EventCacheStoreLock { + pub fn event_cache_store(&self) -> &EventCacheStoreLock { self.base_client().event_cache_store() } From 4ee96aaffcadc43d99b2bdfd5e095786ff289d56 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 15:35:05 +0100 Subject: [PATCH 749/979] feat(event cache): add a way to clear a single room's persistent storage --- bindings/matrix-sdk-ffi/src/room.rs | 10 ++ crates/matrix-sdk/src/event_cache/mod.rs | 7 + crates/matrix-sdk/src/event_cache/room/mod.rs | 140 ++++++++++++++++++ 3 files changed, 157 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 47712d0ddf8..9f0e3014909 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -898,6 +898,16 @@ impl Room { Ok(()) } + + /// Clear the event cache storage for the current room. + /// + /// This will remove all the information related to the event cache, in + /// memory and in the persisted storage, if enabled. + pub async fn clear_event_cache_storage(&self) -> Result<(), ClientError> { + let (room_event_cache, _drop_handles) = self.inner.event_cache().await?; + room_event_cache.clear().await?; + Ok(()) + } } /// Generates a `matrix.to` permalink to the given room alias. diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index cda4fe48567..762e2a64950 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -352,6 +352,13 @@ struct AllEventsCache { relations: RelationsMap, } +impl AllEventsCache { + fn clear(&mut self) { + self.events.clear(); + self.relations.clear(); + } +} + struct EventCacheInner { /// A weak reference to the inner client, useful when trying to get a handle /// on the owning client. diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index cdebfc10326..57a504a987d 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -128,6 +128,27 @@ impl RoomEventCache { } } + /// Clear all the storage for this [`RoomEventCache`]. + /// + /// This will get rid of all the events from the linked chunk and persisted + /// storage. + pub async fn clear(&self) -> Result<()> { + // Clear the linked chunk and persisted storage. + self.inner.state.write().await.clear().await?; + + // Clear the (temporary) events mappings. + self.inner.all_events.write().await.clear(); + + // Reset the paginator. + // TODO: properly stop any ongoing back-pagination. + let _ = self.inner.paginator.set_idle_state(PaginatorState::Initial, None, None); + + // Notify observers about the update. + let _ = self.inner.sender.send(RoomEventCacheUpdate::Clear); + + Ok(()) + } + /// Looks for related event ids for the passed event id, and appends them to /// the `results` parameter. Then it'll recursively get the related /// event ids for those too. @@ -602,6 +623,14 @@ mod private { Ok(Self { room, store, events, waited_for_initial_prev_token: false }) } + /// Clear all cached content for this [`RoomEventCacheState`]. + pub async fn clear(&mut self) -> Result<(), EventCacheError> { + self.events.reset(); + self.propagate_changes().await?; + self.waited_for_initial_prev_token = false; + Ok(()) + } + /// Removes the bundled relations from an event, if they were present. /// /// Only replaces the present if it contained bundled relations. @@ -1112,6 +1141,117 @@ mod tests { assert!(chunks.next().is_none()); } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. + #[async_test] + async fn test_clear() { + use crate::{assert_let_timeout, event_cache::RoomEventCacheUpdate}; + + let room_id = room_id!("!galette:saucisse.bzh"); + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + let event_cache_store = Arc::new(MemoryStore::new()); + + let event_id1 = event_id!("$1"); + let event_id2 = event_id!("$2"); + + let ev1 = f.text_msg("hello world").sender(*ALICE).event_id(event_id1).into_sync(); + let ev2 = f.text_msg("how's it going").sender(*BOB).event_id(event_id2).into_sync(); + + // Prefill the store with some data. + event_cache_store + .handle_linked_chunk_updates( + room_id, + vec![ + // An empty items chunk. + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + // A gap chunk. + Update::NewGapChunk { + previous: Some(ChunkIdentifier::new(0)), + // Chunk IDs aren't supposed to be ordered, so use a random value here. + new: ChunkIdentifier::new(42), + next: None, + gap: Gap { prev_token: "cheddar".to_owned() }, + }, + // Another items chunk, non-empty this time. + Update::NewItemsChunk { + previous: Some(ChunkIdentifier::new(42)), + new: ChunkIdentifier::new(1), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(1), 0), + items: vec![ev1.clone()], + }, + // And another items chunk, non-empty again. + Update::NewItemsChunk { + previous: Some(ChunkIdentifier::new(1)), + new: ChunkIdentifier::new(2), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(2), 0), + items: vec![ev2.clone()], + }, + ], + ) + .await + .unwrap(); + + let client = MockClientBuilder::new("http://localhost".to_owned()) + .store_config( + StoreConfig::new("hodlor".to_owned()).event_cache_store(event_cache_store.clone()), + ) + .build() + .await; + + let event_cache = client.event_cache(); + + // Don't forget to subscribe and like^W enable storage! + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (items, mut stream) = room_event_cache.subscribe().await.unwrap(); + + // The rooms knows about the cached events. + assert!(room_event_cache.event(event_id1).await.is_some()); + + // The reloaded room must contain the two events. + assert_eq!(items.len(), 2); + assert_eq!(items[0].event_id().unwrap(), event_id1); + assert_eq!(items[1].event_id().unwrap(), event_id2); + + assert!(stream.is_empty()); + + // After clearing,… + room_event_cache.clear().await.unwrap(); + + //…we get an update that the content has been cleared. + assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = stream.recv()); + + // The room event cache has forgotten about the events. + assert!(room_event_cache.event(event_id1).await.is_none()); + + let (items, _) = room_event_cache.subscribe().await.unwrap(); + assert!(items.is_empty()); + + // The event cache store too. + let reloaded = event_cache_store.reload_linked_chunk(room_id).await.unwrap(); + // Note: while the event cache store could return `None` here, clearing it will + // reset it to its initial form, maintaining the invariant that it + // contains a single items chunk that's empty. + let linked_chunk = reloaded.unwrap(); + assert_eq!(linked_chunk.num_items(), 0); + } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. #[async_test] async fn test_load_from_storage() { From 32e2070f56731da80cf73fc662ac31f72febb42a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 10 Dec 2024 11:45:53 +0100 Subject: [PATCH 750/979] refactor(ffi): use a bool instead of an option to make the API less awkward By default, the event cache store will be disabled. If disabled, it will clean all the events in the cache store; most of the time this will do nothing, since the store will not even be filled with any event data, so it would be cheap to do. If some data was filled in the cache store before, then it would be cleared after the cache store has been disabled. This makes a less awkward API than the previous one, where `None` and `Some(false)` carried different semantics. --- bindings/matrix-sdk-ffi/src/client_builder.rs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 189b9e48b52..d080233399d 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -273,7 +273,9 @@ pub struct ClientBuilder { decryption_trust_requirement: TrustRequirement, request_config: Option, - use_event_cache_persistent_storage: Option, + /// Whether to enable use of the event cache store, for reloading events + /// when building timelines et al. + use_event_cache_persistent_storage: bool, } #[matrix_sdk_ffi_macros::export] @@ -304,7 +306,7 @@ impl ClientBuilder { room_key_recipient_strategy: Default::default(), decryption_trust_requirement: TrustRequirement::Untrusted, request_config: Default::default(), - use_event_cache_persistent_storage: None, + use_event_cache_persistent_storage: false, }) } @@ -313,13 +315,15 @@ impl ClientBuilder { /// This is a temporary feature flag, for testing the event cache's /// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280. /// - /// To enable the persistent storage: call this with `true`. + /// This is disabled by default. When disabled, a one-time cleanup is + /// performed when creating the client, and it will clear all the events + /// previously stored in the event cache. /// - /// To explicitly disable a previously-enabled persistent storage, call this - /// with `false` to clear the previous content in storage. + /// When enabled, it will attempt to store events in the event cache as + /// they're received, and reuse them when reconstructing timelines. pub fn use_event_cache_persistent_storage(self: Arc, value: bool) -> Arc { let mut builder = unwrap_or_clone_arc(self); - builder.use_event_cache_persistent_storage = Some(value); + builder.use_event_cache_persistent_storage = value; Arc::new(builder) } @@ -645,19 +649,17 @@ impl ClientBuilder { let sdk_client = inner_builder.build().await?; - if let Some(val) = builder.use_event_cache_persistent_storage { - if val { - // Enable the persistent storage \o/ - sdk_client.event_cache().enable_storage()?; - } else { - // Get rid of the previous events, if any. - let store = sdk_client - .event_cache_store() - .lock() - .await - .map_err(EventCacheError::LockingStorage)?; - store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?; - } + if builder.use_event_cache_persistent_storage { + // Enable the persistent storage \o/ + sdk_client.event_cache().enable_storage()?; + } else { + // Get rid of all the previous events, if any. + let store = sdk_client + .event_cache_store() + .lock() + .await + .map_err(EventCacheError::LockingStorage)?; + store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?; } Ok(Arc::new( From a04f9187f8a6001f86ec2f51fd92238797c81f99 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 1 Dec 2024 21:58:41 +0000 Subject: [PATCH 751/979] refactor(ui): store UTD info within `PendingUtdReport` ... making it easier to report late decryptions. --- .../src/timeline/controller/mod.rs | 23 ++++++------- .../src/unable_to_decrypt_hook.rs | 34 ++++++++++++------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 5d82572470c..6fb796a0447 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1052,17 +1052,16 @@ impl TimelineController

{ async move { let event_item = item.as_event()?; - let (session_id, utd_cause) = - match event_item.content().as_unable_to_decrypt()? { - EncryptedMessage::MegolmV1AesSha2 { session_id, cause, .. } - if should_retry(session_id) => - { - (session_id, cause) - } - EncryptedMessage::MegolmV1AesSha2 { .. } - | EncryptedMessage::OlmV1Curve25519AesSha2 { .. } - | EncryptedMessage::Unknown => return None, - }; + let session_id = match event_item.content().as_unable_to_decrypt()? { + EncryptedMessage::MegolmV1AesSha2 { session_id, .. } + if should_retry(session_id) => + { + session_id + } + EncryptedMessage::MegolmV1AesSha2 { .. } + | EncryptedMessage::OlmV1Curve25519AesSha2 { .. } + | EncryptedMessage::Unknown => return None, + }; tracing::Span::current().record("session_id", session_id); @@ -1091,7 +1090,7 @@ impl TimelineController

{ } else { // Notify observers that we managed to eventually decrypt an event. if let Some(hook) = unable_to_decrypt_hook { - hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await; + hook.on_late_decrypt(&remote_event.event_id).await; } Some(event) diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index 6279a6d7ad7..f9cf2a201f7 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -73,6 +73,9 @@ struct PendingUtdReport { /// The task that will report this UTD to the parent hook. report_task: JoinHandle<()>, + + /// The UnableToDecryptInfo structure for this UTD event. + utd_info: UnableToDecryptInfo, } /// A manager over an existing [`UnableToDecryptHook`] that deduplicates UTDs @@ -227,6 +230,7 @@ impl UtdHookManager { let reported_utds = self.reported_utds.clone(); let parent = self.parent.clone(); let client = self.client.clone(); + let owned_event_id = event_id.to_owned(); // Spawn a task that will wait for the given delay, and maybe call the parent // hook then. @@ -243,15 +247,22 @@ impl UtdHookManager { // Remove the task from the outstanding set. But if it's already been removed, // it's been decrypted since the task was added! - if pending_delayed.lock().unwrap().remove(&info.event_id).is_some() { - Self::report_utd(info, &parent, &client, &mut reported_utds_lock).await; + let pending_report = pending_delayed.lock().unwrap().remove(&owned_event_id); + if let Some(pending_report) = pending_report { + Self::report_utd( + pending_report.utd_info, + &parent, + &client, + &mut reported_utds_lock, + ) + .await; } }); // Add the task to the set of pending tasks. self.pending_delayed.lock().unwrap().insert( event_id.to_owned(), - PendingUtdReport { marked_utd_at: Instant::now(), report_task: handle }, + PendingUtdReport { marked_utd_at: Instant::now(), report_task: handle, utd_info: info }, ); } @@ -260,7 +271,7 @@ impl UtdHookManager { /// /// Note: if this is called for an event that was never marked as a UTD /// before, it has no effect. - pub(crate) async fn on_late_decrypt(&self, event_id: &EventId, cause: UtdCause) { + pub(crate) async fn on_late_decrypt(&self, event_id: &EventId) { // Hold the lock on `reported_utds` throughout, to avoid races with other // threads. let mut reported_utds_lock = self.reported_utds.lock().await; @@ -275,12 +286,9 @@ impl UtdHookManager { // We can also cancel the reporting task. pending_utd_report.report_task.abort(); - // Now we can report the late decryption. - let info = UnableToDecryptInfo { - event_id: event_id.to_owned(), - time_to_decrypt: Some(pending_utd_report.marked_utd_at.elapsed()), - cause, - }; + // Update the UTD Info struct with new data, then report it + let mut info = pending_utd_report.utd_info; + info.time_to_decrypt = Some(pending_utd_report.marked_utd_at.elapsed()); Self::report_utd(info, &self.parent, &self.client, &mut reported_utds_lock).await; } @@ -476,7 +484,7 @@ mod tests { // And I call the `on_late_decrypt` method before the event had been marked as // utd, - wrapper.on_late_decrypt(event_id!("$1"), UtdCause::Unknown).await; + wrapper.on_late_decrypt(event_id!("$1")).await; // Then nothing is registered in the parent hook. assert!(hook.utds.lock().unwrap().is_empty()); @@ -502,7 +510,7 @@ mod tests { } // And when I call the `on_late_decrypt` method, - wrapper.on_late_decrypt(event_id!("$1"), UtdCause::Unknown).await; + wrapper.on_late_decrypt(event_id!("$1")).await; // Then the event is not reported again as a late-decryption. { @@ -573,7 +581,7 @@ mod tests { // If I wait for 1 second, and mark the event as late-decrypted, sleep(Duration::from_secs(1)).await; - wrapper.on_late_decrypt(event_id!("$1"), UtdCause::Unknown).await; + wrapper.on_late_decrypt(event_id!("$1")).await; // Then it's being immediately reported as a late-decryption UTD. { From c501a39ad48be9146aee76c71fd24ec201e7d7fb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 1 Dec 2024 20:41:02 +0000 Subject: [PATCH 752/979] refactor(sdk): Add `Encryption::device_creation_timestamp` ... so that we can use it in more places --- crates/matrix-sdk/src/encryption/mod.rs | 13 +++++++++++-- crates/matrix-sdk/src/room/mod.rs | 19 ++++++------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 9a4130e4ffb..78376cf99f8 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -54,7 +54,7 @@ use ruma::{ direct::DirectUserIdentifier, room::{MediaSource, ThumbnailInfo}, }, - DeviceId, OwnedDeviceId, OwnedUserId, TransactionId, UserId, + DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, TransactionId, UserId, }; use serde::Deserialize; use tokio::sync::{Mutex, RwLockReadGuard}; @@ -718,6 +718,15 @@ impl Encryption { self.client.olm_machine().await.as_ref().map(|o| o.identity_keys().curve25519) } + /// Get the current device creation timestamp. + pub async fn device_creation_timestamp(&self) -> MilliSecondsSinceUnixEpoch { + match self.get_own_device().await { + Ok(Some(device)) => device.first_time_seen_ts(), + // Should not happen, there should always be an own device + _ => MilliSecondsSinceUnixEpoch::now(), + } + } + #[cfg(feature = "experimental-oidc")] pub(crate) async fn import_secrets_bundle( &self, @@ -1153,7 +1162,7 @@ impl Encryption { /// # let client = Client::new(homeserver).await?; /// # let user_id = unimplemented!(); /// let encryption = client.encryption(); - /// + /// /// if let Some(handle) = encryption.reset_cross_signing().await? { /// match handle.auth_type() { /// CrossSigningResetAuthType::Uiaa(uiaa) => { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index a5908d29897..438a28c4091 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -48,6 +48,11 @@ use matrix_sdk_base::{ }; use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timeout::timeout}; use mime::Mime; +#[cfg(feature = "e2e-encryption")] +use ruma::events::{ + room::encrypted::OriginalSyncRoomEncryptedEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, + SyncMessageLikeEvent, +}; use ruma::{ api::client::{ config::{set_global_account_data, set_room_account_data}, @@ -108,14 +113,6 @@ use ruma::{ EventId, Int, MatrixToUri, MatrixUri, MxcUri, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId, }; -#[cfg(feature = "e2e-encryption")] -use ruma::{ - events::{ - room::encrypted::OriginalSyncRoomEncryptedEvent, AnySyncMessageLikeEvent, - AnySyncTimelineEvent, SyncMessageLikeEvent, - }, - MilliSecondsSinceUnixEpoch, -}; use serde::de::DeserializeOwned; use thiserror::Error; use tokio::sync::broadcast; @@ -621,11 +618,7 @@ impl Room { pub async fn crypto_context_info(&self) -> CryptoContextInfo { let encryption = self.client.encryption(); CryptoContextInfo { - device_creation_ts: match encryption.get_own_device().await { - Ok(Some(device)) => device.first_time_seen_ts(), - // Should not happen, there will always be an own device - _ => MilliSecondsSinceUnixEpoch::now(), - }, + device_creation_ts: encryption.device_creation_timestamp().await, is_backup_configured: encryption.backups().state() == BackupState::Enabled, } } From e8b3949db3023e6c19d8abb38ed512783e5d517f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 1 Dec 2024 21:18:24 +0000 Subject: [PATCH 753/979] feat(ui): Add `UnableToDecryptInfo::event_local_age_millis` --- .../src/timeline/event_handler.rs | 2 +- .../src/unable_to_decrypt_hook.rs | 86 ++++++++++++++----- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 1eba4147626..f8eecea4c3e 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -443,7 +443,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // timeline. if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() { if let Some(event_id) = &self.ctx.flow.event_id() { - hook.on_utd(event_id, utd_cause).await; + hook.on_utd(event_id, utd_cause, self.ctx.timestamp).await; } } } diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index f9cf2a201f7..6e89f00ce02 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -27,7 +27,7 @@ use std::{ use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; use matrix_sdk::{crypto::types::events::UtdCause, Client}; use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError}; -use ruma::{EventId, OwnedEventId}; +use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId}; use tokio::{ spawn, sync::{Mutex as AsyncMutex, MutexGuard}, @@ -63,6 +63,11 @@ pub struct UnableToDecryptInfo { /// What we know about what caused this UTD. E.g. was this event sent when /// we were not a member of this room? pub cause: UtdCause, + + /// The difference between the event creation time (`origin_server_ts`) and + /// the time our device was created. If negative, this event was sent + /// *before* our device was created. + pub event_local_age_millis: i64, } /// Data about a UTD event which we are waiting to report to the parent hook. @@ -200,7 +205,19 @@ impl UtdHookManager { /// The function to call whenever a UTD is seen for the first time. /// /// Pipe in any information that needs to be included in the final report. - pub(crate) async fn on_utd(&self, event_id: &EventId, cause: UtdCause) { + /// + /// # Arguments + /// * `event_id` - The ID of the event that could not be decrypted. + /// * `cause` - Our best guess at the reason why the event can't be + /// decrypted. + /// * `event_timestamp` - The event's `origin_server_ts` field (or creation + /// time for local echo). + pub(crate) async fn on_utd( + &self, + event_id: &EventId, + cause: UtdCause, + event_timestamp: MilliSecondsSinceUnixEpoch, + ) { // Hold the lock on `reported_utds` throughout, to avoid races with other // threads. let mut reported_utds_lock = self.reported_utds.lock().await; @@ -216,8 +233,16 @@ impl UtdHookManager { return; } - let info = - UnableToDecryptInfo { event_id: event_id.to_owned(), time_to_decrypt: None, cause }; + let event_local_age_millis = i64::from(event_timestamp.get()).saturating_sub_unsigned( + self.client.encryption().device_creation_timestamp().await.get().into(), + ); + + let info = UnableToDecryptInfo { + event_id: event_id.to_owned(), + time_to_decrypt: None, + cause, + event_local_age_millis, + }; let Some(max_delay) = self.max_delay else { // No delay: immediately report the event to the parent hook. @@ -341,7 +366,7 @@ impl Drop for UtdHookManager { #[cfg(test)] mod tests { - use matrix_sdk::test_utils::no_retry_test_client; + use matrix_sdk::test_utils::{logged_in_client, no_retry_test_client}; use matrix_sdk_test::async_test; use ruma::event_id; @@ -364,15 +389,16 @@ mod tests { let hook = Arc::new(Dummy::default()); // And I wrap with the UtdHookManager, - let wrapper = UtdHookManager::new(hook.clone(), no_retry_test_client(None).await); + let wrapper = UtdHookManager::new(hook.clone(), logged_in_client(None).await); // And I call the `on_utd` method multiple times, sometimes on the same event, - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; - wrapper.on_utd(event_id!("$2"), UtdCause::Unknown).await; - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; - wrapper.on_utd(event_id!("$2"), UtdCause::Unknown).await; - wrapper.on_utd(event_id!("$3"), UtdCause::Unknown).await; + let event_timestamp = MilliSecondsSinceUnixEpoch::now(); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp).await; + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp).await; + wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp).await; + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp).await; + wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp).await; + wrapper.on_utd(event_id!("$3"), UtdCause::Unknown, event_timestamp).await; // Then the event ids have been deduplicated, { @@ -386,6 +412,12 @@ mod tests { assert!(utds[0].time_to_decrypt.is_none()); assert!(utds[1].time_to_decrypt.is_none()); assert!(utds[2].time_to_decrypt.is_none()); + + // event_local_age_millis should be a small positive number, because the + // timestamp we used was after we created the device + let utd_local_age = utds[0].event_local_age_millis; + assert!(utd_local_age >= 0); + assert!(utd_local_age <= 1000); } } @@ -401,8 +433,12 @@ mod tests { let wrapper = UtdHookManager::new(hook.clone(), client.clone()); // I call it a couple of times with different events - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; - wrapper.on_utd(event_id!("$2"), UtdCause::Unknown).await; + wrapper + .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .await; + wrapper + .on_utd(event_id!("$2"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .await; // Sanity-check the reported event IDs { @@ -422,8 +458,12 @@ mod tests { wrapper.reload_from_store().await.unwrap(); // Call it with more events, some of which match the previous instance - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; - wrapper.on_utd(event_id!("$3"), UtdCause::Unknown).await; + wrapper + .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .await; + wrapper + .on_utd(event_id!("$3"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .await; // Only the *new* ones should be reported let utds = hook.utds.lock().unwrap(); @@ -447,7 +487,9 @@ mod tests { .with_max_delay(Duration::from_secs(2)); // a UTD event - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; + wrapper + .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .await; // The event ID should not yet have been reported. { @@ -463,7 +505,9 @@ mod tests { wrapper.reload_from_store().await.unwrap(); // Call the new hook with the same event - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; + wrapper + .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .await; // And it should be reported. sleep(Duration::from_millis(2500)).await; @@ -499,7 +543,7 @@ mod tests { let wrapper = UtdHookManager::new(hook.clone(), no_retry_test_client(None).await); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()).await; // Then the UTD has been notified, but not as late-decrypted event. { @@ -535,7 +579,7 @@ mod tests { .with_max_delay(Duration::from_secs(2)); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()).await; // Then the UTD is not being reported immediately. assert!(hook.utds.lock().unwrap().is_empty()); @@ -572,7 +616,7 @@ mod tests { .with_max_delay(Duration::from_secs(2)); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown).await; + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()).await; // Then the UTD has not been notified quite yet. assert!(hook.utds.lock().unwrap().is_empty()); From 1e72131e7fe2abbd7dc55cb2fd04c64ec47a579c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 1 Dec 2024 22:19:52 +0000 Subject: [PATCH 754/979] feat(ui) Add `UnableToDecryptInfo::user_trusts_own_identity` --- crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index 6e89f00ce02..ff40371b870 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -68,6 +68,9 @@ pub struct UnableToDecryptInfo { /// the time our device was created. If negative, this event was sent /// *before* our device was created. pub event_local_age_millis: i64, + + /// Whether the user had verified their own identity at the point they received the UTD event. + pub user_trusts_own_identity: bool, } /// Data about a UTD event which we are waiting to report to the parent hook. @@ -236,12 +239,23 @@ impl UtdHookManager { let event_local_age_millis = i64::from(event_timestamp.get()).saturating_sub_unsigned( self.client.encryption().device_creation_timestamp().await.get().into(), ); + let user_trusts_own_identity = if let Some(own_user_id) = self.client.user_id() { + if let Ok(Some(own_id)) = self.client.encryption().get_user_identity(own_user_id).await + { + own_id.is_verified() + } else { + false + } + } else { + false + }; let info = UnableToDecryptInfo { event_id: event_id.to_owned(), time_to_decrypt: None, cause, event_local_age_millis, + user_trusts_own_identity, }; let Some(max_delay) = self.max_delay else { From 1d72d2774f918f5c5dba0fa6be3b6e827e15261b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sun, 1 Dec 2024 22:38:27 +0000 Subject: [PATCH 755/979] feat(ui): Add more properties to `UnableToDecryptInfo` --- .../src/timeline/event_handler.rs | 3 +- .../src/unable_to_decrypt_hook.rs | 115 +++++++++++++++--- 2 files changed, 98 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index f8eecea4c3e..a7b4a2ef934 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -443,7 +443,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { // timeline. if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() { if let Some(event_id) = &self.ctx.flow.event_id() { - hook.on_utd(event_id, utd_cause, self.ctx.timestamp).await; + hook.on_utd(event_id, utd_cause, self.ctx.timestamp, &self.ctx.sender) + .await; } } } diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index ff40371b870..7207b8c2655 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -27,7 +27,7 @@ use std::{ use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; use matrix_sdk::{crypto::types::events::UtdCause, Client}; use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError}; -use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId}; +use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedServerName, UserId}; use tokio::{ spawn, sync::{Mutex as AsyncMutex, MutexGuard}, @@ -69,8 +69,16 @@ pub struct UnableToDecryptInfo { /// *before* our device was created. pub event_local_age_millis: i64, - /// Whether the user had verified their own identity at the point they received the UTD event. + /// Whether the user had verified their own identity at the point they + /// received the UTD event. pub user_trusts_own_identity: bool, + + /// The homeserver of the user that sent the undecryptable event. + pub sender_homeserver: OwnedServerName, + + /// Our local user's own homeserver, or `None` if the client is not logged + /// in. + pub own_homeserver: Option, } /// Data about a UTD event which we are waiting to report to the parent hook. @@ -215,11 +223,14 @@ impl UtdHookManager { /// decrypted. /// * `event_timestamp` - The event's `origin_server_ts` field (or creation /// time for local echo). + /// * `sender_user_id` - The Matrix user ID of the user that sent the + /// undecryptable message. pub(crate) async fn on_utd( &self, event_id: &EventId, cause: UtdCause, event_timestamp: MilliSecondsSinceUnixEpoch, + sender_user_id: &UserId, ) { // Hold the lock on `reported_utds` throughout, to avoid races with other // threads. @@ -239,7 +250,9 @@ impl UtdHookManager { let event_local_age_millis = i64::from(event_timestamp.get()).saturating_sub_unsigned( self.client.encryption().device_creation_timestamp().await.get().into(), ); - let user_trusts_own_identity = if let Some(own_user_id) = self.client.user_id() { + + let own_user_id = self.client.user_id(); + let user_trusts_own_identity = if let Some(own_user_id) = own_user_id { if let Ok(Some(own_id)) = self.client.encryption().get_user_identity(own_user_id).await { own_id.is_verified() @@ -250,12 +263,17 @@ impl UtdHookManager { false }; + let own_homeserver = own_user_id.map(|id| id.server_name().to_owned()); + let sender_homeserver = sender_user_id.server_name().to_owned(); + let info = UnableToDecryptInfo { event_id: event_id.to_owned(), time_to_decrypt: None, cause, event_local_age_millis, user_trusts_own_identity, + own_homeserver, + sender_homeserver, }; let Some(max_delay) = self.max_delay else { @@ -382,7 +400,7 @@ impl Drop for UtdHookManager { mod tests { use matrix_sdk::test_utils::{logged_in_client, no_retry_test_client}; use matrix_sdk_test::async_test; - use ruma::event_id; + use ruma::{event_id, server_name, user_id}; use super::*; @@ -407,12 +425,14 @@ mod tests { // And I call the `on_utd` method multiple times, sometimes on the same event, let event_timestamp = MilliSecondsSinceUnixEpoch::now(); - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp).await; - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp).await; - wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp).await; - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp).await; - wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp).await; - wrapper.on_utd(event_id!("$3"), UtdCause::Unknown, event_timestamp).await; + let sender_user = user_id!("@example2:localhost"); + let federated_user = user_id!("@example2:example.com"); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp, sender_user).await; + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp, sender_user).await; + wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp, federated_user).await; + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp, sender_user).await; + wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp, federated_user).await; + wrapper.on_utd(event_id!("$3"), UtdCause::Unknown, event_timestamp, sender_user).await; // Then the event ids have been deduplicated, { @@ -432,6 +452,12 @@ mod tests { let utd_local_age = utds[0].event_local_age_millis; assert!(utd_local_age >= 0); assert!(utd_local_age <= 1000); + + assert_eq!(utds[0].sender_homeserver, server_name!("localhost")); + assert_eq!(utds[0].own_homeserver, Some(server_name!("localhost").to_owned())); + + assert_eq!(utds[1].sender_homeserver, server_name!("example.com")); + assert_eq!(utds[1].own_homeserver, Some(server_name!("localhost").to_owned())); } } @@ -448,10 +474,20 @@ mod tests { // I call it a couple of times with different events wrapper - .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .on_utd( + event_id!("$1"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) .await; wrapper - .on_utd(event_id!("$2"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .on_utd( + event_id!("$2"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) .await; // Sanity-check the reported event IDs @@ -473,10 +509,20 @@ mod tests { // Call it with more events, some of which match the previous instance wrapper - .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .on_utd( + event_id!("$1"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) .await; wrapper - .on_utd(event_id!("$3"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .on_utd( + event_id!("$3"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) .await; // Only the *new* ones should be reported @@ -502,7 +548,12 @@ mod tests { // a UTD event wrapper - .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .on_utd( + event_id!("$1"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) .await; // The event ID should not yet have been reported. @@ -520,7 +571,12 @@ mod tests { // Call the new hook with the same event wrapper - .on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()) + .on_utd( + event_id!("$1"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) .await; // And it should be reported. @@ -557,7 +613,14 @@ mod tests { let wrapper = UtdHookManager::new(hook.clone(), no_retry_test_client(None).await); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()).await; + wrapper + .on_utd( + event_id!("$1"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) + .await; // Then the UTD has been notified, but not as late-decrypted event. { @@ -593,7 +656,14 @@ mod tests { .with_max_delay(Duration::from_secs(2)); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()).await; + wrapper + .on_utd( + event_id!("$1"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) + .await; // Then the UTD is not being reported immediately. assert!(hook.utds.lock().unwrap().is_empty()); @@ -630,7 +700,14 @@ mod tests { .with_max_delay(Duration::from_secs(2)); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, MilliSecondsSinceUnixEpoch::now()).await; + wrapper + .on_utd( + event_id!("$1"), + UtdCause::Unknown, + MilliSecondsSinceUnixEpoch::now(), + user_id!("@a:b"), + ) + .await; // Then the UTD has not been notified quite yet. assert!(hook.utds.lock().unwrap().is_empty()); From 935e4df9273e2add9e1a5c9c6e7d0dbc8fa2832d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 9 Dec 2024 14:15:00 +0200 Subject: [PATCH 756/979] feat(ui): make the timeline date separators configurable; have them appear either when the day changes or when the month changes. --- bindings/matrix-sdk-ffi/src/room.rs | 5 +- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 16 +++ crates/matrix-sdk-ui/src/timeline/builder.rs | 9 +- .../src/timeline/controller/mod.rs | 25 +++- .../src/timeline/controller/state.rs | 9 +- .../src/timeline/day_dividers.rs | 128 +++++++++++++----- crates/matrix-sdk-ui/src/timeline/mod.rs | 7 + crates/matrix-sdk-ui/src/timeline/util.rs | 6 + 8 files changed, 162 insertions(+), 43 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 9f0e3014909..fd0f6aef3fa 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -39,7 +39,7 @@ use crate::{ room_info::RoomInfo, room_member::RoomMember, ruma::{ImageInfo, Mentions, NotifyType}, - timeline::{FocusEventError, ReceiptType, SendHandle, Timeline}, + timeline::{DateDividerMode, FocusEventError, ReceiptType, SendHandle, Timeline}, utils::u64_to_uint, TaskHandle, }; @@ -278,6 +278,7 @@ impl Room { &self, internal_id_prefix: Option, allowed_message_types: Vec, + date_divider_mode: DateDividerMode, ) -> Result, ClientError> { let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner); @@ -285,6 +286,8 @@ impl Room { builder = builder.with_internal_id_prefix(internal_id_prefix); } + builder = builder.with_date_divider_mode(date_divider_mode.into()); + builder = builder.event_filter(move |event, room_version_id| { default_event_filter(event, room_version_id) && match event { diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index c9075a928c1..08553bbc09a 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1358,3 +1358,19 @@ impl LazyTimelineItemProvider { self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle))) } } + +/// Changes how dividers get inserted, either in between each day or in between each month +#[derive(Debug, Clone, uniffi::Enum)] +pub enum DateDividerMode { + Daily, + Monthly, +} + +impl From for matrix_sdk_ui::timeline::DateDividerMode { + fn from(value: DateDividerMode) -> Self { + match value { + DateDividerMode::Daily => Self::Daily, + DateDividerMode::Monthly => Self::Monthly, + } + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 8e131e21f6e..b3e7ec8cd01 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -28,7 +28,7 @@ use tracing::{info, info_span, trace, warn, Instrument, Span}; use super::{ controller::{TimelineController, TimelineSettings}, to_device::{handle_forwarded_room_key_event, handle_room_key_event}, - Error, Timeline, TimelineDropHandle, TimelineFocus, + DateDividerMode, Error, Timeline, TimelineDropHandle, TimelineFocus, }; use crate::{ timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin}, @@ -89,6 +89,13 @@ impl TimelineBuilder { self } + /// Chose when to insert the date separators, either in between each day + /// or each month. + pub fn with_date_divider_mode(mut self, mode: DateDividerMode) -> Self { + self.settings.date_divider_mode = mode; + self + } + /// Enable tracking of the fully-read marker and the read receipts on the /// timeline. pub fn track_read_marker_and_receipts(mut self) -> Self { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 6fb796a0447..96231a094b4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -69,9 +69,9 @@ use super::{ item::TimelineUniqueId, traits::{Decryptor, RoomDataProvider}, util::{rfind_event_by_id, rfind_event_item, RelativePosition}, - Error, EventSendState, EventTimelineItem, InReplyToDetails, Message, PaginationError, Profile, - ReactionInfo, RepliedToEvent, TimelineDetails, TimelineEventItemId, TimelineFocus, - TimelineItem, TimelineItemContent, TimelineItemKind, + DateDividerMode, Error, EventSendState, EventTimelineItem, InReplyToDetails, Message, + PaginationError, Profile, ReactionInfo, RepliedToEvent, TimelineDetails, TimelineEventItemId, + TimelineFocus, TimelineItem, TimelineItemContent, TimelineItemKind, }; use crate::{ timeline::{ @@ -136,6 +136,8 @@ pub(super) struct TimelineSettings { pub(super) event_filter: Arc, /// Are unparsable events added as timeline items of their own kind? pub(super) add_failed_to_parse: bool, + /// Should the timeline items be grouped by day or month? + pub(super) date_divider_mode: DateDividerMode, } #[cfg(not(tarpaulin_include))] @@ -154,6 +156,7 @@ impl Default for TimelineSettings { track_read_receipts: false, event_filter: Arc::new(default_event_filter), add_failed_to_parse: true, + date_divider_mode: DateDividerMode::Daily, } } } @@ -742,9 +745,19 @@ impl TimelineController

{ // Only add new items if the timeline is live. let should_add_new_items = self.is_live().await; + let date_divider_mode = self.settings.date_divider_mode.clone(); + let mut state = self.state.write().await; state - .handle_local_event(sender, profile, should_add_new_items, txn_id, send_handle, content) + .handle_local_event( + sender, + profile, + should_add_new_items, + date_divider_mode, + txn_id, + send_handle, + content, + ) .await; } @@ -784,7 +797,7 @@ impl TimelineController

{ txn.items.remove(idx); // Adjust the day dividers, if needs be. - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(self.settings.date_divider_mode.clone()); adjuster.run(&mut txn.items, &mut txn.meta); } @@ -883,7 +896,7 @@ impl TimelineController

{ // A read marker or a day divider may have been inserted before the local echo. // Ensure both are up to date. - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(self.settings.date_divider_mode.clone()); adjuster.run(&mut txn.items, &mut txn.meta); txn.meta.update_read_marker(&mut txn.items); diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 878573af3ea..aaf502d1d79 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -48,7 +48,7 @@ use super::{ AllRemoteEvents, ObservableItems, ObservableItemsTransaction, ObservableItemsTransactionEntry, }, - HandleManyEventsResult, TimelineFocusKind, TimelineSettings, + DateDividerMode, HandleManyEventsResult, TimelineFocusKind, TimelineSettings, }; use crate::{ events::SyncTimelineEventWithoutContent, @@ -198,6 +198,7 @@ impl TimelineState { own_user_id: OwnedUserId, own_profile: Option, should_add_new_items: bool, + date_divider_mode: DateDividerMode, txn_id: OwnedTransactionId, send_handle: Option, content: TimelineEventKind, @@ -216,7 +217,7 @@ impl TimelineState { let mut txn = self.transaction(); - let mut day_divider_adjuster = DayDividerAdjuster::default(); + let mut day_divider_adjuster = DayDividerAdjuster::new(date_divider_mode); TimelineEventHandler::new(&mut txn, ctx) .handle_event(&mut day_divider_adjuster, content) @@ -239,7 +240,7 @@ impl TimelineState { { let mut txn = self.transaction(); - let mut day_divider_adjuster = DayDividerAdjuster::default(); + let mut day_divider_adjuster = DayDividerAdjuster::new(settings.date_divider_mode.clone()); // Loop through all the indices, in order so we don't decrypt edits // before the event being edited, if both were UTD. Keep track of @@ -381,7 +382,7 @@ impl TimelineStateTransaction<'_> { let position = position.into(); - let mut day_divider_adjuster = DayDividerAdjuster::default(); + let mut day_divider_adjuster = DayDividerAdjuster::new(settings.date_divider_mode.clone()); // Implementation note: when `position` is `TimelineEnd::Front`, events are in // the reverse topological order. Prepending them one by one in the order they diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index ba9cd9175b3..3b78c6ae858 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -23,7 +23,7 @@ use tracing::{error, event_enabled, instrument, trace, warn, Level}; use super::{ controller::{ObservableItemsTransaction, TimelineMetadata}, util::timestamp_to_date, - TimelineItem, TimelineItemKind, VirtualTimelineItem, + DateDividerMode, TimelineItem, TimelineItemKind, VirtualTimelineItem, }; /// Algorithm ensuring that day dividers are adjusted correctly, according to @@ -36,6 +36,8 @@ pub(super) struct DayDividerAdjuster { /// A boolean indicating whether the struct has been used and thus must be /// mark unused manually by calling [`Self::run`]. consumed: bool, + + mode: DateDividerMode, } impl Drop for DayDividerAdjuster { @@ -47,17 +49,6 @@ impl Drop for DayDividerAdjuster { } } -impl Default for DayDividerAdjuster { - fn default() -> Self { - Self { - ops: Default::default(), - // The adjuster starts as consumed, and it will be marked no consumed iff it's used - // with `mark_used`. - consumed: true, - } - } -} - /// A descriptor for a previous item. struct PrevItemDesc<'a> { /// The index of the item in the `self.items` array. @@ -71,6 +62,16 @@ struct PrevItemDesc<'a> { } impl DayDividerAdjuster { + pub fn new(mode: DateDividerMode) -> Self { + Self { + ops: Default::default(), + // The adjuster starts as consumed, and it will be marked no consumed iff it's used + // with `mark_used`. + consumed: true, + mode: mode, + } + } + /// Marks this [`DayDividerAdjuster`] as used, which means it'll require a /// call to [`DayDividerAdjuster::run`] before getting dropped. pub fn mark_used(&mut self) { @@ -199,7 +200,7 @@ impl DayDividerAdjuster { match prev_item.kind() { TimelineItemKind::Event(event) => { // This day divider is preceded by an event. - if is_same_date_as(event.timestamp(), ts) { + if self.is_same_date_as(event.timestamp(), ts) { // The event has the same date as the day divider: remove the current day // divider. trace!("removing day divider following event with same timestamp @ {i}"); @@ -245,7 +246,7 @@ impl DayDividerAdjuster { // insert a day divider. let prev_ts = prev_event.timestamp(); - if !is_same_date_as(prev_ts, ts) { + if !self.is_same_date_as(prev_ts, ts) { trace!("inserting day divider @ {} between two events with different dates", i); self.ops.push(DayDividerOperation::Insert(i, ts)); } @@ -415,7 +416,7 @@ impl DayDividerAdjuster { // We have the same date as the previous event we've seen. if let Some(prev_ts) = prev_event_ts { - if !is_same_date_as(prev_ts, ts) { + if !self.is_same_date_as(prev_ts, ts) { report.errors.push( DayDividerInsertError::MissingDayDividerBetweenEvents { at: i }, ); @@ -424,7 +425,7 @@ impl DayDividerAdjuster { // There is a day divider before us, and it's the same date as our timestamp. if let Some(prev_ts) = prev_day_divider_ts { - if !is_same_date_as(prev_ts, ts) { + if !self.is_same_date_as(prev_ts, ts) { report.errors.push( DayDividerInsertError::InconsistentDateAfterPreviousDayDivider { at: i, @@ -443,7 +444,7 @@ impl DayDividerAdjuster { { // The previous day divider is for a different date. if let Some(prev_ts) = prev_day_divider_ts { - if is_same_date_as(prev_ts, *ts) { + if self.is_same_date_as(prev_ts, *ts) { report .errors .push(DayDividerInsertError::DuplicateDayDivider { at: i }); @@ -472,6 +473,20 @@ impl DayDividerAdjuster { Some(report) } } + + /// Returns whether the two dates for the given timestamps are the same or not. + fn is_same_date_as( + &self, + lhs: MilliSecondsSinceUnixEpoch, + rhs: MilliSecondsSinceUnixEpoch, + ) -> bool { + match self.mode { + DateDividerMode::Daily => timestamp_to_date(lhs) == timestamp_to_date(rhs), + DateDividerMode::Monthly => { + timestamp_to_date(lhs).is_same_month_as(timestamp_to_date(rhs)) + } + } + } } #[derive(Debug)] @@ -491,12 +506,6 @@ impl DayDividerOperation { } } -/// Returns whether the two dates for the given timestamps are the same or not. -#[inline] -fn is_same_date_as(lhs: MilliSecondsSinceUnixEpoch, rhs: MilliSecondsSinceUnixEpoch) -> bool { - timestamp_to_date(lhs) == timestamp_to_date(rhs) -} - /// A report returned by [`DayDividerAdjuster::check_invariants`]. struct DayDividerInvariantsReport<'a, 'o> { /// Initial state before inserting the items. @@ -607,7 +616,7 @@ mod tests { controller::TimelineMetadata, event_item::{EventTimelineItemKind, RemoteEventTimelineItem}, util::timestamp_to_date, - EventTimelineItem, TimelineItemContent, VirtualTimelineItem, + DateDividerMode, EventTimelineItem, TimelineItemContent, VirtualTimelineItem, }; fn event_with_ts(timestamp: MilliSecondsSinceUnixEpoch) -> EventTimelineItem { @@ -661,7 +670,7 @@ mod tests { ); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -701,7 +710,7 @@ mod tests { txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); txn.push_back(meta.new_timeline_item(event), None); - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -734,7 +743,7 @@ mod tests { txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -766,7 +775,7 @@ mod tests { txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -792,7 +801,7 @@ mod tests { txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -818,7 +827,7 @@ mod tests { txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -840,7 +849,7 @@ mod tests { txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); - let mut adjuster = DayDividerAdjuster::default(); + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -852,4 +861,61 @@ mod tests { assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().is_none()); } + + #[test] + fn test_dayly_divider_mode() { + let mut items = ObservableVector::new(); + let mut txn = items.transaction(); + + let mut meta = test_metadata(); + + txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(0))))); + txn.push_back( + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(100000000)))), + ); + txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now()))); + + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + adjuster.run(&mut txn, &mut meta); + + txn.commit(); + + let mut iter = items.iter(); + + assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_remote_event()); + assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_remote_event()); + assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_remote_event()); + assert!(iter.next().is_none()); + } + + #[test] + fn test_monthly_divider_mode() { + let mut items = ObservableVector::new(); + let mut txn = items.transaction(); + + let mut meta = test_metadata(); + + txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(0))))); + txn.push_back( + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(100000000)))), + ); + txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now()))); + + let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Monthly); + adjuster.run(&mut txn, &mut meta); + + txn.commit(); + + let mut iter = items.iter(); + + assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_remote_event()); + assert!(iter.next().unwrap().is_remote_event()); + assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_remote_event()); + assert!(iter.next().is_none()); + } } diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index da5dcbfef78..2461251d4e2 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -177,6 +177,13 @@ impl TimelineFocus { } } +/// Changes how dividers get inserted, either in between each day or in between each month +#[derive(Debug, Clone)] +pub enum DateDividerMode { + Daily, + Monthly, +} + impl Timeline { /// Create a new [`TimelineBuilder`] for the given room. pub fn builder(room: &Room) -> TimelineBuilder { diff --git a/crates/matrix-sdk-ui/src/timeline/util.rs b/crates/matrix-sdk-ui/src/timeline/util.rs index 9ad3be2b851..2bb9c449456 100644 --- a/crates/matrix-sdk-ui/src/timeline/util.rs +++ b/crates/matrix-sdk-ui/src/timeline/util.rs @@ -132,6 +132,12 @@ pub(super) struct Date { day: u32, } +impl Date { + pub fn is_same_month_as(&self, date: Date) -> bool { + self.year == date.year && self.month == date.month + } +} + /// Converts a timestamp since Unix Epoch to a year, month and day. pub(super) fn timestamp_to_date(ts: MilliSecondsSinceUnixEpoch) -> Date { let datetime = Local From 634edf2b65943be7d7b852b3987226cf68a5309a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 9 Dec 2024 17:56:40 +0200 Subject: [PATCH 757/979] chore(ui): rename all timeline "day dividers" to "date deviders" following the introduction of montly divider mode --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 10 +- .../src/timeline/controller/mod.rs | 13 +- .../src/timeline/controller/state.rs | 35 +- .../src/timeline/day_dividers.rs | 350 +++++++++--------- .../src/timeline/event_handler.rs | 8 +- crates/matrix-sdk-ui/src/timeline/item.rs | 12 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 3 +- .../matrix-sdk-ui/src/timeline/tests/basic.rs | 34 +- .../matrix-sdk-ui/src/timeline/tests/echo.rs | 62 ++-- .../matrix-sdk-ui/src/timeline/tests/edit.rs | 20 +- .../src/timeline/tests/encryption.rs | 8 +- .../src/timeline/tests/event_filter.rs | 4 +- .../src/timeline/tests/reactions.rs | 4 +- .../src/timeline/tests/read_receipts.rs | 22 +- .../src/timeline/tests/redaction.rs | 4 +- .../src/timeline/tests/shields.rs | 6 +- .../matrix-sdk-ui/src/timeline/tests/virt.rs | 52 +-- .../src/timeline/virtual_item.rs | 5 +- .../tests/integration/room_list_service.rs | 2 +- .../tests/integration/timeline/echo.rs | 30 +- .../tests/integration/timeline/edit.rs | 32 +- .../tests/integration/timeline/focus_event.rs | 20 +- .../tests/integration/timeline/mod.rs | 32 +- .../tests/integration/timeline/pagination.rs | 22 +- .../integration/timeline/pinned_event.rs | 32 +- .../tests/integration/timeline/profiles.rs | 8 +- .../tests/integration/timeline/queue.rs | 12 +- .../tests/integration/timeline/reactions.rs | 14 +- .../integration/timeline/read_receipts.rs | 8 +- .../tests/integration/timeline/replies.rs | 6 +- .../integration/timeline/sliding_sync.rs | 18 +- .../tests/integration/timeline/subscribe.rs | 12 +- labs/multiverse/src/main.rs | 2 +- 33 files changed, 465 insertions(+), 437 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 08553bbc09a..89f226dc6a0 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -986,7 +986,7 @@ impl TimelineItem { pub fn as_virtual(self: Arc) -> Option { use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem; match self.0.as_virtual()? { - VItem::DayDivider(ts) => Some(VirtualTimelineItem::DayDivider { ts: ts.0.into() }), + VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: ts.0.into() }), VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker), } } @@ -1255,8 +1255,9 @@ impl SendAttachmentJoinHandle { /// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event. #[derive(uniffi::Enum)] pub enum VirtualTimelineItem { - /// A divider between messages of two days. - DayDivider { + /// A divider between messages of different day or month depending on + /// timeline settings. + DateDivider { /// A timestamp in milliseconds since Unix Epoch on that day in local /// time. ts: u64, @@ -1359,7 +1360,8 @@ impl LazyTimelineItemProvider { } } -/// Changes how dividers get inserted, either in between each day or in between each month +/// Changes how date dividers get inserted, either in between each day or in +/// between each month #[derive(Debug, Clone, uniffi::Enum)] pub enum DateDividerMode { Daily, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 96231a094b4..222dea6bf25 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -75,7 +75,7 @@ use super::{ }; use crate::{ timeline::{ - day_dividers::DayDividerAdjuster, + day_dividers::DateDividerAdjuster, event_item::EventTimelineItemKind, pinned_events_loader::{PinnedEventsLoader, PinnedEventsLoaderError}, reactions::FullReactionKey, @@ -796,8 +796,9 @@ impl TimelineController

{ warn!("Message echo got duplicated, removing the local one"); txn.items.remove(idx); - // Adjust the day dividers, if needs be. - let mut adjuster = DayDividerAdjuster::new(self.settings.date_divider_mode.clone()); + // Adjust the date dividers, if needs be. + let mut adjuster = + DateDividerAdjuster::new(self.settings.date_divider_mode.clone()); adjuster.run(&mut txn.items, &mut txn.meta); } @@ -894,9 +895,9 @@ impl TimelineController

{ txn.items.remove(idx); - // A read marker or a day divider may have been inserted before the local echo. + // A read marker or a date divider may have been inserted before the local echo. // Ensure both are up to date. - let mut adjuster = DayDividerAdjuster::new(self.settings.date_divider_mode.clone()); + let mut adjuster = DateDividerAdjuster::new(self.settings.date_divider_mode.clone()); adjuster.run(&mut txn.items, &mut txn.meta); txn.meta.update_read_marker(&mut txn.items); @@ -986,7 +987,7 @@ impl TimelineController

{ txn.items.replace(idx, new_item); // This doesn't change the original sending time, so there's no need to adjust - // day dividers. + // date dividers. txn.commit(); diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index aaf502d1d79..d9fdb090896 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -53,7 +53,7 @@ use super::{ use crate::{ events::SyncTimelineEventWithoutContent, timeline::{ - day_dividers::DayDividerAdjuster, + day_dividers::DateDividerAdjuster, event_handler::{ Flow, HandleEventResult, TimelineEventContext, TimelineEventHandler, TimelineEventKind, TimelineItemPosition, @@ -192,6 +192,7 @@ impl TimelineState { } /// Adds a local echo (for an event) to the timeline. + #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] pub(super) async fn handle_local_event( &mut self, @@ -217,13 +218,13 @@ impl TimelineState { let mut txn = self.transaction(); - let mut day_divider_adjuster = DayDividerAdjuster::new(date_divider_mode); + let mut date_divider_adjuster = DateDividerAdjuster::new(date_divider_mode); TimelineEventHandler::new(&mut txn, ctx) - .handle_event(&mut day_divider_adjuster, content) + .handle_event(&mut date_divider_adjuster, content) .await; - txn.adjust_day_dividers(day_divider_adjuster); + txn.adjust_date_dividers(date_divider_adjuster); txn.commit(); } @@ -240,7 +241,8 @@ impl TimelineState { { let mut txn = self.transaction(); - let mut day_divider_adjuster = DayDividerAdjuster::new(settings.date_divider_mode.clone()); + let mut date_divider_adjuster = + DateDividerAdjuster::new(settings.date_divider_mode.clone()); // Loop through all the indices, in order so we don't decrypt edits // before the event being edited, if both were UTD. Keep track of @@ -262,7 +264,7 @@ impl TimelineState { TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx }, room_data_provider, settings, - &mut day_divider_adjuster, + &mut date_divider_adjuster, ) .await; @@ -273,7 +275,7 @@ impl TimelineState { } } - txn.adjust_day_dividers(day_divider_adjuster); + txn.adjust_date_dividers(date_divider_adjuster); txn.commit(); } @@ -382,7 +384,8 @@ impl TimelineStateTransaction<'_> { let position = position.into(); - let mut day_divider_adjuster = DayDividerAdjuster::new(settings.date_divider_mode.clone()); + let mut date_divider_adjuster = + DateDividerAdjuster::new(settings.date_divider_mode.clone()); // Implementation note: when `position` is `TimelineEnd::Front`, events are in // the reverse topological order. Prepending them one by one in the order they @@ -399,7 +402,7 @@ impl TimelineStateTransaction<'_> { position, room_data_provider, settings, - &mut day_divider_adjuster, + &mut date_divider_adjuster, ) .await; @@ -407,7 +410,7 @@ impl TimelineStateTransaction<'_> { total.items_updated += handle_one_res.items_updated as u64; } - self.adjust_day_dividers(day_divider_adjuster); + self.adjust_date_dividers(date_divider_adjuster); self.check_no_unused_unique_ids(); total @@ -443,7 +446,7 @@ impl TimelineStateTransaction<'_> { position: TimelineItemPosition, room_data_provider: &P, settings: &TimelineSettings, - day_divider_adjuster: &mut DayDividerAdjuster, + date_divider_adjuster: &mut DateDividerAdjuster, ) -> HandleEventResult { let SyncTimelineEvent { push_actions, kind } = event; let encryption_info = kind.encryption_info().cloned(); @@ -643,7 +646,7 @@ impl TimelineStateTransaction<'_> { }; // Handle the event to create or update a timeline item. - TimelineEventHandler::new(self, ctx).handle_event(day_divider_adjuster, event_kind).await + TimelineEventHandler::new(self, ctx).handle_event(date_divider_adjuster, event_kind).await } fn clear(&mut self) { @@ -662,11 +665,11 @@ impl TimelineStateTransaction<'_> { } }); - // Remove stray day dividers + // Remove stray date dividers let mut idx = 0; while idx < self.items.len() { - if self.items[idx].is_day_divider() - && self.items.get(idx + 1).map_or(true, |item| item.is_day_divider()) + if self.items[idx].is_date_divider() + && self.items.get(idx + 1).map_or(true, |item| item.is_date_divider()) { self.items.remove(idx); // don't increment idx because all elements have shifted @@ -774,7 +777,7 @@ impl TimelineStateTransaction<'_> { } } - fn adjust_day_dividers(&mut self, mut adjuster: DayDividerAdjuster) { + fn adjust_date_dividers(&mut self, mut adjuster: DateDividerAdjuster) { adjuster.run(&mut self.items, &mut self.meta); } diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs index 3b78c6ae858..b04287ab22b 100644 --- a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/day_dividers.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Algorithm to adjust (insert/replace/remove) day dividers after new events +//! Algorithm to adjust (insert/replace/remove) date dividers after new events //! have been received from any source. use std::{fmt::Display, sync::Arc}; @@ -26,12 +26,12 @@ use super::{ DateDividerMode, TimelineItem, TimelineItemKind, VirtualTimelineItem, }; -/// Algorithm ensuring that day dividers are adjusted correctly, according to +/// Algorithm ensuring that date dividers are adjusted correctly, according to /// new items that have been inserted. -pub(super) struct DayDividerAdjuster { +pub(super) struct DateDividerAdjuster { /// The list of recorded operations to apply, after analyzing the latest /// items. - ops: Vec, + ops: Vec, /// A boolean indicating whether the struct has been used and thus must be /// mark unused manually by calling [`Self::run`]. @@ -40,11 +40,11 @@ pub(super) struct DayDividerAdjuster { mode: DateDividerMode, } -impl Drop for DayDividerAdjuster { +impl Drop for DateDividerAdjuster { fn drop(&mut self) { // Only run the assert if we're not currently panicking. if !std::thread::panicking() && !self.consumed { - error!("a DayDividerAdjuster has not been consumed with run()"); + error!("a DateDividerAdjuster has not been consumed with run()"); } } } @@ -61,19 +61,19 @@ struct PrevItemDesc<'a> { insert_op_at: usize, } -impl DayDividerAdjuster { +impl DateDividerAdjuster { pub fn new(mode: DateDividerMode) -> Self { Self { ops: Default::default(), // The adjuster starts as consumed, and it will be marked no consumed iff it's used // with `mark_used`. consumed: true, - mode: mode, + mode, } } - /// Marks this [`DayDividerAdjuster`] as used, which means it'll require a - /// call to [`DayDividerAdjuster::run`] before getting dropped. + /// Marks this [`DateDividerAdjuster`] as used, which means it'll require a + /// call to [`DateDividerAdjuster::run`] before getting dropped. pub fn mark_used(&mut self) { // Mark the adjuster as needing to be consumed. self.consumed = false; @@ -84,7 +84,7 @@ impl DayDividerAdjuster { #[instrument(skip_all)] pub fn run(&mut self, items: &mut ObservableItemsTransaction<'_>, meta: &mut TimelineMetadata) { // We're going to record vector operations like inserting, replacing and - // removing day dividers. Since we may remove or insert new items, + // removing date dividers. Since we may remove or insert new items, // recorded offsets will change as we're iterating over the array. The // only way this is possible is because we're recording operations // happening in non-decreasing order of the indices, i.e. we can't do an @@ -105,10 +105,10 @@ impl DayDividerAdjuster { for (i, item) in items.iter().enumerate() { match item.kind() { - TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(ts)) => { - // Record what the last alive item pair is only if we haven't removed the day + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(ts)) => { + // Record what the last alive item pair is only if we haven't removed the date // divider. - if !self.handle_day_divider(i, *ts, prev_item.as_ref().map(|desc| desc.item)) { + if !self.handle_date_divider(i, *ts, prev_item.as_ref().map(|desc| desc.item)) { prev_item = Some(PrevItemDesc { item_index: i, item, @@ -133,19 +133,19 @@ impl DayDividerAdjuster { } } - // Also chase trailing day dividers explicitly, by iterating from the end to the - // start. Since they wouldn't be the prev_item of anything, we wouldn't - // analyze them in the previous loop. + // Also chase trailing date dividers explicitly, by iterating from the end to + // the start. Since they wouldn't be the prev_item of anything, we + // wouldn't analyze them in the previous loop. for (i, item) in items.iter().enumerate().rev() { - if item.is_day_divider() { - // The item is a trailing day divider: remove it, if it wasn't already scheduled - // for deletion. + if item.is_date_divider() { + // The item is a trailing date divider: remove it, if it wasn't already + // scheduled for deletion. if !self .ops .iter() - .any(|op| matches!(op, DayDividerOperation::Remove(j) if i == *j)) + .any(|op| matches!(op, DateDividerOperation::Remove(j) if i == *j)) { - trace!("removing trailing day divider @ {i}"); + trace!("removing trailing date divider @ {i}"); // Find the index at which to insert the removal operation. It must be before // any other operation on a bigger index, to maintain the @@ -153,7 +153,7 @@ impl DayDividerAdjuster { let index = self.ops.iter().position(|op| op.index() > i).unwrap_or(self.ops.len()); - self.ops.insert(index, DayDividerOperation::Remove(i)); + self.ops.insert(index, DateDividerOperation::Remove(i)); } } @@ -181,38 +181,38 @@ impl DayDividerAdjuster { self.consumed = true; } - /// Decides what to do with a day divider. + /// Decides what to do with a date divider. /// /// Returns whether it's been removed or not. #[inline] - fn handle_day_divider( + fn handle_date_divider( &mut self, i: usize, ts: MilliSecondsSinceUnixEpoch, prev_item: Option<&Arc>, ) -> bool { let Some(prev_item) = prev_item else { - // No interesting item prior to the day divider: it must be the first one, + // No interesting item prior to the date divider: it must be the first one, // nothing to do. return false; }; match prev_item.kind() { TimelineItemKind::Event(event) => { - // This day divider is preceded by an event. + // This date divider is preceded by an event. if self.is_same_date_as(event.timestamp(), ts) { - // The event has the same date as the day divider: remove the current day + // The event has the same date as the date divider: remove the current date // divider. - trace!("removing day divider following event with same timestamp @ {i}"); - self.ops.push(DayDividerOperation::Remove(i)); + trace!("removing date divider following event with same timestamp @ {i}"); + self.ops.push(DateDividerOperation::Remove(i)); return true; } } - TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(_)) => { - trace!("removing duplicate day divider @ {i}"); - // This day divider is preceded by another one: remove the current one. - self.ops.push(DayDividerOperation::Remove(i)); + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)) => { + trace!("removing duplicate date divider @ {i}"); + // This date divider is preceded by another one: remove the current one. + self.ops.push(DateDividerOperation::Remove(i)); return true; } @@ -233,45 +233,48 @@ impl DayDividerAdjuster { latest_event_ts: Option, ) { let Some(PrevItemDesc { item_index, insert_op_at, item }) = prev_item_desc else { - // The event was the first item, so there wasn't any day divider before it: + // The event was the first item, so there wasn't any date divider before it: // insert one. - trace!("inserting the first day divider @ {}", i); - self.ops.push(DayDividerOperation::Insert(i, ts)); + trace!("inserting the first date divider @ {}", i); + self.ops.push(DateDividerOperation::Insert(i, ts)); return; }; match item.kind() { TimelineItemKind::Event(prev_event) => { // The event is preceded by another event. If they're not the same date, - // insert a day divider. + // insert a date divider. let prev_ts = prev_event.timestamp(); if !self.is_same_date_as(prev_ts, ts) { - trace!("inserting day divider @ {} between two events with different dates", i); - self.ops.push(DayDividerOperation::Insert(i, ts)); + trace!( + "inserting date divider @ {} between two events with different dates", + i + ); + self.ops.push(DateDividerOperation::Insert(i, ts)); } } - TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(prev_ts)) => { + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(prev_ts)) => { let event_date = timestamp_to_date(ts); - // The event is preceded by a day divider. + // The event is preceded by a date divider. if timestamp_to_date(*prev_ts) != event_date { - // The day divider is wrong. Should we replace it with the correct value, or + // The date divider is wrong. Should we replace it with the correct value, or // remove it entirely? if let Some(last_event_ts) = latest_event_ts { if timestamp_to_date(last_event_ts) == event_date { // There's a previous event with the same date: remove the divider. - trace!("removed day divider @ {item_index} between two events that have the same date"); - self.ops.insert(insert_op_at, DayDividerOperation::Remove(item_index)); + trace!("removed date divider @ {item_index} between two events that have the same date"); + self.ops.insert(insert_op_at, DateDividerOperation::Remove(item_index)); return; } } // There's no previous event or there's one with a different date: replace // the current divider. - trace!("replacing day divider @ {item_index} with new timestamp from event"); - self.ops.insert(insert_op_at, DayDividerOperation::Replace(item_index, ts)); + trace!("replacing date divider @ {item_index} with new timestamp from event"); + self.ops.insert(insert_op_at, DateDividerOperation::Replace(item_index, ts)); } } @@ -290,7 +293,7 @@ impl DayDividerAdjuster { for op in &self.ops { match *op { - DayDividerOperation::Insert(i, ts) => { + DateDividerOperation::Insert(i, ts) => { assert!(i >= max_i, "trying to insert at {i} < max_i={max_i}"); let at = (i64::try_from(i).unwrap() + offset) @@ -298,7 +301,7 @@ impl DayDividerAdjuster { assert!(at >= 0); let at = at as usize; - let item = meta.new_timeline_item(VirtualTimelineItem::DayDivider(ts)); + let item = meta.new_timeline_item(VirtualTimelineItem::DateDivider(ts)); // Keep push semantics, if we're inserting at the front or the back. if at == items.len() { @@ -313,7 +316,7 @@ impl DayDividerAdjuster { max_i = i; } - DayDividerOperation::Replace(i, ts) => { + DateDividerOperation::Replace(i, ts) => { assert!(i >= max_i, "trying to replace at {i} < max_i={max_i}"); let at = i64::try_from(i).unwrap() + offset; @@ -321,13 +324,13 @@ impl DayDividerAdjuster { let at = at as usize; let replaced = &items[at]; - if !replaced.is_day_divider() { - error!("we replaced a non day-divider @ {i}: {:?}", replaced.kind()); + if !replaced.is_date_divider() { + error!("we replaced a non date-divider @ {i}: {:?}", replaced.kind()); } let unique_id = replaced.unique_id(); let item = TimelineItem::new( - VirtualTimelineItem::DayDivider(ts), + VirtualTimelineItem::DateDivider(ts), unique_id.to_owned(), ); @@ -335,15 +338,15 @@ impl DayDividerAdjuster { max_i = i; } - DayDividerOperation::Remove(i) => { + DateDividerOperation::Remove(i) => { assert!(i >= max_i, "trying to replace at {i} < max_i={max_i}"); let at = i64::try_from(i).unwrap() + offset; assert!(at >= 0); let removed = items.remove(at as usize); - if !removed.is_day_divider() { - error!("we removed a non day-divider @ {i}: {:?}", removed.kind()); + if !removed.is_date_divider() { + error!("we removed a non date-divider @ {i}: {:?}", removed.kind()); } offset -= 1; @@ -353,7 +356,7 @@ impl DayDividerAdjuster { } } - /// Checks the invariants that must hold at any time after inserting day + /// Checks the invariants that must hold at any time after inserting date /// dividers. /// /// Returns a report if and only if there was at least one error. @@ -361,8 +364,8 @@ impl DayDividerAdjuster { &mut self, items: &'a ObservableItemsTransaction<'o>, initial_state: Option>>, - ) -> Option> { - let mut report = DayDividerInvariantsReport { + ) -> Option> { + let mut report = DateDividerInvariantsReport { initial_state, errors: Vec::new(), operations: std::mem::take(&mut self.ops), @@ -370,45 +373,45 @@ impl DayDividerAdjuster { }; // Assert invariants. - // 1. The timeline starts with a day divider. + // 1. The timeline starts with a date divider. if let Some(item) = items.get(0) { if item.is_read_marker() { if let Some(next_item) = items.get(1) { - if !next_item.is_day_divider() { - report.errors.push(DayDividerInsertError::FirstItemNotDayDivider); + if !next_item.is_date_divider() { + report.errors.push(DateDividerInsertError::FirstItemNotDateDivider); } } - } else if !item.is_day_divider() { - report.errors.push(DayDividerInsertError::FirstItemNotDayDivider); + } else if !item.is_date_divider() { + report.errors.push(DateDividerInsertError::FirstItemNotDateDivider); } } - // 2. There are no two day dividers following each other. + // 2. There are no two date dividers following each other. { - let mut prev_was_day_divider = false; + let mut prev_was_date_divider = false; for (i, item) in items.iter().enumerate() { - if item.is_day_divider() { - if prev_was_day_divider { - report.errors.push(DayDividerInsertError::DuplicateDayDivider { at: i }); + if item.is_date_divider() { + if prev_was_date_divider { + report.errors.push(DateDividerInsertError::DuplicateDateDivider { at: i }); } - prev_was_day_divider = true; + prev_was_date_divider = true; } else { - prev_was_day_divider = false; + prev_was_date_divider = false; } } }; - // 3. There's no trailing day divider. + // 3. There's no trailing date divider. if let Some(last) = items.last() { - if last.is_day_divider() { - report.errors.push(DayDividerInsertError::TrailingDayDivider); + if last.is_date_divider() { + report.errors.push(DateDividerInsertError::TrailingDateDivider); } } - // 4. Items are properly separated with day dividers. + // 4. Items are properly separated with date dividers. { let mut prev_event_ts = None; - let mut prev_day_divider_ts = None; + let mut prev_date_divider_ts = None; for (i, item) in items.iter().enumerate() { if let Some(ev) = item.as_event() { @@ -418,16 +421,16 @@ impl DayDividerAdjuster { if let Some(prev_ts) = prev_event_ts { if !self.is_same_date_as(prev_ts, ts) { report.errors.push( - DayDividerInsertError::MissingDayDividerBetweenEvents { at: i }, + DateDividerInsertError::MissingDateDividerBetweenEvents { at: i }, ); } } - // There is a day divider before us, and it's the same date as our timestamp. - if let Some(prev_ts) = prev_day_divider_ts { + // There is a date divider before us, and it's the same date as our timestamp. + if let Some(prev_ts) = prev_date_divider_ts { if !self.is_same_date_as(prev_ts, ts) { report.errors.push( - DayDividerInsertError::InconsistentDateAfterPreviousDayDivider { + DateDividerInsertError::InconsistentDateAfterPreviousDateDivider { at: i, }, ); @@ -435,24 +438,24 @@ impl DayDividerAdjuster { } else { report .errors - .push(DayDividerInsertError::MissingDayDividerBeforeEvent { at: i }); + .push(DateDividerInsertError::MissingDateDividerBeforeEvent { at: i }); } prev_event_ts = Some(ts); - } else if let TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(ts)) = + } else if let TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(ts)) = item.kind() { - // The previous day divider is for a different date. - if let Some(prev_ts) = prev_day_divider_ts { + // The previous date divider is for a different date. + if let Some(prev_ts) = prev_date_divider_ts { if self.is_same_date_as(prev_ts, *ts) { report .errors - .push(DayDividerInsertError::DuplicateDayDivider { at: i }); + .push(DateDividerInsertError::DuplicateDateDivider { at: i }); } } prev_event_ts = None; - prev_day_divider_ts = Some(*ts); + prev_date_divider_ts = Some(*ts); } } } @@ -463,7 +466,7 @@ impl DayDividerAdjuster { if state.iter().any(|item| item.is_read_marker()) && !report.final_state.iter().any(|item| item.is_read_marker()) { - report.errors.push(DayDividerInsertError::ReadMarkerDisappeared); + report.errors.push(DateDividerInsertError::ReadMarkerDisappeared); } } @@ -474,7 +477,8 @@ impl DayDividerAdjuster { } } - /// Returns whether the two dates for the given timestamps are the same or not. + /// Returns whether the two dates for the given timestamps are the same or + /// not. fn is_same_date_as( &self, lhs: MilliSecondsSinceUnixEpoch, @@ -490,35 +494,35 @@ impl DayDividerAdjuster { } #[derive(Debug)] -enum DayDividerOperation { +enum DateDividerOperation { Insert(usize, MilliSecondsSinceUnixEpoch), Replace(usize, MilliSecondsSinceUnixEpoch), Remove(usize), } -impl DayDividerOperation { +impl DateDividerOperation { fn index(&self) -> usize { match self { - DayDividerOperation::Insert(i, _) - | DayDividerOperation::Replace(i, _) - | DayDividerOperation::Remove(i) => *i, + DateDividerOperation::Insert(i, _) + | DateDividerOperation::Replace(i, _) + | DateDividerOperation::Remove(i) => *i, } } } -/// A report returned by [`DayDividerAdjuster::check_invariants`]. -struct DayDividerInvariantsReport<'a, 'o> { +/// A report returned by [`DateDividerAdjuster::check_invariants`]. +struct DateDividerInvariantsReport<'a, 'o> { /// Initial state before inserting the items. initial_state: Option>>, /// The operations that have been applied on the list. - operations: Vec, - /// Final state after inserting the day dividers. + operations: Vec, + /// Final state after inserting the date dividers. final_state: &'a ObservableItemsTransaction<'o>, /// Errors encountered in the algorithm. - errors: Vec, + errors: Vec, } -impl Display for DayDividerInvariantsReport<'_, '_> { +impl Display for DateDividerInvariantsReport<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Write all the items of a slice of timeline items. fn write_items( @@ -526,7 +530,7 @@ impl Display for DayDividerInvariantsReport<'_, '_> { items: &[Arc], ) -> std::fmt::Result { for (i, item) in items.iter().enumerate() { - if let TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(ts)) = item.kind() + if let TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(ts)) = item.kind() { writeln!(f, "#{i} --- {}", ts.0)?; } else if let Some(event) = item.as_event() { @@ -554,9 +558,9 @@ impl Display for DayDividerInvariantsReport<'_, '_> { writeln!(f, "\nOperations to apply:")?; for op in &self.operations { match *op { - DayDividerOperation::Insert(i, ts) => writeln!(f, "insert @ {i}: {}", ts.0)?, - DayDividerOperation::Replace(i, ts) => writeln!(f, "replace @ {i}: {}", ts.0)?, - DayDividerOperation::Remove(i) => writeln!(f, "remove @ {i}")?, + DateDividerOperation::Insert(i, ts) => writeln!(f, "insert @ {i}: {}", ts.0)?, + DateDividerOperation::Replace(i, ts) => writeln!(f, "replace @ {i}: {}", ts.0)?, + DateDividerOperation::Remove(i) => writeln!(f, "remove @ {i}")?, } } @@ -575,31 +579,31 @@ impl Display for DayDividerInvariantsReport<'_, '_> { } #[derive(Debug, thiserror::Error)] -enum DayDividerInsertError { - /// The first item isn't a day divider. - #[error("The first item isn't a day divider")] - FirstItemNotDayDivider, +enum DateDividerInsertError { + /// The first item isn't a date divider. + #[error("The first item isn't a date divider")] + FirstItemNotDateDivider, - /// There are two day dividers for the same date. - #[error("Duplicate day divider @ {at}.")] - DuplicateDayDivider { at: usize }, + /// There are two date dividers for the same date. + #[error("Duplicate date divider @ {at}.")] + DuplicateDateDivider { at: usize }, - /// The last item is a day divider. - #[error("The last item is a day divider.")] - TrailingDayDivider, + /// The last item is a date divider. + #[error("The last item is a date divider.")] + TrailingDateDivider, /// Two events are following each other but they have different dates - /// without a day divider between them. - #[error("Missing day divider between events @ {at}")] - MissingDayDividerBetweenEvents { at: usize }, + /// without a date divider between them. + #[error("Missing date divider between events @ {at}")] + MissingDateDividerBetweenEvents { at: usize }, - /// Some event is missing a day divider before it. - #[error("Missing day divider before event @ {at}")] - MissingDayDividerBeforeEvent { at: usize }, + /// Some event is missing a date divider before it. + #[error("Missing date divider before event @ {at}")] + MissingDateDividerBeforeEvent { at: usize }, - /// An event and the previous day divider aren't focused on the same date. - #[error("Event @ {at} and the previous day divider aren't targeting the same date")] - InconsistentDateAfterPreviousDayDivider { at: usize }, + /// An event and the previous date divider aren't focused on the same date. + #[error("Event @ {at} and the previous date divider aren't targeting the same date")] + InconsistentDateAfterPreviousDateDivider { at: usize }, /// The read marker has been removed. #[error("The read marker has been removed")] @@ -611,7 +615,7 @@ mod tests { use assert_matches2::assert_let; use ruma::{owned_event_id, owned_user_id, uint, MilliSecondsSinceUnixEpoch}; - use super::{super::controller::ObservableItems, DayDividerAdjuster}; + use super::{super::controller::ObservableItems, DateDividerAdjuster}; use crate::timeline::{ controller::TimelineMetadata, event_item::{EventTimelineItemKind, RemoteEventTimelineItem}, @@ -653,7 +657,7 @@ mod tests { } #[test] - fn test_no_trailing_day_divider() { + fn test_no_trailing_date_divider() { let mut items = ObservableItems::new(); let mut txn = items.transaction(); @@ -665,12 +669,12 @@ mod tests { txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); txn.push_back( - meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp_next_day)), + meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp_next_day)), None, ); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -678,7 +682,7 @@ mod tests { let mut iter = items.iter(); assert_let!(Some(item) = iter.next()); - assert!(item.is_day_divider()); + assert!(item.is_date_divider()); assert_let!(Some(item) = iter.next()); assert!(item.is_remote_event()); @@ -690,7 +694,7 @@ mod tests { } #[test] - fn test_read_marker_in_between_event_and_day_divider() { + fn test_read_marker_in_between_event_and_date_divider() { let mut items = ObservableItems::new(); let mut txn = items.transaction(); @@ -704,20 +708,20 @@ mod tests { let event = event_with_ts(timestamp); txn.push_back(meta.new_timeline_item(event.clone()), None); txn.push_back( - meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp_next_day)), + meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp_next_day)), None, ); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); txn.push_back(meta.new_timeline_item(event), None); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); let mut iter = items.iter(); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().unwrap().is_read_marker()); assert!(iter.next().unwrap().is_remote_event()); @@ -725,7 +729,7 @@ mod tests { } #[test] - fn test_read_marker_in_between_day_dividers() { + fn test_read_marker_in_between_date_dividers() { let mut items = ObservableItems::new(); let mut txn = items.transaction(); @@ -737,29 +741,29 @@ mod tests { assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day)); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); let mut iter = items.iter(); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().unwrap().is_read_marker()); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().is_none()); } #[test] - fn test_remove_all_day_dividers() { + fn test_remove_all_date_dividers() { let mut items = ObservableItems::new(); let mut txn = items.transaction(); @@ -771,25 +775,25 @@ mod tests { assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day)); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); let mut iter = items.iter(); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().is_none()); } #[test] - fn test_event_read_marker_spurious_day_divider() { + fn test_event_read_marker_spurious_date_divider() { let mut items = ObservableItems::new(); let mut txn = items.transaction(); @@ -799,23 +803,23 @@ mod tests { txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); let mut iter = items.iter(); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().unwrap().is_read_marker()); assert!(iter.next().is_none()); } #[test] - fn test_multiple_trailing_day_dividers() { + fn test_multiple_trailing_date_dividers() { let mut items = ObservableItems::new(); let mut txn = items.transaction(); @@ -824,10 +828,10 @@ mod tests { let timestamp = MilliSecondsSinceUnixEpoch(uint!(42)); txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); - txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DayDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); + txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -849,7 +853,7 @@ mod tests { txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None); txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); @@ -857,64 +861,78 @@ mod tests { let mut iter = items.iter(); assert!(iter.next().unwrap().is_read_marker()); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().is_none()); } #[test] fn test_dayly_divider_mode() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); - txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(0))))); + txn.push_back( + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(0)))), + None, + ); txn.push_back( meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(100000000)))), + None, + ); + txn.push_back( + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now())), + None, ); - txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now()))); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Daily); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily); adjuster.run(&mut txn, &mut meta); txn.commit(); let mut iter = items.iter(); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().is_none()); } #[test] fn test_monthly_divider_mode() { - let mut items = ObservableVector::new(); + let mut items = ObservableItems::new(); let mut txn = items.transaction(); let mut meta = test_metadata(); - txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(0))))); + txn.push_back( + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(0)))), + None, + ); txn.push_back( meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(100000000)))), + None, + ); + txn.push_back( + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now())), + None, ); - txn.push_back(meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now()))); - let mut adjuster = DayDividerAdjuster::new(DateDividerMode::Monthly); + let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Monthly); adjuster.run(&mut txn, &mut meta); txn.commit(); let mut iter = items.iter(); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().unwrap().is_remote_event()); - assert!(iter.next().unwrap().is_day_divider()); + assert!(iter.next().unwrap().is_date_divider()); assert!(iter.next().unwrap().is_remote_event()); assert!(iter.next().is_none()); } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index a7b4a2ef934..0709e851afb 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -54,7 +54,7 @@ use super::{ ObservableItemsTransaction, ObservableItemsTransactionEntry, PendingEdit, PendingEditKind, TimelineMetadata, TimelineStateTransaction, }, - day_dividers::DayDividerAdjuster, + day_dividers::DateDividerAdjuster, event_item::{ extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content, AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, @@ -348,12 +348,12 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { #[instrument(skip_all, fields(txn_id, event_id, position))] pub(super) async fn handle_event( mut self, - day_divider_adjuster: &mut DayDividerAdjuster, + date_divider_adjuster: &mut DateDividerAdjuster, event_kind: TimelineEventKind, ) -> HandleEventResult { let span = tracing::Span::current(); - day_divider_adjuster.mark_used(); + date_divider_adjuster.mark_used(); match &self.ctx.flow { Flow::Local { txn_id, .. } => { @@ -1144,7 +1144,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let old_item_id = old_item.internal_id; if idx == self.items.len() - 1 { - // If the old item is the last one and no day divider + // If the old item is the last one and no date divider // changes need to happen, replace and return early. trace!(idx, "Replacing existing event"); self.items.replace(idx, TimelineItem::new(item, old_item_id.to_owned())); diff --git a/crates/matrix-sdk-ui/src/timeline/item.rs b/crates/matrix-sdk-ui/src/timeline/item.rs index 6487d28f6d7..a192cb2c666 100644 --- a/crates/matrix-sdk-ui/src/timeline/item.rs +++ b/crates/matrix-sdk-ui/src/timeline/item.rs @@ -33,7 +33,7 @@ pub enum TimelineItemKind { /// An event or aggregation of multiple events. Event(EventTimelineItem), /// An item that doesn't correspond to an event, for example the user's - /// own read marker, or a day divider. + /// own read marker, or a date divider. Virtual(VirtualTimelineItem), } @@ -79,9 +79,9 @@ impl TimelineItem { /// /// It identifies the item on a best-effort basis. For instance, edits /// to an [`EventTimelineItem`] will not change the ID of the - /// enclosing `TimelineItem`. For some virtual items like day + /// enclosing `TimelineItem`. For some virtual items like date /// dividers, identity isn't easy to define though and you might - /// see a new ID getting generated for a day divider that you + /// see a new ID getting generated for a date divider that you /// perceive to be "the same" as a previous one. pub fn unique_id(&self) -> &TimelineUniqueId { &self.internal_id @@ -106,10 +106,10 @@ impl TimelineItem { matches!(&self.kind, TimelineItemKind::Event(_)) } - /// Check whether this item is a day divider. + /// Check whether this item is a date divider. #[must_use] - pub fn is_day_divider(&self) -> bool { - matches!(self.kind, TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(_))) + pub fn is_date_divider(&self) -> bool { + matches!(self.kind, TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_))) } pub(crate) fn is_read_marker(&self) -> bool { diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 2461251d4e2..fe8dfc03a8f 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -177,7 +177,8 @@ impl TimelineFocus { } } -/// Changes how dividers get inserted, either in between each day or in between each month +/// Changes how dividers get inserted, either in between each day or in between +/// each month #[derive(Debug, Clone)] pub enum DateDividerMode { Daily, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index 85cc7c2d48c..ba7abb9f7a3 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -62,7 +62,7 @@ async fn test_initial_events() { assert_eq!(item.as_event().unwrap().sender(), *BOB); let item = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert_matches!(&item.kind, TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(_))); + assert_matches!(&item.kind, TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_))); } #[async_test] @@ -100,7 +100,7 @@ async fn test_replace_with_initial_events_and_read_marker() { let items = timeline.controller.items().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "hey"); let ev = f.text_msg("yo").sender(*BOB).into_sync(); @@ -111,7 +111,7 @@ async fn test_replace_with_initial_events_and_read_marker() { let items = timeline.controller.items().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "yo"); } @@ -269,8 +269,8 @@ async fn test_other_state() { assert_eq!(content.name, "Alice's room"); assert_matches!(prev_content, None); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); timeline.handle_live_redacted_state_event(&ALICE, RedactedRoomTopicEventContent::new()).await; @@ -298,7 +298,7 @@ async fn test_dedup_pagination() { assert_eq!(timeline_items.len(), 2); assert_matches!( timeline_items[0].kind, - TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(_)) + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)) ); assert_matches!(timeline_items[1].kind, TimelineItemKind::Event(_)); } @@ -333,7 +333,7 @@ async fn test_dedup_initial() { let timeline_items = timeline.controller.items().await; assert_eq!(timeline_items.len(), 4); - assert!(timeline_items[0].is_day_divider()); + assert!(timeline_items[0].is_date_divider()); let event1 = &timeline_items[1]; let event2 = &timeline_items[2]; @@ -371,7 +371,7 @@ async fn test_internal_id_prefix() { let timeline_items = timeline.controller.items().await; assert_eq!(timeline_items.len(), 4); - assert!(timeline_items[0].is_day_divider()); + assert!(timeline_items[0].is_date_divider()); assert_eq!(timeline_items[0].unique_id().0, "le_prefix_3"); let event1 = &timeline_items[1]; @@ -427,8 +427,8 @@ async fn test_sanitized() { " ); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -445,8 +445,8 @@ async fn test_reply() { let first_event_id = first_event.event_id().unwrap(); let first_event_sender = *ALICE; - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); let reply_formatted_body = format!("\ \ @@ -495,8 +495,8 @@ async fn test_thread() { let first_event = item.as_event().unwrap(); let first_event_id = first_event.event_id().unwrap(); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); timeline .handle_live_event( @@ -537,7 +537,7 @@ async fn test_replace_with_initial_events_when_batched() { let (items, mut stream) = timeline.controller.subscribe_batched().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "hey"); let ev = f.text_msg("yo").sender(*BOB).into_sync(); @@ -547,7 +547,7 @@ async fn test_replace_with_initial_events_when_batched() { .await; // Assert there are more than a single Clear diff in the next batch: - // Clear + PushBack (event) + PushFront (day divider) + // Clear + PushBack (event) + PushFront (date divider) let batched_diffs = stream.next().await.unwrap(); assert_eq!(batched_diffs.len(), 3); assert_matches!(batched_diffs[0], VectorDiff::Clear); @@ -555,6 +555,6 @@ async fn test_replace_with_initial_events_when_batched() { assert!(value.as_event().is_some()); }); assert_matches!(&batched_diffs[2], VectorDiff::PushFront { value } => { - assert_matches!(value.as_virtual(), Some(VirtualTimelineItem::DayDivider(_))); + assert_matches!(value.as_virtual(), Some(VirtualTimelineItem::DateDivider(_))); }); } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index cd88dfd9b73..533c1c2c56d 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -56,9 +56,9 @@ async fn test_remote_echo_full_trip() { }; { - // The day divider comes in late. - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + // The date divider comes in late. + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); } // Scenario 2: The local event has not been sent to the server successfully, it @@ -143,8 +143,8 @@ async fn test_remote_echo_new_position() { let txn_id_from_event = item.as_event().unwrap(); assert_eq!(txn_id, txn_id_from_event.transaction_id().unwrap()); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); // … and another event that comes back before the remote echo timeline.handle_live_event(f.text_msg("test").sender(&BOB)).await; @@ -153,8 +153,8 @@ async fn test_remote_echo_new_position() { let bob_message = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); assert!(bob_message.is_remote_event()); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); // When the remote echo comes in… timeline @@ -171,13 +171,13 @@ async fn test_remote_echo_new_position() { let item = assert_next_matches!(stream, VectorDiff::Set { index: 3, value } => value); assert!(!item.as_event().unwrap().is_local_echo()); - // … the day divider is removed (because both bob's and alice's message are from - // the same day according to server timestamps). + // … the date divider is removed (because both bob's and alice's message are + // from the same day according to server timestamps). assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); } #[async_test] -async fn test_day_divider_duplication() { +async fn test_date_divider_duplication() { let timeline = TestTimeline::new(); // Given two remote events from one day, and a local event from another day… @@ -192,28 +192,28 @@ async fn test_day_divider_duplication() { let items = timeline.controller.items().await; assert_eq!(items.len(), 5); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); assert!(items[2].is_remote_event()); - assert!(items[3].is_day_divider()); + assert!(items[3].is_date_divider()); assert!(items[4].is_local_echo()); // … when the second remote event is re-received (day still the same) let event_id = items[2].as_event().unwrap().event_id().unwrap(); timeline.handle_live_event(f.text_msg("B").event_id(event_id).server_ts(1)).await; - // … it should not impact the day dividers. + // … it should not impact the date dividers. let items = timeline.controller.items().await; assert_eq!(items.len(), 5); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); assert!(items[2].is_remote_event()); - assert!(items[3].is_day_divider()); + assert!(items[3].is_date_divider()); assert!(items[4].is_local_echo()); } #[async_test] -async fn test_day_divider_removed_after_local_echo_disappeared() { +async fn test_date_divider_removed_after_local_echo_disappeared() { let timeline = TestTimeline::new(); let f = &timeline.factory; @@ -225,7 +225,7 @@ async fn test_day_divider_removed_after_local_echo_disappeared() { let items = timeline.controller.items().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); // Add a local echo. @@ -237,9 +237,9 @@ async fn test_day_divider_removed_after_local_echo_disappeared() { let items = timeline.controller.items().await; assert_eq!(items.len(), 4); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); - assert!(items[2].is_day_divider()); + assert!(items[2].is_date_divider()); assert!(items[3].is_local_echo()); // Cancel the local echo. @@ -252,7 +252,7 @@ async fn test_day_divider_removed_after_local_echo_disappeared() { let items = timeline.controller.items().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); } @@ -285,7 +285,7 @@ async fn test_no_read_marker_with_local_echo() { let items = timeline.controller.items().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); // Add a local echo. @@ -297,7 +297,7 @@ async fn test_no_read_marker_with_local_echo() { let items = timeline.controller.items().await; assert_eq!(items.len(), 3); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); assert!(items[2].is_local_echo()); @@ -311,7 +311,7 @@ async fn test_no_read_marker_with_local_echo() { let items = timeline.controller.items().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); assert!(items[1].is_remote_event()); } @@ -339,10 +339,10 @@ async fn test_no_reuse_of_counters() { item.unique_id().to_owned() }); - // The day divider comes in late. - // Timeline = [day-divider local] + // The date divider comes in late. + // Timeline = [date-divider local] assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // Add a remote event now. @@ -357,19 +357,19 @@ async fn test_no_reuse_of_counters() { ) .await; - // Timeline = [remote day-divider local] + // Timeline = [remote date-divider local] assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value: item } => { assert!(!item.as_event().unwrap().is_local_echo()); // Both items have a different unique id. assert_ne!(local_id, item.unique_id().to_owned()); }); - // Day divider shenanigans. - // Timeline = [day-divider remote day-divider local] + // Date divider shenanigans. + // Timeline = [date-divider remote date-divider local] assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); - // Timeline = [day-divider remote local] + // Timeline = [date-divider remote local] assert_next_matches_with_timeout!(stream, VectorDiff::Remove { index: 2 }); // When clearing the timeline, the local echo remains. diff --git a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs index e6c59889891..a0efc1370c1 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs @@ -58,8 +58,8 @@ async fn test_live_redacted() { assert_eq!(timeline.controller.items().await.len(), 2); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -81,8 +81,8 @@ async fn test_live_sanitized() { assert_eq!(text.body, "**original** message"); assert_eq!(text.formatted.as_ref().unwrap().body, "original message"); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); let first_event_id = first_event.event_id().unwrap(); @@ -151,8 +151,8 @@ async fn test_aggregated_sanitized() { assert_eq!(text.body, "!!edited!! **better** message"); assert_eq!(text.formatted.as_ref().unwrap().body, " better message"); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -288,8 +288,8 @@ async fn test_relations_edit_overrides_pending_edit_msg() { let text = event.content().as_message().unwrap(); assert_eq!(text.body(), "edit 2"); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); assert_pending!(stream); } @@ -366,8 +366,8 @@ async fn test_relations_edit_overrides_pending_edit_poll() { ); assert_eq!(poll.start_event_content.poll_start.answers.len(), 3); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); assert_pending!(stream); } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index 1daebc20160..135a662bca1 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -127,7 +127,7 @@ async fn test_retry_message_decryption() { assert_eq!(session_id, SESSION_ID); assert_next_matches!(stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); { @@ -451,7 +451,7 @@ async fn test_retry_edit_and_more() { let timeline_items = timeline.controller.items().await; assert_eq!(timeline_items.len(), 3); - assert!(timeline_items[0].is_day_divider()); + assert!(timeline_items[0].is_date_divider()); assert_eq!( timeline_items[1].as_event().unwrap().content().as_message().unwrap().body(), "edited" @@ -520,8 +520,8 @@ async fn test_retry_message_decryption_highlighted() { ); assert_eq!(session_id, SESSION_ID); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); let own_user_id = user_id!("@example:matrix.org"); let exported_keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "1234").unwrap(); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs b/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs index b6617cf5682..eb9e3bbafaf 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs @@ -47,7 +47,7 @@ async fn test_default_filter() { timeline.handle_live_event(f.text_msg("The first message").sender(&ALICE)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); - let _day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + let _date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); let first_event_id = item.as_event().unwrap().event_id().unwrap(); timeline @@ -134,7 +134,7 @@ async fn test_custom_filter() { let f = &timeline.factory; timeline.handle_live_event(f.text_msg("The first message").sender(&ALICE)).await; let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); - let _day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + let _date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); timeline .handle_live_redacted_message_event(&ALICE, RedactedRoomMessageEventContent::new()) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs index 5cd0282bd84..46f89bc0606 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs @@ -229,8 +229,8 @@ async fn send_first_message( let event_id = event_item.event_id().unwrap().to_owned(); let position = timeline.len().await - 1; - let day_divider = assert_next_matches!(*stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(*stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); (item_id, event_id, position) } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs index 2da7584f949..99ef23a08a9 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/read_receipts.rs @@ -54,7 +54,7 @@ async fn test_read_receipts_updates_on_live_events() { let event_a = item_a.as_event().unwrap(); assert!(event_a.read_receipts().is_empty()); - let _day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + let _date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); // Implicit read receipt of Bob. let item_b = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -124,7 +124,7 @@ async fn test_read_receipts_updates_on_back_paginated_events() { let items = timeline.controller.items().await; assert_eq!(items.len(), 3); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); // Implicit read receipt of Bob. let event_a = items[2].as_event().unwrap(); @@ -155,7 +155,7 @@ async fn test_read_receipts_updates_on_filtered_events() { let event_a = item_a.as_event().unwrap(); assert!(event_a.read_receipts().is_empty()); - let _day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + let _date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); // Implicit read receipt of Bob. let item_a = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value); @@ -240,7 +240,7 @@ async fn test_read_receipts_updates_on_filtered_events_with_stored() { let event_a = item_a.as_event().unwrap(); assert!(event_a.read_receipts().is_empty()); - let _day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + let _date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); // Stored read receipt of Bob. let item_a = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value); @@ -301,8 +301,8 @@ async fn test_read_receipts_updates_on_back_paginated_filtered_events() { let event_a = item_a.as_event().unwrap(); assert!(event_a.read_receipts().is_empty()); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); // Add non-filtered event to show read receipts. timeline @@ -318,11 +318,11 @@ async fn test_read_receipts_updates_on_back_paginated_filtered_events() { assert!(event_c.read_receipts().get(*BOB).is_some()); assert!(event_c.read_receipts().get(*CAROL).is_some()); - // Reinsert a new day divider before the first back-paginated event. - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + // Reinsert a new date divider before the first back-paginated event. + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); - // Remove the last day divider. + // Remove the last date divider. assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); assert_pending!(stream); @@ -413,7 +413,7 @@ async fn test_read_receipts_updates_on_message_decryption() { assert_eq!(clear_event.read_receipts().len(), 1); assert!(clear_event.read_receipts().get(*CAROL).is_some()); - let _day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + let _date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); // The second event is encrypted and only has Bob's receipt. let encrypted_item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs index ea1e06d80c4..11fa3023721 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs @@ -160,7 +160,7 @@ async fn test_reaction_redaction_timeline_filter() { // Adding a room message timeline.handle_live_event(f.text_msg("hi!").sender(&ALICE)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); - // Creates a day divider and the message. + // Creates a date divider and the message. assert_eq!(timeline.controller.items().await.len(), 2); // Reaction is attached to the message and doesn't add a timeline item. @@ -192,7 +192,7 @@ async fn test_receive_unredacted() { // redact the first one as well let items = timeline.controller.items().await; - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); let fst = items[1].as_event().unwrap(); timeline.handle_live_event(f.redaction(fst.event_id().unwrap()).sender(&ALICE)).await; diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index 33c90d0e25c..b87177e4ed0 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -75,9 +75,9 @@ async fn test_local_sent_in_clear_shield() { assert_eq!(shield, None); { - // The day divider comes in late. - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + // The date divider comes in late. + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); } // When the event is sent (but without a remote echo). diff --git a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs index 14c230ca0fd..a0ddb01da26 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/virt.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/virt.rs @@ -28,7 +28,7 @@ use super::TestTimeline; use crate::timeline::{traits::RoomDataProvider as _, VirtualTimelineItem}; #[async_test] -async fn test_day_divider() { +async fn test_date_divider() { let timeline = TestTimeline::new(); let mut stream = timeline.subscribe().await; @@ -41,8 +41,8 @@ async fn test_day_divider() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert_let!(VirtualTimelineItem::DayDivider(ts) = day_divider.as_virtual().unwrap()); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert_let!(VirtualTimelineItem::DateDivider(ts) = date_divider.as_virtual().unwrap()); let date = Local.timestamp_millis_opt(ts.0.into()).single().unwrap(); assert_eq!(date.year(), 1970); assert_eq!(date.month(), 1); @@ -65,8 +65,9 @@ async fn test_day_divider() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); - let day_divider = assert_next_matches!(stream, VectorDiff::Insert { index: 3, value } => value); - assert_let!(VirtualTimelineItem::DayDivider(ts) = day_divider.as_virtual().unwrap()); + let date_divider = + assert_next_matches!(stream, VectorDiff::Insert { index: 3, value } => value); + assert_let!(VirtualTimelineItem::DateDivider(ts) = date_divider.as_virtual().unwrap()); let date = Local.timestamp_millis_opt(ts.0.into()).single().unwrap(); assert_eq!(date.year(), 1970); assert_eq!(date.month(), 1); @@ -81,10 +82,11 @@ async fn test_day_divider() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); - // The other events are in the past so a local event always creates a new day + // The other events are in the past so a local event always creates a new date // divider. - let day_divider = assert_next_matches!(stream, VectorDiff::Insert { index: 5, value } => value); - assert!(day_divider.is_day_divider()); + let date_divider = + assert_next_matches!(stream, VectorDiff::Insert { index: 5, value } => value); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -102,9 +104,9 @@ async fn test_update_read_marker() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let event_id1 = item.as_event().unwrap().event_id().unwrap().to_owned(); - // Timeline: [day-divider, A]. - let day_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + // Timeline: [date-divider, A]. + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); timeline.controller.handle_fully_read_marker(event_id1.clone()).await; @@ -113,13 +115,13 @@ async fn test_update_read_marker() { // ^-- fully read assert!(stream.next().now_or_never().is_none()); - // Timeline: [day-divider, A, B]. + // Timeline: [date-divider, A, B]. timeline.handle_live_event(f.text_msg("B").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let event_id2 = item.as_event().unwrap().event_id().unwrap().to_owned(); // Now the read marker appears after the first event. - // Timeline: [day-divider, A, read-marker, B]. + // Timeline: [date-divider, A, read-marker, B]. // fully read --^ let item = assert_next_matches!(stream, VectorDiff::Insert { index: 2, value } => value); assert_matches!(item.as_virtual(), Some(VirtualTimelineItem::ReadMarker)); @@ -128,18 +130,18 @@ async fn test_update_read_marker() { // The read marker is removed but not reinserted, because it cannot be added at // the end. - // Timeline: [day-divider, A, B]. + // Timeline: [date-divider, A, B]. // ^-- fully read assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); - // Timeline: [day-divider, A, B, C]. + // Timeline: [date-divider, A, B, C]. // ^-- fully read timeline.handle_live_event(f.text_msg("C").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let event_id3 = item.as_event().unwrap().event_id().unwrap().to_owned(); // Now the read marker is reinserted after the second event. - // Timeline: [day-divider, A, B, read-marker, C]. + // Timeline: [date-divider, A, B, read-marker, C]. // ^-- fully read let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 3, value } => value); assert!(marker.is_read_marker()); @@ -158,7 +160,7 @@ async fn test_update_read_marker() { timeline.controller.handle_fully_read_marker(event_id2).await; assert!(stream.next().now_or_never().is_none()); - // Timeline: [day-divider, A, B, read-marker, C, D]. + // Timeline: [date-divider, A, B, read-marker, C, D]. // ^-- fully read timeline.handle_live_event(f.text_msg("D").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -167,17 +169,17 @@ async fn test_update_read_marker() { timeline.controller.handle_fully_read_marker(event_id3).await; // The read marker is moved after the third event (sent by another user). - // Timeline: [day-divider, A, B, C, D]. + // Timeline: [date-divider, A, B, C, D]. // fully read --^ assert_next_matches!(stream, VectorDiff::Remove { index: 3 }); - // Timeline: [day-divider, A, B, C, read-marker, D]. + // Timeline: [date-divider, A, B, C, read-marker, D]. // fully read --^ let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 4, value } => value); assert!(marker.is_read_marker()); // If the current user sends an event afterwards, the read marker doesn't move. - // Timeline: [day-divider, A, B, C, read-marker, D, E]. + // Timeline: [date-divider, A, B, C, read-marker, D, E]. // fully read --^ timeline.handle_live_event(f.text_msg("E").sender(&own_user)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -187,7 +189,7 @@ async fn test_update_read_marker() { // If the marker moved forward to another user's event, and there's no other // event sent from another user, then it will be removed. - // Timeline: [day-divider, A, B, C, D, E]. + // Timeline: [date-divider, A, B, C, D, E]. // fully read --^ timeline.controller.handle_fully_read_marker(event_id4).await; assert_next_matches!(stream, VectorDiff::Remove { index: 4 }); @@ -195,13 +197,13 @@ async fn test_update_read_marker() { assert!(stream.next().now_or_never().is_none()); // When a last event is inserted by ourselves, still no read marker. - // Timeline: [day-divider, A, B, C, D, E, F]. + // Timeline: [date-divider, A, B, C, D, E, F]. // fully read --^ timeline.handle_live_event(f.text_msg("F").sender(&own_user)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); - // Timeline: [day-divider, A, B, C, D, E, F, G]. + // Timeline: [date-divider, A, B, C, D, E, F, G]. // fully read --^ timeline.handle_live_event(f.text_msg("G").sender(&own_user)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); @@ -213,14 +215,14 @@ async fn test_update_read_marker() { // before their message. It is the first message that's both after the // fully-read event and not sent by us. // - // Timeline: [day-divider, A, B, C, D, E, F, G, H]. + // Timeline: [date-divider, A, B, C, D, E, F, G, H]. // fully read --^ timeline.handle_live_event(f.text_msg("H").sender(&BOB)).await; let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); item.as_event().unwrap(); // [our own] v-- sent by Bob - // Timeline: [day-divider, A, B, C, D, E, F, G, read-marker, H]. + // Timeline: [date-divider, A, B, C, D, E, F, G, read-marker, H]. // fully read --^ let marker = assert_next_matches!(stream, VectorDiff::Insert { index: 8, value } => value); assert!(marker.is_read_marker()); diff --git a/crates/matrix-sdk-ui/src/timeline/virtual_item.rs b/crates/matrix-sdk-ui/src/timeline/virtual_item.rs index 650bc38b643..727a44c2f5c 100644 --- a/crates/matrix-sdk-ui/src/timeline/virtual_item.rs +++ b/crates/matrix-sdk-ui/src/timeline/virtual_item.rs @@ -17,11 +17,12 @@ use ruma::MilliSecondsSinceUnixEpoch; /// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event. #[derive(Clone, Debug)] pub enum VirtualTimelineItem { - /// A divider between messages of two days. + /// A divider between messages of two days or months depending on the + /// timeline configuration. /// /// The value is a timestamp in milliseconds since Unix Epoch on the given /// day in local time. - DayDivider(MilliSecondsSinceUnixEpoch), + DateDivider(MilliSecondsSinceUnixEpoch), /// The user's own read marker. ReadMarker, 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 c9e8374c16b..b28df20f150 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -2448,7 +2448,7 @@ async fn test_room_timeline() -> Result<(), Error> { // Previous timeline items. assert_matches!( **previous_timeline_items[0], - TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(_)) + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)) ); assert_matches!( &**previous_timeline_items[1], diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs index 1479c3648c3..f00ad135d05 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs @@ -92,8 +92,8 @@ async fn test_echo() { assert!(item.event_id().is_none()); let txn_id = item.transaction_id().unwrap(); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); // Wait for the sending to finish and assert everything was successful send_hdl.await.unwrap().unwrap(); @@ -127,10 +127,10 @@ async fn test_echo() { assert!(item.is_own()); assert_eq!(item.timestamp(), MilliSecondsSinceUnixEpoch(uint!(152038280))); - // The day divider is also replaced. - let day_divider = + // The date divider is also replaced. + let date_divider = assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => value); - assert!(day_divider.is_day_divider()); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -255,9 +255,9 @@ async fn test_dedup_by_event_id_late() { let item = local_echo.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - // Timeline: [day-divider, local echo] - let day_divider = assert_next_matches_with_timeout!( timeline_stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + // Timeline: [date-divider, local echo] + let date_divider = assert_next_matches_with_timeout!( timeline_stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); let f = EventFactory::new(); sync_builder.add_joined_room( @@ -273,20 +273,20 @@ async fn test_dedup_by_event_id_late() { mock_sync(&server, sync_builder.build_json_sync_response(), None).await; let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - // Timeline: [remote-echo, day-divider, local echo] + // Timeline: [remote-echo, date-divider, local echo] let remote_echo = assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => value); let item = remote_echo.as_event().unwrap(); assert_eq!(item.event_id(), Some(event_id)); - // Timeline: [day-divider, remote-echo, day-divider, local echo] - let day_divider = assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => value); - assert!(day_divider.is_day_divider()); + // Timeline: [date-divider, remote-echo, date-divider, local echo] + let date_divider = assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); - // Local echo and its day divider are removed. - // Timeline: [day-divider, remote-echo, day-divider] + // Local echo and its date divider are removed. + // Timeline: [date-divider, remote-echo, date-divider] assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 3 })); - // Timeline: [day-divider, remote-echo] + // Timeline: [date-divider, remote-echo] assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 4ad9e834238..9945e6e2c9b 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -103,8 +103,8 @@ async fn test_edit() { assert_matches!(msg.in_reply_to(), None); assert!(!msg.is_edited()); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); sync_builder.add_joined_room( JoinedRoomBuilder::new(room_id) @@ -208,8 +208,8 @@ async fn test_edit_local_echo() { let item = item.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); // We haven't set a route for sending events, so this will fail. @@ -780,8 +780,8 @@ async fn test_edit_local_echo_with_unsupported_content() { let item = item.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); // We haven't set a route for sending events, so this will fail. @@ -939,9 +939,9 @@ async fn test_pending_edit() { assert!(msg.is_edited()); assert_eq!(msg.body(), "[edit]"); - // The day divider. + // The date divider. assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // And nothing else. @@ -992,9 +992,9 @@ async fn test_pending_edit_overrides() { assert!(msg.is_edited()); assert_eq!(msg.body(), "bonjour"); - // The day divider. + // The date divider. assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // And nothing else. @@ -1040,9 +1040,9 @@ async fn test_pending_edit_from_backpagination() { assert!(msg.is_edited()); assert_eq!(msg.body(), "hello"); - // The day divider. + // The date divider. assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // And nothing else. @@ -1100,9 +1100,9 @@ async fn test_pending_edit_from_backpagination_doesnt_override_pending_edit_from assert!(msg.is_edited()); assert_eq!(msg.body(), "[edit]"); - // The day divider. + // The date divider. assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // And nothing else. @@ -1170,9 +1170,9 @@ async fn test_pending_poll_edit() { assert_eq!(results.answers[0].text, "Yes"); assert_eq!(results.answers[1].text, "No"); - // The day divider. + // The date divider. assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // And nothing else. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs index 240586a7ad0..29a68e0d939 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs @@ -91,8 +91,8 @@ async fn test_new_focused() { let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 5 + 1); // event items + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 5 + 1); // event items + a date divider + assert!(items[0].is_date_divider()); assert_eq!( items[1].as_event().unwrap().content().as_message().unwrap().body(), "i tried so hard" @@ -141,9 +141,9 @@ async fn test_new_focused() { "I kept everything inside" ); - // Day divider post processing. + // Date divider post processing. assert_let!(Some(VectorDiff::PushFront { value: item }) = timeline_stream.next().await); - assert!(item.is_day_divider()); + assert!(item.is_date_divider()); assert_let!(Some(VectorDiff::Remove { index }) = timeline_stream.next().await); assert_eq!(index, 3); @@ -227,8 +227,8 @@ async fn test_focused_timeline_reacts() { let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event items + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event items + a date divider + assert!(items[0].is_date_divider()); let event_item = items[1].as_event().unwrap(); assert_eq!(event_item.content().as_message().unwrap().body(), "yolo"); @@ -309,8 +309,8 @@ async fn test_focused_timeline_local_echoes() { let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event items + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event items + a date divider + assert!(items[0].is_date_divider()); let event_item = items[1].as_event().unwrap(); assert_eq!(event_item.content().as_message().unwrap().body(), "yolo"); @@ -385,8 +385,8 @@ async fn test_focused_timeline_doesnt_show_local_echoes() { let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event items + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event items + a date divider + assert!(items[0].is_date_divider()); let event_item = items[1].as_event().unwrap(); assert_eq!(event_item.content().as_message().unwrap().body(), "yolo"); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index df1319c7066..570323f4e8c 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -145,9 +145,9 @@ async fn test_reaction() { let senders: Vec<_> = group.keys().collect(); assert_eq!(senders.as_slice(), [user_id!("@bob:example.org")]); - // The day divider. - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + // The date divider. + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( sync_timeline_event!({ @@ -228,8 +228,8 @@ async fn test_redacted_message() { assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); assert_matches!(first.as_event().unwrap().content(), TimelineItemContent::RedactedMessage); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -270,8 +270,8 @@ async fn test_redact_message() { "buy my bitcoins bro" ); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); // Redacting a remote event works. mock_redaction(event_id!("$42")).mount(&server).await; @@ -347,11 +347,11 @@ async fn test_redact_local_sent_message() { assert!(event.is_local_echo()); assert_matches!(event.send_state(), Some(EventSendState::NotSentYet)); - // As well as a day divider. + // As well as a date divider. assert_let_timeout!( - Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next() + Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next() ); - assert!(day_divider.is_day_divider()); + assert!(date_divider.is_date_divider()); // We receive an update in the timeline from the send queue. assert_let_timeout!(Some(VectorDiff::Set { index, value: item }) = timeline_stream.next()); @@ -439,8 +439,8 @@ async fn test_read_marker() { assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); assert_matches!(message.as_event().unwrap().content(), TimelineItemContent::Message(_)); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); sync_builder.add_joined_room( JoinedRoomBuilder::new(room_id).add_account_data(RoomAccountDataTestEvent::FullyRead), @@ -525,8 +525,8 @@ async fn test_sync_highlighted() { // Own events don't trigger push rules. assert!(!remote_event.is_highlighted()); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( sync_timeline_event!({ @@ -589,7 +589,7 @@ async fn test_duplicate_maintains_correct_order() { let items = timeline.items().await; assert_eq!(items.len(), 2); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); let content = items[1].as_event().unwrap().content().as_message().unwrap().body(); assert_eq!(content, "C"); @@ -609,7 +609,7 @@ async fn test_duplicate_maintains_correct_order() { let items = timeline.items().await; assert_eq!(items.len(), 4, "{items:?}"); - assert!(items[0].is_day_divider()); + assert!(items[0].is_date_divider()); let content = items[1].as_event().unwrap().content().as_message().unwrap().body(); assert_eq!(content, "A"); let content = items[2].as_event().unwrap().content().as_message().unwrap().body(); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs index bc0225d92ea..06189a3531f 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs @@ -118,11 +118,11 @@ async fn test_back_pagination() { assert_eq!(content.name, "New room name"); assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); - let day_divider = assert_next_matches!( + let date_divider = assert_next_matches!( timeline_stream, VectorDiff::PushFront { value } => value ); - assert!(day_divider.is_day_divider()); + assert!(date_divider.is_date_divider()); Mock::given(method("GET")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) @@ -228,11 +228,11 @@ async fn test_back_pagination_highlighted() { // `m.room.tombstone` should be highlighted by default. assert!(remote_event.is_highlighted()); - let day_divider = assert_next_matches!( + let date_divider = assert_next_matches!( timeline_stream, VectorDiff::PushFront { value } => value ); - assert!(day_divider.is_day_divider()); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -480,7 +480,7 @@ async fn test_timeline_reset_while_paginating() { // field. assert!(hit_start); - // No events in back-pagination responses, day divider + event from latest + // No events in back-pagination responses, date divider + event from latest // sync is present assert_eq!(timeline.items().await.len(), 2); @@ -644,11 +644,11 @@ async fn test_empty_chunk() { assert_eq!(content.name, "New room name"); assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); - let day_divider = assert_next_matches!( + let date_divider = assert_next_matches!( timeline_stream, VectorDiff::PushFront { value } => value ); - assert!(day_divider.is_day_divider()); + assert!(date_divider.is_date_divider()); } #[async_test] @@ -744,11 +744,11 @@ async fn test_until_num_items_with_empty_chunk() { assert_eq!(content.name, "New room name"); assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); - let day_divider = assert_next_matches!( + let date_divider = assert_next_matches!( timeline_stream, VectorDiff::PushFront { value } => value ); - assert!(day_divider.is_day_divider()); + assert!(date_divider.is_date_divider()); timeline.live_paginate_backwards(10).await.unwrap(); @@ -760,11 +760,11 @@ async fn test_until_num_items_with_empty_chunk() { assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "hello room then"); - let day_divider = assert_next_matches!( + let date_divider = assert_next_matches!( timeline_stream, VectorDiff::PushFront { value } => value ); - assert!(day_divider.is_day_divider()); + assert!(date_divider.is_date_divider()); assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index 044b8cc354d..6a1e75a95fd 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -62,8 +62,8 @@ async fn test_new_pinned_events_are_added_on_sync() { // Load timeline items let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event item + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event item + a date divider + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "in the end"); assert_pending!(timeline_stream); test_helper.server.reset().await; @@ -96,7 +96,7 @@ async fn test_new_pinned_events_are_added_on_sync() { assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); }); assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); test_helper.server.reset().await; } @@ -142,8 +142,8 @@ async fn test_new_pinned_event_ids_reload_the_timeline() { let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event item + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event item + a date divider + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "in the end"); assert_pending!(timeline_stream); test_helper.server.reset().await; @@ -164,7 +164,7 @@ async fn test_new_pinned_event_ids_reload_the_timeline() { assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); }); assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); assert_pending!(timeline_stream); test_helper.server.reset().await; @@ -375,8 +375,8 @@ async fn test_edited_events_are_reflected_in_sync() { // Load timeline items let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event item + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event item + a date divider + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "in the end"); assert_pending!(timeline_stream); test_helper.server.reset().await; @@ -402,7 +402,7 @@ async fn test_edited_events_are_reflected_in_sync() { assert_eq!(event.event_id().unwrap(), event_id!("$1")); }); assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // The edit replaces the original event assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index, value } => { @@ -451,8 +451,8 @@ async fn test_redacted_events_are_reflected_in_sync() { // Load timeline items let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event item + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event item + a date divider + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "in the end"); assert_pending!(timeline_stream); test_helper.server.reset().await; @@ -474,7 +474,7 @@ async fn test_redacted_events_are_reflected_in_sync() { assert_eq!(event.event_id().unwrap(), event_id!("$1")); }); assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // The redaction replaces the original event assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index, value } => { @@ -518,8 +518,8 @@ async fn test_edited_events_survive_pinned_event_ids_change() { // Load timeline items let (items, mut timeline_stream) = timeline.subscribe().await; - assert_eq!(items.len(), 1 + 1); // event item + a day divider - assert!(items[0].is_day_divider()); + assert_eq!(items.len(), 1 + 1); // event item + a date divider + assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "in the end"); assert_pending!(timeline_stream); test_helper.server.reset().await; @@ -547,7 +547,7 @@ async fn test_edited_events_survive_pinned_event_ids_change() { assert_eq!(event.event_id().unwrap(), event_id!("$1")); }); assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); // The edit replaces the original event assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index, value } => { @@ -601,7 +601,7 @@ async fn test_edited_events_survive_pinned_event_ids_change() { assert_eq!(event.event_id().unwrap(), event_id!("$3")); }); assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); assert_pending!(timeline_stream); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/profiles.rs b/crates/matrix-sdk-ui/tests/integration/timeline/profiles.rs index 8dd7a05328d..8eeeefa7b23 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/profiles.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/profiles.rs @@ -79,7 +79,7 @@ async fn test_update_sender_profiles() { let timeline_items = timeline.items().await; assert_eq!(timeline_items.len(), 4); - assert!(timeline_items[0].is_day_divider()); + assert!(timeline_items[0].is_date_divider()); // We have seen a member event for alice from alice => we have a profile // albeit empty. assert_matches!( @@ -104,7 +104,7 @@ async fn test_update_sender_profiles() { let timeline_items = timeline.items().await; assert_eq!(timeline_items.len(), 4); - assert!(timeline_items[0].is_day_divider()); + assert!(timeline_items[0].is_date_divider()); assert_matches!( timeline_items[1].as_event().unwrap().sender_profile(), TimelineDetails::Ready(_) @@ -166,7 +166,7 @@ async fn test_update_sender_profiles() { let timeline_items = timeline.items().await; assert_eq!(timeline_items.len(), 4); - assert!(timeline_items[0].is_day_divider()); + assert!(timeline_items[0].is_date_divider()); assert_matches!( timeline_items[1].as_event().unwrap().sender_profile(), TimelineDetails::Ready(_) @@ -185,7 +185,7 @@ async fn test_update_sender_profiles() { let timeline_items = timeline.items().await; assert_eq!(timeline_items.len(), 4); - assert!(timeline_items[0].is_day_divider()); + assert!(timeline_items[0].is_date_divider()); assert_matches!( timeline_items[1].as_event().unwrap().sender_profile(), TimelineDetails::Ready(_) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs index edc522c5ab0..32a11923dfc 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs @@ -335,7 +335,7 @@ async fn test_clear_with_echoes() { // Wait for the first message to fail. Don't use time, but listen for the first // timeline item diff to get back signalling the error. - let _day_divider = timeline_stream.next().await; + let _date_divider = timeline_stream.next().await; let _local_echo = timeline_stream.next().await; let _local_echo_replaced_with_failure = timeline_stream.next().await; } @@ -389,7 +389,7 @@ async fn test_clear_with_echoes() { } #[async_test] -async fn test_no_duplicate_day_divider() { +async fn test_no_duplicate_date_divider() { let room_id = room_id!("!a98sd12bjh:example.org"); let (client, server) = logged_in_client_with_server().await; let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); @@ -448,7 +448,7 @@ async fn test_no_duplicate_day_divider() { }); assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { @@ -515,12 +515,12 @@ async fn test_no_duplicate_day_divider() { assert_eq!(value.event_id().unwrap(), "$5E2kLK/Sg342bgBU9ceEIEPYpbFaqJpZ"); }); - // A new day divider is inserted -> [DD First DD Second] + // A new date divider is inserted -> [DD First DD Second] assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); - // The useless day divider is removed. -> [DD First Second] + // The useless date divider is removed. -> [DD First Second] assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); assert_pending!(timeline_stream); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs index cbc8c04fe50..fdc958f9af8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs @@ -76,8 +76,8 @@ async fn test_abort_before_being_sent() { let item_id = item.identifier(); assert_eq!(item.content().as_message().unwrap().body(), "hello"); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = stream.next().await); + assert!(date_divider.is_date_divider()); // Now we try to add two reactions to this message… @@ -223,8 +223,8 @@ async fn test_redact_failed() { assert_eq!(item.as_event().unwrap().reactions().len(), 1); }); - assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value: day_divider } => { - assert!(day_divider.is_day_divider()); + assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value: date_divider } => { + assert!(date_divider.is_date_divider()); }); // Now, redact the annotation we previously added. @@ -315,9 +315,9 @@ async fn test_local_reaction_to_local_echo() { item.identifier() }); - // Good ol' day divider. - assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value: day_divider } => { - assert!(day_divider.is_day_divider()); + // Good ol' date divider. + assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value: date_divider } => { + assert!(date_divider.is_date_divider()); }); // Add a reaction before the remote echo comes back. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs index cd9817f80b3..22e39defb78 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs @@ -162,8 +162,8 @@ async fn test_read_receipts_updates() { let (alice_receipt_event_id, _) = timeline.latest_user_read_receipt(alice).await.unwrap(); assert_eq!(alice_receipt_event_id, third_event_id); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); // Read receipt on unknown event is ignored. sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( @@ -412,8 +412,8 @@ async fn test_read_receipts_updates_on_filtered_events() { timeline.latest_user_read_receipt_timeline_event_id(*ALICE).await.unwrap(); assert_eq!(alice_receipt_timeline_event, event_c_id); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); // Read receipt on filtered event. sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_ephemeral_event( diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs index 580bdc92860..e4a953da94c 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs @@ -84,8 +84,8 @@ async fn test_in_reply_to_details() { assert_eq!(in_reply_to.event_id, event_id!("$event1")); assert_matches!(in_reply_to.event, TimelineDetails::Ready(_)); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); // Add an reply to an unknown event to the timeline let event_id_2 = event_id!("$event2"); @@ -198,7 +198,7 @@ async fn test_transfer_in_reply_to_details_to_re_received_item() { server.reset().await; let items = timeline.items().await; - assert_eq!(items.len(), 2); // day divider, reply + assert_eq!(items.len(), 2); // date divider, reply let event_item = items[1].as_event().unwrap(); let in_reply_to = event_item.content().as_message().unwrap().in_reply_to().unwrap(); assert_eq!(in_reply_to.event_id, event_id_1); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs index 3d11c4c4a1a..ae194ceecaf 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs @@ -70,8 +70,8 @@ macro_rules! timeline_event { pub(crate) use timeline_event; macro_rules! assert_timeline_stream { - // `--- day divider ---` - ( @_ [ $stream:ident ] [ --- day divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + // `--- date divider ---` + ( @_ [ $stream:ident ] [ --- date divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ [ $stream ] @@ -85,7 +85,7 @@ macro_rules! assert_timeline_stream { assert_matches!( **value, TimelineItemKind::Virtual( - VirtualTimelineItem::DayDivider(_) + VirtualTimelineItem::DateDivider(_) ) ); } @@ -120,8 +120,8 @@ macro_rules! assert_timeline_stream { ) }; - // `prepend --- day divider ---` - ( @_ [ $stream:ident ] [ prepend --- day divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + // `prepend --- date divider ---` + ( @_ [ $stream:ident ] [ prepend --- date divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ [ $stream ] @@ -134,7 +134,7 @@ macro_rules! assert_timeline_stream { Some(Some(VectorDiff::PushFront { value })) => { assert_matches!( &**value, - TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(_)) => {} + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)) => {} ); } ); @@ -335,7 +335,7 @@ async fn test_timeline_basic() -> Result<()> { append "$x1:bar.org"; update[0] "$x1:bar.org"; append "$x2:bar.org"; - prepend --- day divider ---; + prepend --- date divider ---; }; } @@ -385,7 +385,7 @@ async fn test_timeline_duplicated_events() -> Result<()> { append "$x2:bar.org"; update[1] "$x2:bar.org"; append "$x3:bar.org"; - prepend --- day divider ---; + prepend --- date divider ---; }; } @@ -463,7 +463,7 @@ async fn test_timeline_read_receipts_are_updated_live() -> Result<()> { append "$x1:bar.org"; update[0] "$x1:bar.org"; append "$x2:bar.org"; - prepend --- day divider ---; + prepend --- date divider ---; }; } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs index 4989d51c3b2..249037faa1b 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs @@ -60,7 +60,7 @@ async fn test_batched() { let hdl = tokio::spawn(async move { let next_batch = timeline_stream.next().await.unwrap(); // There can be more than three updates because we add things like - // day dividers and implicit read receipts + // date dividers and implicit read receipts assert!(next_batch.len() >= 3); }); @@ -133,8 +133,8 @@ async fn test_event_filter() { assert_matches!(msg.msgtype(), MessageType::Text(_)); assert!(!msg.is_edited()); - assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await); - assert!(day_divider.is_day_divider()); + assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert!(date_divider.is_date_divider()); let second_event_id = event_id!("$Ga6Y2l0gKY"); let edit_event_id = event_id!("$7i9In0gEmB"); @@ -251,7 +251,7 @@ async fn test_timeline_is_reset_when_a_user_is_ignored_or_unignored() { assert_eq!(value.as_event().unwrap().event_id(), Some(third_event_id)); }); assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); assert_pending!(timeline_stream); @@ -299,7 +299,7 @@ async fn test_timeline_is_reset_when_a_user_is_ignored_or_unignored() { assert_eq!(value.as_event().unwrap().event_id(), Some(fifth_event_id)); }); assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); assert_pending!(timeline_stream); } @@ -370,7 +370,7 @@ async fn test_profile_updates() { assert_matches!(event_2_item.sender_profile(), TimelineDetails::Unavailable); assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_day_divider()); + assert!(value.is_date_divider()); }); assert_pending!(timeline_stream); diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 9032993726b..e61cc17348b 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -832,7 +832,7 @@ impl App { } TimelineItemKind::Virtual(virt) => match virt { - VirtualTimelineItem::DayDivider(unix_ts) => { + VirtualTimelineItem::DateDivider(unix_ts) => { content.push(format!("Date: {unix_ts:?}")); } VirtualTimelineItem::ReadMarker => { From 8f064581d6bd38c07257cd274376001ea09933f4 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 9 Dec 2024 17:59:48 +0200 Subject: [PATCH 758/979] chore(ui): rename the `day_dividers` module to `date_dividers` --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 2 +- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 2 +- .../src/timeline/{day_dividers.rs => date_dividers.rs} | 0 crates/matrix-sdk-ui/src/timeline/event_handler.rs | 2 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename crates/matrix-sdk-ui/src/timeline/{day_dividers.rs => date_dividers.rs} (100%) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 222dea6bf25..a90aafd9d91 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -75,7 +75,7 @@ use super::{ }; use crate::{ timeline::{ - day_dividers::DateDividerAdjuster, + date_dividers::DateDividerAdjuster, event_item::EventTimelineItemKind, pinned_events_loader::{PinnedEventsLoader, PinnedEventsLoaderError}, reactions::FullReactionKey, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index d9fdb090896..bfff486a6fe 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -53,7 +53,7 @@ use super::{ use crate::{ events::SyncTimelineEventWithoutContent, timeline::{ - day_dividers::DateDividerAdjuster, + date_dividers::DateDividerAdjuster, event_handler::{ Flow, HandleEventResult, TimelineEventContext, TimelineEventHandler, TimelineEventKind, TimelineItemPosition, diff --git a/crates/matrix-sdk-ui/src/timeline/day_dividers.rs b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs similarity index 100% rename from crates/matrix-sdk-ui/src/timeline/day_dividers.rs rename to crates/matrix-sdk-ui/src/timeline/date_dividers.rs diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 0709e851afb..475c1f30a14 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -54,7 +54,7 @@ use super::{ ObservableItemsTransaction, ObservableItemsTransactionEntry, PendingEdit, PendingEditKind, TimelineMetadata, TimelineStateTransaction, }, - day_dividers::DateDividerAdjuster, + date_dividers::DateDividerAdjuster, event_item::{ extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content, AnyOtherFullStateEventContent, EventSendState, EventTimelineItemKind, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index fe8dfc03a8f..61fc7824ce2 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -59,7 +59,7 @@ use crate::timeline::pinned_events_loader::PinnedEventsRoom; mod builder; mod controller; -mod day_dividers; +mod date_dividers; mod error; mod event_handler; mod event_item; From d5e7a9c9494142ec640ecabfe1cb3a4ce3b81ff9 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 10 Dec 2024 13:08:47 +0200 Subject: [PATCH 759/979] chore(ui): rename date divider `is_same_date_as` to `is_same_date_divider_group_as` --- .../src/timeline/date_dividers.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs index b04287ab22b..09f903b7b86 100644 --- a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs @@ -200,7 +200,7 @@ impl DateDividerAdjuster { match prev_item.kind() { TimelineItemKind::Event(event) => { // This date divider is preceded by an event. - if self.is_same_date_as(event.timestamp(), ts) { + if self.is_same_date_divider_group_as(event.timestamp(), ts) { // The event has the same date as the date divider: remove the current date // divider. trace!("removing date divider following event with same timestamp @ {i}"); @@ -246,7 +246,7 @@ impl DateDividerAdjuster { // insert a date divider. let prev_ts = prev_event.timestamp(); - if !self.is_same_date_as(prev_ts, ts) { + if !self.is_same_date_divider_group_as(prev_ts, ts) { trace!( "inserting date divider @ {} between two events with different dates", i @@ -419,7 +419,7 @@ impl DateDividerAdjuster { // We have the same date as the previous event we've seen. if let Some(prev_ts) = prev_event_ts { - if !self.is_same_date_as(prev_ts, ts) { + if !self.is_same_date_divider_group_as(prev_ts, ts) { report.errors.push( DateDividerInsertError::MissingDateDividerBetweenEvents { at: i }, ); @@ -428,7 +428,7 @@ impl DateDividerAdjuster { // There is a date divider before us, and it's the same date as our timestamp. if let Some(prev_ts) = prev_date_divider_ts { - if !self.is_same_date_as(prev_ts, ts) { + if !self.is_same_date_divider_group_as(prev_ts, ts) { report.errors.push( DateDividerInsertError::InconsistentDateAfterPreviousDateDivider { at: i, @@ -447,7 +447,7 @@ impl DateDividerAdjuster { { // The previous date divider is for a different date. if let Some(prev_ts) = prev_date_divider_ts { - if self.is_same_date_as(prev_ts, *ts) { + if self.is_same_date_divider_group_as(prev_ts, *ts) { report .errors .push(DateDividerInsertError::DuplicateDateDivider { at: i }); @@ -479,7 +479,7 @@ impl DateDividerAdjuster { /// Returns whether the two dates for the given timestamps are the same or /// not. - fn is_same_date_as( + fn is_same_date_divider_group_as( &self, lhs: MilliSecondsSinceUnixEpoch, rhs: MilliSecondsSinceUnixEpoch, @@ -867,7 +867,7 @@ mod tests { } #[test] - fn test_dayly_divider_mode() { + fn test_daily_divider_mode() { let mut items = ObservableItems::new(); let mut txn = items.transaction(); @@ -878,11 +878,11 @@ mod tests { None, ); txn.push_back( - meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(100000000)))), + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(86_400_000)))), // One day later None, ); txn.push_back( - meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now())), + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(2_678_400_000)))), // One month later None, ); @@ -914,11 +914,11 @@ mod tests { None, ); txn.push_back( - meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(100000000)))), + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(86_400_000)))), // One day later None, ); txn.push_back( - meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch::now())), + meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(2_678_400_000)))), // One month later None, ); From 723d7973d5bf0bce242dc4c887dbb4363d26ffe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 10 Dec 2024 12:53:57 +0100 Subject: [PATCH 760/979] fix(send_queue): Use MediaFormat::File when caching attachment thumbnail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `MediaFormat` reflects only the request that would be made to the homeserver. There is no link between the format of the files stored in the media cache and their purpose in an event. `MediaFormat::Tumbnail` means that we request a server-generated thumbnail of a file in the media repository. Since the thumbnail is its own file in the media repository, it makes more sense to use `MediaFormat::File`. Signed-off-by: Kévin Commaille --- .../matrix-sdk-base/src/store/send_queue.rs | 10 +++++-- crates/matrix-sdk/src/send_queue/upload.rs | 25 ++++++++--------- .../tests/integration/send_queue.rs | 27 +++++-------------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 6e7aac5f072..ece50344e9f 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -248,9 +248,15 @@ pub struct FinishUploadThumbnailInfo { /// Transaction id for the thumbnail upload. pub txn: OwnedTransactionId, /// Thumbnail's width. - pub width: UInt, + /// + /// Used previously, kept for backwards compatibility. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub width: Option, /// Thumbnail's height. - pub height: UInt, + /// + /// Used previously, kept for backwards compatibility. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub height: Option, } /// A transaction id identifying a [`DependentQueuedRequest`] rather than its diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index adc7456a1ff..f67b9d01609 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -137,20 +137,15 @@ impl RoomSendQueue { // Process the thumbnail, if it's been provided. if let Some(thumbnail) = config.thumbnail.take() { - // Normalize information to retrieve the thumbnail in the cache store. - let height = thumbnail.height; - let width = thumbnail.width; - let txn = TransactionId::new(); - trace!(upload_thumbnail_txn = %txn, thumbnail_size = ?(height, width), "attachment has a thumbnail"); + trace!(upload_thumbnail_txn = %txn, "attachment has a thumbnail"); // Create the information required for filling the thumbnail section of the // media event. let (data, content_type, thumbnail_info) = thumbnail.into_parts(); // Cache thumbnail in the cache store. - let thumbnail_media_request = - Media::make_local_thumbnail_media_request(&txn, height, width); + let thumbnail_media_request = Media::make_local_file_media_request(&txn); cache_store .add_media_content(&thumbnail_media_request, data) .await @@ -160,7 +155,7 @@ impl RoomSendQueue { Some(txn.clone()), Some((thumbnail_media_request.source.clone(), thumbnail_info)), Some(( - FinishUploadThumbnailInfo { txn, width, height }, + FinishUploadThumbnailInfo { txn, width: None, height: None }, thumbnail_media_request, content_type, )), @@ -266,18 +261,20 @@ impl QueueStorage { if let Some((info, new_source)) = thumbnail_info.as_ref().zip(sent_media.thumbnail.clone()) { - let from_req = - Media::make_local_thumbnail_media_request(&info.txn, info.height, info.width); + // Previously the media request used `MediaFormat::Thumbnail`. Handle this case + // for send queue requests that were in the state store before the change. + let from_req = if let Some((height, width)) = info.height.zip(info.width) { + Media::make_local_thumbnail_media_request(&info.txn, height, width) + } else { + Media::make_local_file_media_request(&info.txn) + }; trace!(from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store"); - // Reuse the same format for the cached thumbnail with the final MXC ID. - let new_format = from_req.format.clone(); - cache_store .replace_media_key( &from_req, - &MediaRequestParameters { source: new_source, format: new_format }, + &MediaRequestParameters { source: new_source, format: MediaFormat::File }, ) .await .map_err(RoomSendQueueStorageError::EventCacheStoreError)?; diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 70f1181e833..f1e08e2e150 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1893,10 +1893,7 @@ async fn test_media_uploads() { .get_media_content( &MediaRequestParameters { source: local_thumbnail_source.clone(), - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( - tinfo.width.unwrap(), - tinfo.height.unwrap(), - )), + format: MediaFormat::File, }, true, ) @@ -1910,7 +1907,10 @@ async fn test_media_uploads() { .get_media_content( &MediaRequestParameters { source: local_thumbnail_source.clone(), - format: MediaFormat::File, + format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( + tinfo.width.unwrap(), + tinfo.height.unwrap(), + )), }, true, ) @@ -1968,13 +1968,7 @@ async fn test_media_uploads() { let thumbnail_media = client .media() .get_media_content( - &MediaRequestParameters { - source: new_thumbnail_source, - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( - tinfo.width.unwrap(), - tinfo.height.unwrap(), - )), - }, + &MediaRequestParameters { source: new_thumbnail_source, format: MediaFormat::File }, true, ) .await @@ -2150,7 +2144,6 @@ async fn abort_and_verify( let file_source = img_content.source; let info = img_content.info.unwrap(); let thumbnail_source = info.thumbnail_source.unwrap(); - let thumbnail_info = info.thumbnail_info.unwrap(); let aborted = upload_handle.abort().await.unwrap(); assert!(aborted, "upload must have been aborted"); @@ -2170,13 +2163,7 @@ async fn abort_and_verify( client .media() .get_media_content( - &MediaRequestParameters { - source: thumbnail_source, - format: MediaFormat::Thumbnail(MediaThumbnailSettings::new( - thumbnail_info.width.unwrap(), - thumbnail_info.height.unwrap(), - )), - }, + &MediaRequestParameters { source: thumbnail_source, format: MediaFormat::File }, true, ) .await From 7295f290557e03234cc0c3dc3d731a1bbff176c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 10 Dec 2024 13:00:42 +0100 Subject: [PATCH 761/979] doc(send_queue): Document media caching of send_attachment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/send_queue/upload.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index f67b9d01609..7c32c8fa000 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -97,6 +97,11 @@ impl RoomSendQueue { /// client's sending queue will be disabled, and it will need to be /// manually re-enabled by the caller (e.g. after network is back, or when /// something has been done about the faulty requests). + /// + /// The attachment and its optional thumbnail are stored in the media cache + /// and can be retrieved at any time, by calling + /// [`Media::get_media_content()`] with the `MediaSource` that can be found + /// in the local or remote echo, and using a `MediaFormat::File`. #[instrument(skip_all, fields(event_txn))] pub async fn send_attachment( &self, From a562f73b1eeffe2cf39ac86118e0043bc80d6090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 10 Dec 2024 13:05:17 +0100 Subject: [PATCH 762/979] doc(timeline): Document media caching of send_attachment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk-ui/src/timeline/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 61fc7824ce2..9a9623e3f02 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -532,6 +532,12 @@ impl Timeline { /// If the encryption feature is enabled, this method will transparently /// encrypt the room message if the room is encrypted. /// + /// The attachment and its optional thumbnail are stored in the media cache + /// and can be retrieved at any time, by calling + /// [`Media::get_media_content()`] with the `MediaSource` that can be found + /// in the corresponding `TimelineEventItem`, and using a + /// `MediaFormat::File`. + /// /// # Arguments /// /// * `path` - The path of the file to be sent. @@ -540,6 +546,8 @@ impl Timeline { /// /// * `config` - An attachment configuration object containing details about /// the attachment like a thumbnail, its size, duration etc. + /// + /// [`Media::get_media_content()`]: matrix_sdk::Media::get_media_content #[instrument(skip_all)] pub fn send_attachment( &self, From eeb14f6cbed0ba465ccce06632c422e4fe31533e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 16:40:08 +0100 Subject: [PATCH 763/979] refactor!(event cache store): have the event cache store return raw linked chunks, not the full linked chunk And let the caller rebuild the linked chunk. This is slightly nicer in that it allows us to display the raw representation of a reloaded linked chunk, before checking its internal state is consistent; this will allow for better debug of issues related to the linked chunk internal state. No functional changes. --- .../event_cache/store/integration_tests.rs | 26 ++-- .../src/event_cache/store/memory_store.rs | 21 +-- .../src/event_cache/store/traits.rs | 10 +- .../src/linked_chunk/builder.rs | 18 ++- .../matrix-sdk-common/src/linked_chunk/mod.rs | 16 +++ .../src/linked_chunk/relational.rs | 78 +++++------ .../src/event_cache_store.rs | 128 +++++++----------- crates/matrix-sdk/src/event_cache/room/mod.rs | 34 +++-- 8 files changed, 170 insertions(+), 161 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index 0c4f36bbb7f..10651d446d9 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -21,7 +21,9 @@ use matrix_sdk_common::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind, VerificationState, }, - linked_chunk::{ChunkContent, Position, Update}, + linked_chunk::{ + ChunkContent, LinkedChunk, LinkedChunkBuilder, Position, RawLinkedChunk, Update, + }, }; use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use ruma::{ @@ -31,7 +33,7 @@ use ruma::{ use super::DynEventCacheStore; use crate::{ - event_cache::Gap, + event_cache::{Event, Gap}, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, }; @@ -119,6 +121,12 @@ pub trait EventCacheStoreIntegrationTests { async fn test_clear_all_rooms_chunks(&self); } +fn rebuild_linked_chunk( + raws: Vec>, +) -> Option> { + LinkedChunkBuilder::from_raw_parts(raws).build().unwrap() +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl EventCacheStoreIntegrationTests for DynEventCacheStore { @@ -332,7 +340,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { .unwrap(); // The linked chunk is correctly reloaded. - let lc = self.reload_linked_chunk(room_id).await.unwrap().expect("linked chunk not empty"); + let raws = self.reload_linked_chunk(room_id).await.unwrap(); + let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty"); let mut chunks = lc.chunks(); @@ -373,7 +382,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { async fn test_rebuild_empty_linked_chunk(&self) { // When I rebuild a linked chunk from an empty store, it's empty. - assert!(self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap().is_none()); + let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap(); + assert!(rebuild_linked_chunk(raw_parts).is_none()); } async fn test_clear_all_rooms_chunks(&self) { @@ -424,15 +434,15 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { .unwrap(); // Sanity check: both linked chunks can be reloaded. - assert!(self.reload_linked_chunk(r0).await.unwrap().is_some()); - assert!(self.reload_linked_chunk(r1).await.unwrap().is_some()); + assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some()); + assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some()); // Clear the chunks. self.clear_all_rooms_chunks().await.unwrap(); // Both rooms now have no linked chunk. - assert!(self.reload_linked_chunk(r0).await.unwrap().is_none()); - assert!(self.reload_linked_chunk(r1).await.unwrap().is_none()); + assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none()); + assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none()); } } diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 8f921b5808d..02aeba58fe1 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -16,13 +16,13 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::{relational::RelationalLinkedChunk, LinkedChunk, LinkedChunkBuilder, Update}, + linked_chunk::{relational::RelationalLinkedChunk, RawLinkedChunk, Update}, ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; use ruma::{MxcUri, OwnedMxcUri, RoomId}; -use super::{EventCacheStore, EventCacheStoreError, Result, DEFAULT_CHUNK_CAPACITY}; +use super::{EventCacheStore, EventCacheStoreError, Result}; use crate::{ event_cache::{Event, Gap}, media::{MediaRequestParameters, UniqueKey as _}, @@ -96,23 +96,12 @@ impl EventCacheStore for MemoryStore { async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { let inner = self.inner.read().unwrap(); - - let mut builder = LinkedChunkBuilder::new(); - inner .events - .reload_chunks(room_id, &mut builder) - .map_err(|err| EventCacheStoreError::InvalidData { details: err })?; - - builder.with_update_history(); - - let result = builder.build().map_err(|err| EventCacheStoreError::InvalidData { - details: format!("when rebuilding a linked chunk: {err}"), - })?; - - Ok(result) + .reload_chunks(room_id) + .map_err(|err| EventCacheStoreError::InvalidData { details: err }) } async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> { diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 50707a6dd50..4251eb8987e 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -16,7 +16,7 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::{LinkedChunk, Update}, + linked_chunk::{RawLinkedChunk, Update}, AsyncTraitDeps, }; use ruma::{MxcUri, RoomId}; @@ -29,6 +29,7 @@ use crate::{ /// A default capacity for linked chunks, when manipulating in conjunction with /// an `EventCacheStore` implementation. +// TODO: move back? pub const DEFAULT_CHUNK_CAPACITY: usize = 128; /// An abstract trait that can be used to implement different store backends @@ -56,11 +57,12 @@ pub trait EventCacheStore: AsyncTraitDeps { updates: Vec>, ) -> Result<(), Self::Error>; - /// Reconstruct a full linked chunk by reloading it from storage. + /// Return all the raw components of a linked chunk, so the caller may + /// reconstruct the linked chunk later. async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error>; + ) -> Result>, Self::Error>; /// Clear persisted events for all the rooms. /// @@ -190,7 +192,7 @@ impl EventCacheStore for EraseEventCacheStoreError { async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { self.0.reload_linked_chunk(room_id).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-common/src/linked_chunk/builder.rs b/crates/matrix-sdk-common/src/linked_chunk/builder.rs index 16550866cbd..0bcb3e20f31 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/builder.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/builder.rs @@ -21,7 +21,7 @@ use tracing::error; use super::{ Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk, - ObservableUpdates, + ObservableUpdates, RawLinkedChunk, }; /// A temporary chunk representation in the [`LinkedChunkBuilder`]. @@ -260,6 +260,22 @@ impl LinkedChunkBuilder { Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData })) } + + /// Fills a linked chunk builder from all the given raw parts. + pub fn from_raw_parts(raws: Vec>) -> Self { + let mut this = Self::new(); + for raw in raws { + match raw.content { + ChunkContent::Gap(gap) => { + this.push_gap(raw.previous, raw.id, raw.next, gap); + } + ChunkContent::Items(vec) => { + this.push_items(raw.previous, raw.id, raw.next, vec); + } + } + } + this + } } #[derive(thiserror::Error, Debug)] diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 40f3fbb019f..a0cb1a820be 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -1419,6 +1419,22 @@ impl EmptyChunk { } } +/// The raw representation of a linked chunk, as persisted in storage. +#[derive(Debug)] +pub struct RawLinkedChunk { + /// Content section of the linked chunk. + pub content: ChunkContent, + + /// Link to the previous chunk, via its identifier. + pub previous: Option, + + /// Current chunk's identifier. + pub id: ChunkIdentifier, + + /// Link to the next chunk, via its identifier. + pub next: Option, +} + #[cfg(test)] mod tests { use std::{ diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index 27f13740616..e823e927cfc 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -17,7 +17,7 @@ use ruma::{OwnedRoomId, RoomId}; -use super::LinkedChunkBuilder; +use super::{ChunkContent, RawLinkedChunk}; use crate::linked_chunk::{ChunkIdentifier, Position, Update}; /// A row of the [`RelationalLinkedChunk::chunks`]. @@ -290,11 +290,12 @@ where /// /// Return an error result if the data was malformed in the struct, with a /// string message explaining details about the error. - pub fn reload_chunks( + pub fn reload_chunks( &self, room_id: &RoomId, - builder: &mut LinkedChunkBuilder, - ) -> Result<(), String> { + ) -> Result>, String> { + let mut result = Vec::new(); + for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) { // Find all items that correspond to the chunk. let mut items = self @@ -309,12 +310,12 @@ where let Some(first) = items.peek() else { // The only possibility is that we created an empty items chunk; mark it as // such, and continue. - builder.push_items( - chunk_row.previous_chunk, - chunk_row.chunk, - chunk_row.next_chunk, - Vec::new(), - ); + result.push(RawLinkedChunk { + content: ChunkContent::Items(Vec::new()), + previous: chunk_row.previous_chunk, + id: chunk_row.chunk, + next: chunk_row.next_chunk, + }); continue; }; @@ -339,12 +340,14 @@ where // Sort them by their position. collected_items.sort_unstable_by_key(|(_item, index)| *index); - builder.push_items( - chunk_row.previous_chunk, - chunk_row.chunk, - chunk_row.next_chunk, - collected_items.into_iter().map(|(item, _index)| item), - ); + result.push(RawLinkedChunk { + content: ChunkContent::Items( + collected_items.into_iter().map(|(item, _index)| item).collect(), + ), + previous: chunk_row.previous_chunk, + id: chunk_row.chunk, + next: chunk_row.next_chunk, + }); } Either::Gap(gap) => { @@ -358,17 +361,17 @@ where )); } - builder.push_gap( - chunk_row.previous_chunk, - chunk_row.chunk, - chunk_row.next_chunk, - gap.clone(), - ); + result.push(RawLinkedChunk { + content: ChunkContent::Gap(gap.clone()), + previous: chunk_row.previous_chunk, + id: chunk_row.chunk, + next: chunk_row.next_chunk, + }); } } } - Ok(()) + Ok(result) } } @@ -383,6 +386,7 @@ mod tests { use ruma::room_id; use super::{ChunkIdentifier as CId, *}; + use crate::linked_chunk::LinkedChunkBuilder; #[test] fn test_new_items_chunk() { @@ -828,25 +832,17 @@ mod tests { } #[test] - fn test_rebuild_empty_linked_chunk() { - let mut builder = LinkedChunkBuilder::<3, _, _>::new(); - + fn test_reload_empty_linked_chunk() { let room_id = room_id!("!r0:matrix.org"); - // When I rebuild a linked chunk from an empty store, + // When I reload the linked chunk components from an empty store, let relational_linked_chunk = RelationalLinkedChunk::::new(); - relational_linked_chunk.reload_chunks(room_id, &mut builder).unwrap(); - - let lc = builder.build().expect("building succeeds"); - - // The builder won't return a linked chunk. - assert!(lc.is_none()); + let result = relational_linked_chunk.reload_chunks(room_id).unwrap(); + assert!(result.is_empty()); } #[test] fn test_reload_linked_chunk_with_empty_items() { - let mut builder = LinkedChunkBuilder::<3, _, _>::new(); - let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); @@ -858,9 +854,8 @@ mod tests { ); // It correctly gets reloaded as such. - relational_linked_chunk.reload_chunks(room_id, &mut builder).unwrap(); - - let lc = builder + let raws = relational_linked_chunk.reload_chunks(room_id).unwrap(); + let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws) .build() .expect("building succeeds") .expect("this leads to a non-empty linked chunk"); @@ -870,8 +865,6 @@ mod tests { #[test] fn test_rebuild_linked_chunk() { - let mut builder = LinkedChunkBuilder::<3, _, _>::new(); - let room_id = room_id!("!r0:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); @@ -896,9 +889,8 @@ mod tests { ], ); - relational_linked_chunk.reload_chunks(room_id, &mut builder).unwrap(); - - let lc = builder + let raws = relational_linked_chunk.reload_chunks(room_id).unwrap(); + let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws) .build() .expect("building succeeds") .expect("this leads to a non-empty linked chunk"); diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 08c8eb58435..be255445d20 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -21,11 +21,8 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc}; use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ - event_cache::{ - store::{EventCacheStore, DEFAULT_CHUNK_CAPACITY}, - Event, Gap, - }, - linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunk, LinkedChunkBuilder, Update}, + event_cache::{store::EventCacheStore, Event, Gap}, + linked_chunk::{ChunkContent, ChunkIdentifier, RawLinkedChunk, Update}, media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; @@ -147,49 +144,11 @@ impl SqliteEventCacheStore { )) } - async fn load_chunks(&self, room_id: &RoomId) -> Result> { - let room_id = room_id.to_owned(); - let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, &room_id); - - let this = self.clone(); - - let result = self - .acquire() - .await? - .with_transaction(move |txn| -> Result<_> { - let mut items = Vec::new(); - - // Use `ORDER BY id` to get a deterministic ordering for testing purposes. - for data in txn - .prepare( - "SELECT id, previous, next, type FROM linked_chunks WHERE room_id = ? ORDER BY id", - )? - .query_map((&hashed_room_id,), Self::map_row_to_chunk)? - { - let (id, previous, next, chunk_type) = data?; - let new = txn.rebuild_chunk( - &this, - &hashed_room_id, - previous, - id, - next, - chunk_type.as_str(), - )?; - items.push(new); - } - - Ok(items) - }) - .await?; - - Ok(result) - } - async fn load_chunk_with_id( &self, room_id: &RoomId, chunk_id: ChunkIdentifier, - ) -> Result { + ) -> Result> { let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, room_id); let this = self.clone(); @@ -217,7 +176,7 @@ trait TransactionExtForLinkedChunks { index: u64, next: Option, chunk_type: &str, - ) -> Result; + ) -> Result>; fn load_gap_content( &self, @@ -243,7 +202,7 @@ impl TransactionExtForLinkedChunks for Transaction<'_> { id: u64, next: Option, chunk_type: &str, - ) -> Result { + ) -> Result> { let previous = previous.map(ChunkIdentifier::new); let next = next.map(ChunkIdentifier::new); let id = ChunkIdentifier::new(id); @@ -317,14 +276,6 @@ impl TransactionExtForLinkedChunks for Transaction<'_> { } } -struct RawLinkedChunk { - content: ChunkContent, - - previous: Option, - id: ChunkIdentifier, - next: Option, -} - async fn create_pool(path: &Path) -> Result { fs::create_dir_all(path).await.map_err(OpenStoreError::CreateDir)?; let cfg = deadpool_sqlite::Config::new(path.join("matrix-sdk-event-cache.sqlite3")); @@ -586,27 +537,42 @@ impl EventCacheStore for SqliteEventCacheStore { async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error> { - let chunks = self.load_chunks(room_id).await?; + ) -> Result>, Self::Error> { + let room_id = room_id.to_owned(); + let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, &room_id); - let mut builder = LinkedChunkBuilder::new(); + let this = self.clone(); - for c in chunks { - match c.content { - ChunkContent::Gap(gap) => { - builder.push_gap(c.previous, c.id, c.next, gap); - } - ChunkContent::Items(items) => { - builder.push_items(c.previous, c.id, c.next, items); + let result = self + .acquire() + .await? + .with_transaction(move |txn| -> Result<_> { + let mut items = Vec::new(); + + // Use `ORDER BY id` to get a deterministic ordering for testing purposes. + for data in txn + .prepare( + "SELECT id, previous, next, type FROM linked_chunks WHERE room_id = ? ORDER BY id", + )? + .query_map((&hashed_room_id,), Self::map_row_to_chunk)? + { + let (id, previous, next, chunk_type) = data?; + let new = txn.rebuild_chunk( + &this, + &hashed_room_id, + previous, + id, + next, + chunk_type.as_str(), + )?; + items.push(new); } - } - } - builder.with_update_history(); + Ok(items) + }) + .await?; - builder.build().map_err(|err| Error::InvalidData { - details: format!("when rebuilding a linked chunk: {err}"), - }) + Ok(result) } async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> { @@ -931,7 +897,7 @@ mod tests { .await .unwrap(); - let mut chunks = store.load_chunks(room_id).await.unwrap(); + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert_eq!(chunks.len(), 3); @@ -982,7 +948,7 @@ mod tests { .await .unwrap(); - let mut chunks = store.load_chunks(room_id).await.unwrap(); + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert_eq!(chunks.len(), 1); @@ -1030,7 +996,7 @@ mod tests { .await .unwrap(); - let mut chunks = store.load_chunks(room_id).await.unwrap(); + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert_eq!(chunks.len(), 2); @@ -1103,7 +1069,7 @@ mod tests { .await .unwrap(); - let mut chunks = store.load_chunks(room_id).await.unwrap(); + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert_eq!(chunks.len(), 1); @@ -1148,7 +1114,7 @@ mod tests { .await .unwrap(); - let mut chunks = store.load_chunks(room_id).await.unwrap(); + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert_eq!(chunks.len(), 1); @@ -1207,7 +1173,7 @@ mod tests { .await .unwrap(); - let mut chunks = store.load_chunks(room_id).await.unwrap(); + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert_eq!(chunks.len(), 1); @@ -1254,7 +1220,7 @@ mod tests { .await .unwrap(); - let mut chunks = store.load_chunks(room_id).await.unwrap(); + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert_eq!(chunks.len(), 1); @@ -1305,7 +1271,7 @@ mod tests { .await .unwrap(); - let chunks = store.load_chunks(room_id).await.unwrap(); + let chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert!(chunks.is_empty()); } @@ -1359,7 +1325,7 @@ mod tests { .unwrap(); // Check chunks from room 1. - let mut chunks_room1 = store.load_chunks(room1).await.unwrap(); + let mut chunks_room1 = store.reload_linked_chunk(room1).await.unwrap(); assert_eq!(chunks_room1.len(), 1); let c = chunks_room1.remove(0); @@ -1370,7 +1336,7 @@ mod tests { }); // Check chunks from room 2. - let mut chunks_room2 = store.load_chunks(room2).await.unwrap(); + let mut chunks_room2 = store.reload_linked_chunk(room2).await.unwrap(); assert_eq!(chunks_room2.len(), 1); let c = chunks_room2.remove(0); @@ -1415,7 +1381,7 @@ mod tests { // If the updates have been handled transactionally, then no new chunks should // have been added; failure of the second update leads to the first one being // rolled back. - let chunks = store.load_chunks(room_id).await.unwrap(); + let chunks = store.reload_linked_chunk(room_id).await.unwrap(); assert!(chunks.is_empty()); } } diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 57a504a987d..ee9586bedfa 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -573,8 +573,8 @@ mod private { use matrix_sdk_base::{ deserialized_responses::{SyncTimelineEvent, TimelineEventKind}, - event_cache::store::EventCacheStoreLock, - linked_chunk::Update, + event_cache::store::{EventCacheStoreError, EventCacheStoreLock}, + linked_chunk::{LinkedChunkBuilder, Update}, }; use once_cell::sync::OnceCell; use ruma::{serde::Raw, OwnedRoomId}; @@ -614,8 +614,15 @@ mod private { ) -> Result { let events = if let Some(store) = store.get() { let locked = store.lock().await?; - let chunks = locked.reload_linked_chunk(&room).await?; - RoomEvents::with_initial_chunks(chunks) + let raw_chunks = locked.reload_linked_chunk(&room).await?; + + let mut builder = LinkedChunkBuilder::from_raw_parts(raw_chunks); + builder.with_update_history(); + let linked_chunk = builder.build().map_err(|err| { + EventCacheStoreError::InvalidData { details: err.to_string() } + })?; + + RoomEvents::with_initial_chunks(linked_chunk) } else { RoomEvents::default() }; @@ -998,6 +1005,8 @@ mod tests { #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. #[async_test] async fn test_write_to_storage() { + use matrix_sdk_base::linked_chunk::LinkedChunkBuilder; + let room_id = room_id!("!galette:saucisse.bzh"); let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); @@ -1034,7 +1043,9 @@ mod tests { .await .unwrap(); - let linked_chunk = event_cache_store.reload_linked_chunk(room_id).await.unwrap().unwrap(); + let raws = event_cache_store.reload_linked_chunk(room_id).await.unwrap(); + let linked_chunk = + LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws).build().unwrap().unwrap(); assert_eq!(linked_chunk.chunks().count(), 3); @@ -1065,6 +1076,7 @@ mod tests { #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. #[async_test] async fn test_write_to_storage_strips_bundled_relations() { + use matrix_sdk_base::linked_chunk::LinkedChunkBuilder; use ruma::events::BundledMessageLikeRelations; let room_id = room_id!("!galette:saucisse.bzh"); @@ -1121,7 +1133,9 @@ mod tests { } // The one in storage does not. - let linked_chunk = event_cache_store.reload_linked_chunk(room_id).await.unwrap().unwrap(); + let raws = event_cache_store.reload_linked_chunk(room_id).await.unwrap(); + let linked_chunk = + LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws).build().unwrap().unwrap(); assert_eq!(linked_chunk.chunks().count(), 1); @@ -1144,6 +1158,8 @@ mod tests { #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. #[async_test] async fn test_clear() { + use matrix_sdk_base::linked_chunk::LinkedChunkBuilder; + use crate::{assert_let_timeout, event_cache::RoomEventCacheUpdate}; let room_id = room_id!("!galette:saucisse.bzh"); @@ -1244,11 +1260,13 @@ mod tests { assert!(items.is_empty()); // The event cache store too. - let reloaded = event_cache_store.reload_linked_chunk(room_id).await.unwrap(); + let raws = event_cache_store.reload_linked_chunk(room_id).await.unwrap(); + let linked_chunk = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws).build().unwrap(); + // Note: while the event cache store could return `None` here, clearing it will // reset it to its initial form, maintaining the invariant that it // contains a single items chunk that's empty. - let linked_chunk = reloaded.unwrap(); + let linked_chunk = linked_chunk.unwrap(); assert_eq!(linked_chunk.num_items(), 0); } From 832fedb05e14e893124886a4b70cabaf43f98a89 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 17:12:56 +0100 Subject: [PATCH 764/979] feat(event cache): display raw linked chunks from storage when they fail to be rebuilt --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 20 ++++ crates/matrix-sdk/src/event_cache/room/mod.rs | 100 +++++++++++++++++- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index a0cb1a820be..faf92ef2b09 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -1074,6 +1074,15 @@ pub enum ChunkContent { Items(Vec), } +impl Clone for ChunkContent { + fn clone(&self) -> Self { + match self { + Self::Gap(gap) => Self::Gap(gap.clone()), + Self::Items(items) => Self::Items(items.clone()), + } + } +} + /// A chunk is a node in the [`LinkedChunk`]. pub struct Chunk { /// The previous chunk. @@ -1435,6 +1444,17 @@ pub struct RawLinkedChunk { pub next: Option, } +impl Clone for RawLinkedChunk { + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + previous: self.previous, + id: self.id, + next: self.next, + } + } +} + #[cfg(test)] mod tests { use std::{ diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index ee9586bedfa..c3f155611fb 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -573,11 +573,15 @@ mod private { use matrix_sdk_base::{ deserialized_responses::{SyncTimelineEvent, TimelineEventKind}, - event_cache::store::{EventCacheStoreError, EventCacheStoreLock}, - linked_chunk::{LinkedChunkBuilder, Update}, + event_cache::{ + store::{EventCacheStoreError, EventCacheStoreLock}, + Event, Gap, + }, + linked_chunk::{ChunkContent, LinkedChunkBuilder, RawLinkedChunk, Update}, }; use once_cell::sync::OnceCell; use ruma::{serde::Raw, OwnedRoomId}; + use tracing::trace; use super::events::RoomEvents; use crate::event_cache::EventCacheError; @@ -616,9 +620,19 @@ mod private { let locked = store.lock().await?; let raw_chunks = locked.reload_linked_chunk(&room).await?; - let mut builder = LinkedChunkBuilder::from_raw_parts(raw_chunks); + let mut builder = LinkedChunkBuilder::from_raw_parts(raw_chunks.clone()); + builder.with_update_history(); + let linked_chunk = builder.build().map_err(|err| { + // Show a debug string representing the known chunks. + if tracing::enabled!(tracing::Level::TRACE) { + trace!("couldn't build a linked chunk from the raw parts. Raw chunks:"); + for line in raw_chunks_debug_string(raw_chunks) { + trace!("{line}"); + } + } + EventCacheStoreError::InvalidData { details: err.to_string() } })?; @@ -735,6 +749,86 @@ mod private { Ok(output) } } + + /// Create a debug string over multiple lines (one String per line), + /// offering a debug representation of a [`RawLinkedChunk`] loaded from + /// disk. + fn raw_chunks_debug_string(mut raw_chunks: Vec>) -> Vec { + let mut result = Vec::new(); + + // Sort the chunks by id, for the output to be deterministic. + raw_chunks.sort_by_key(|c| c.id.index()); + + for c in raw_chunks { + let content = match c.content { + ChunkContent::Gap(Gap { prev_token }) => { + format!("gap['{prev_token}']") + } + ChunkContent::Items(vec) => { + let items = vec + .into_iter() + .map(|event| { + event + .event_id() + .map_or_else(|| "".to_owned(), |id| id.to_string()) + }) + .collect::>() + .join(", "); + format!("events[{items}]") + } + }; + + let prev = + c.previous.map_or_else(|| "".to_owned(), |prev| prev.index().to_string()); + let next = c.next.map_or_else(|| "".to_owned(), |prev| prev.index().to_string()); + + let line = format!("chunk #{} (prev={prev}, next={next}): {content}", c.id.index()); + + result.push(line); + } + + result + } + + #[cfg(test)] + mod tests { + use matrix_sdk_base::{ + event_cache::Gap, + linked_chunk::{ChunkContent, ChunkIdentifier as CId, RawLinkedChunk}, + }; + use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; + use ruma::event_id; + + use super::raw_chunks_debug_string; + + #[test] + fn test_raw_chunks_debug_string() { + let mut raws = Vec::new(); + let f = EventFactory::new().room(&DEFAULT_TEST_ROOM_ID).sender(*ALICE); + + raws.push(RawLinkedChunk { + content: ChunkContent::Items(vec![ + f.text_msg("hey").event_id(event_id!("$1")).into_sync(), + f.text_msg("you").event_id(event_id!("$2")).into_sync(), + ]), + id: CId::new(1), + previous: Some(CId::new(0)), + next: None, + }); + + raws.push(RawLinkedChunk { + content: ChunkContent::Gap(Gap { prev_token: "prev-token".to_owned() }), + id: CId::new(0), + previous: None, + next: Some(CId::new(1)), + }); + + let output = raw_chunks_debug_string(raws); + assert_eq!(output.len(), 2); + assert_eq!(&output[0], "chunk #0 (prev=, next=1): gap['prev-token']"); + assert_eq!(&output[1], "chunk #1 (prev=0, next=): events[$1, $2]"); + } + } } pub(super) use private::RoomEventCacheState; From 20184552a8a99b669f807aa4a0d6fb9f249b9f96 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 9 Dec 2024 17:22:39 +0100 Subject: [PATCH 765/979] feat(event cache): start with an empty linked chunk if reloading failed --- crates/matrix-sdk/src/event_cache/room/mod.rs | 115 +++++++++++++++--- 1 file changed, 98 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index c3f155611fb..2d7b69abca7 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -574,14 +574,17 @@ mod private { use matrix_sdk_base::{ deserialized_responses::{SyncTimelineEvent, TimelineEventKind}, event_cache::{ - store::{EventCacheStoreError, EventCacheStoreLock}, + store::{ + EventCacheStoreError, EventCacheStoreLock, EventCacheStoreLockGuard, + DEFAULT_CHUNK_CAPACITY, + }, Event, Gap, }, - linked_chunk::{ChunkContent, LinkedChunkBuilder, RawLinkedChunk, Update}, + linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, RawLinkedChunk, Update}, }; use once_cell::sync::OnceCell; - use ruma::{serde::Raw, OwnedRoomId}; - use tracing::trace; + use ruma::{serde::Raw, OwnedRoomId, RoomId}; + use tracing::{error, trace}; use super::events::RoomEvents; use crate::event_cache::EventCacheError; @@ -611,6 +614,30 @@ mod private { } impl RoomEventCacheState { + async fn try_reload_linked_chunk( + room: &RoomId, + locked: &EventCacheStoreLockGuard<'_>, + ) -> Result>, EventCacheError> + { + let raw_chunks = locked.reload_linked_chunk(room).await?; + + let mut builder = LinkedChunkBuilder::from_raw_parts(raw_chunks.clone()); + + builder.with_update_history(); + + Ok(builder.build().map_err(|err| { + // Show a debug string representing the known chunks. + if tracing::enabled!(tracing::Level::TRACE) { + trace!("couldn't build a linked chunk from the raw parts. Raw chunks:"); + for line in raw_chunks_debug_string(raw_chunks) { + trace!("{line}"); + } + } + + EventCacheStoreError::InvalidData { details: err.to_string() } + })?) + } + /// Create a new state, or reload it from storage if it's been enabled. pub async fn new( room: OwnedRoomId, @@ -618,23 +645,21 @@ mod private { ) -> Result { let events = if let Some(store) = store.get() { let locked = store.lock().await?; - let raw_chunks = locked.reload_linked_chunk(&room).await?; - let mut builder = LinkedChunkBuilder::from_raw_parts(raw_chunks.clone()); + // Try to reload a linked chunk from storage. If it fails, log the error and + // restart with a fresh, empty linked chunk. + let linked_chunk = match Self::try_reload_linked_chunk(&room, &locked).await { + Ok(linked_chunk) => linked_chunk, + Err(err) => { + error!("error when reloading a linked chunk from memory: {err}"); - builder.with_update_history(); + // Clear storage for this room. + locked.handle_linked_chunk_updates(&room, vec![Update::Clear]).await?; - let linked_chunk = builder.build().map_err(|err| { - // Show a debug string representing the known chunks. - if tracing::enabled!(tracing::Level::TRACE) { - trace!("couldn't build a linked chunk from the raw parts. Raw chunks:"); - for line in raw_chunks_debug_string(raw_chunks) { - trace!("{line}"); - } + // Restart with an empty linked chunk. + None } - - EventCacheStoreError::InvalidData { details: err.to_string() } - })?; + }; RoomEvents::with_initial_chunks(linked_chunk) } else { @@ -1465,6 +1490,62 @@ mod tests { assert_eq!(items[1].event_id().unwrap(), event_id1); } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. + #[async_test] + async fn test_load_from_storage_resilient_to_failure() { + let room_id = room_id!("!galette:saucisse.bzh"); + let event_cache_store = Arc::new(MemoryStore::new()); + + // Prefill the store with invalid data: two chunks that form a cycle. + event_cache_store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + Update::NewItemsChunk { + previous: Some(ChunkIdentifier::new(0)), + new: ChunkIdentifier::new(1), + next: Some(ChunkIdentifier::new(0)), + }, + ], + ) + .await + .unwrap(); + + let client = MockClientBuilder::new("http://localhost".to_owned()) + .store_config( + StoreConfig::new("holder".to_owned()).event_cache_store(event_cache_store.clone()), + ) + .build() + .await; + + let event_cache = client.event_cache(); + + // Don't forget to subscribe and like^W enable storage! + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (items, _stream) = room_event_cache.subscribe().await.unwrap(); + + // Because the persisted content was invalid, the room store is reset: there are + // no events in the cache. + assert!(items.is_empty()); + + // Storage doesn't contain anything. It would also be valid that it contains a + // single initial empty items chunk. + let raw_chunks = event_cache_store.reload_linked_chunk(room_id).await.unwrap(); + assert!(raw_chunks.is_empty()); + } + async fn assert_relations( room_id: &RoomId, original_event: SyncTimelineEvent, From 4402f59e74e2bc192d6ef73da24daa2f63f89652 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 10 Dec 2024 12:16:32 +0100 Subject: [PATCH 766/979] refactor(event cache): spawn a task to handle updates to the event cache store --- crates/matrix-sdk/src/event_cache/room/mod.rs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 2d7b69abca7..e31f3310c8e 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -723,8 +723,6 @@ mod private { if !updates.is_empty() { if let Some(store) = self.store.get() { - let locked = store.lock().await?; - // Strip relations from the `PushItems` updates. for up in updates.iter_mut() { match up { @@ -743,7 +741,28 @@ mod private { } } - locked.handle_linked_chunk_updates(&self.room, updates).await?; + // Spawn a task to make sure that all the changes are effectively forwarded to + // the store, even if the call to this method gets aborted. + // + // The store cross-process locking involves an actual mutex, which ensures that + // storing updates happens in the expected order. + + let store = store.clone(); + let room_id = self.room.clone(); + + matrix_sdk_common::executor::spawn(async move { + let locked = store.lock().await?; + + if let Err(err) = + locked.handle_linked_chunk_updates(&room_id, updates).await + { + error!("unable to handle linked chunk updates: {err}"); + } + + super::Result::Ok(()) + }) + .await + .expect("joining failed")?; } } From 925d10f2ffe67b08e570bb6ee3a11f2971c07263 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 10 Dec 2024 14:44:42 +0100 Subject: [PATCH 767/979] task(event cache store): include the number of added items in one log --- crates/matrix-sdk-sqlite/src/event_cache_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index be255445d20..f78e3c1c8c8 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -461,7 +461,7 @@ impl EventCacheStore for SqliteEventCacheStore { Update::PushItems { at, items } => { let chunk_id = at.chunk_identifier().index(); - trace!(%room_id, "pushing items @ {chunk_id}"); + trace!(%room_id, "pushing {} items @ {chunk_id}", items.len()); for (i, event) in items.into_iter().enumerate() { let serialized = serde_json::to_vec(&event)?; From d42c449612aaeee707c249add489553b35cf3c90 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 10 Dec 2024 14:55:52 +0100 Subject: [PATCH 768/979] refactor(event cache): only store a prev-batch token if the timeline was limited --- .../tests/integration/timeline/pagination.rs | 10 +- crates/matrix-sdk/src/event_cache/room/mod.rs | 104 +++++++++++++++++- .../tests/integration/event_cache.rs | 56 ++++++---- 3 files changed, 142 insertions(+), 28 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs index 06189a3531f..3a2c0aeaa54 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs @@ -273,7 +273,8 @@ async fn test_wait_for_token() { &ALICE, RoomMessageEventContent::text_plain("live event!"), )) - .set_timeline_prev_batch(from.to_owned()), + .set_timeline_prev_batch(from.to_owned()) + .set_timeline_limited(), ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; @@ -385,7 +386,8 @@ async fn test_timeline_reset_while_paginating() { &ALICE, RoomMessageEventContent::text_plain("live event!"), )) - .set_timeline_prev_batch("pagination_1".to_owned()), + .set_timeline_prev_batch("pagination_1".to_owned()) + .set_timeline_limited(), ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; client.sync_once(sync_settings.clone()).await.unwrap(); @@ -399,8 +401,8 @@ async fn test_timeline_reset_while_paginating() { &BOB, RoomMessageEventContent::text_plain("new live event."), )) - .set_timeline_limited() - .set_timeline_prev_batch("pagination_2".to_owned()), + .set_timeline_prev_batch("pagination_2".to_owned()) + .set_timeline_limited(), ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index e31f3310c8e..626230d842d 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -349,9 +349,14 @@ impl RoomEventCacheInner { // Add all the events to the backend. trace!("adding new events"); + // Only keep the previous-batch token if we have a limited timeline; otherwise, + // we know about all the events, and we don't need to back-paginate, + // so we wouldn't make use of the given previous-batch token. + let prev_batch = if timeline.limited { timeline.prev_batch } else { None }; + self.append_new_events( timeline.events, - timeline.prev_batch, + prev_batch, ephemeral_events, ambiguity_changes, ) @@ -1170,7 +1175,7 @@ mod tests { // Propagate an update for a message and a prev-batch token. let timeline = Timeline { - limited: false, + limited: true, prev_batch: Some("raclette".to_owned()), events: vec![f.text_msg("hey yo").sender(*ALICE).into_sync()], }; @@ -1565,6 +1570,101 @@ mod tests { assert!(raw_chunks.is_empty()); } + #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. + #[async_test] + async fn test_no_useless_gaps() { + let room_id = room_id!("!galette:saucisse.bzh"); + + let client = MockClientBuilder::new("http://localhost".to_owned()).build().await; + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let has_storage = true; // for testing purposes only + event_cache.enable_storage().unwrap(); + + client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined); + let room = client.get_room(room_id).unwrap(); + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let f = EventFactory::new().room(room_id).sender(*ALICE); + + // Propagate an update including a limited timeline with one message and a + // prev-batch token. + room_event_cache + .inner + .handle_joined_room_update( + has_storage, + JoinedRoomUpdate { + timeline: Timeline { + limited: true, + prev_batch: Some("raclette".to_owned()), + events: vec![f.text_msg("hey yo").into_sync()], + }, + ..Default::default() + }, + ) + .await + .unwrap(); + + { + let state = room_event_cache.inner.state.read().await; + + let mut num_gaps = 0; + let mut num_events = 0; + + for c in state.events().chunks() { + match c.content() { + ChunkContent::Items(items) => num_events += items.len(), + ChunkContent::Gap(_) => num_gaps += 1, + } + } + + // The gap must have been stored. + assert_eq!(num_gaps, 1); + assert_eq!(num_events, 1); + } + + // Now, propagate an update for another message, but the timeline isn't limited + // this time. + room_event_cache + .inner + .handle_joined_room_update( + has_storage, + JoinedRoomUpdate { + timeline: Timeline { + limited: false, + prev_batch: Some("fondue".to_owned()), + events: vec![f.text_msg("sup").into_sync()], + }, + ..Default::default() + }, + ) + .await + .unwrap(); + + { + let state = room_event_cache.inner.state.read().await; + + let mut num_gaps = 0; + let mut num_events = 0; + + for c in state.events().chunks() { + match c.content() { + ChunkContent::Items(items) => num_events += items.len(), + ChunkContent::Gap(gap) => { + assert_eq!(gap.prev_token, "raclette"); + num_gaps += 1; + } + } + } + + // There's only the previous gap, no new ones. + assert_eq!(num_gaps, 1); + assert_eq!(num_events, 2); + } + } + async fn assert_relations( room_id: &RoomId, original_event: SyncTimelineEvent, diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index c593f8e96e5..95490990741 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1,8 +1,10 @@ use std::{future::ready, ops::ControlFlow, time::Duration}; use assert_matches::assert_matches; +use futures_util::FutureExt as _; use matrix_sdk::{ assert_let_timeout, assert_next_matches_with_timeout, + deserialized_responses::SyncTimelineEvent, event_cache::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, @@ -15,7 +17,7 @@ use matrix_sdk_test::{ }; use ruma::{event_id, room_id, user_id}; use serde_json::json; -use tokio::spawn; +use tokio::{spawn, sync::broadcast}; use wiremock::ResponseTemplate; use crate::mock_sync; @@ -205,6 +207,23 @@ async fn test_ignored_unignored() { assert!(subscriber.is_empty()); } +/// Small helper for backpagination tests, to wait for things to stabilize. +async fn wait_for_initial_events( + events: Vec, + room_stream: &mut broadcast::Receiver, +) { + if events.is_empty() { + let mut update = room_stream.recv().await.expect("read error"); + // Could be a clear because of the limited timeline. + if matches!(update, RoomEventCacheUpdate::Clear) { + update = room_stream.recv().await.expect("read error"); + } + assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + } else { + assert_eq!(events.len(), 1); + } +} + #[async_test] async fn test_backpaginate_once() { let server = MatrixMockServer::new().await; @@ -226,8 +245,9 @@ async fn test_backpaginate_once() { JoinedRoomBuilder::new(room_id) // Note to self: a timeline must have at least single event to be properly // serialized. - .add_timeline_event(f.text_msg("heyo")) - .set_timeline_prev_batch("prev_batch".to_owned()), + .add_timeline_event(f.text_msg("heyo").event_id(event_id!("$1"))) + .set_timeline_prev_batch("prev_batch".to_owned()) + .set_timeline_limited(), ) .await; @@ -238,11 +258,7 @@ async fn test_backpaginate_once() { // This is racy: either the initial message has been processed by the event // cache (and no room updates will happen in this case), or it hasn't, and // the stream will return the next message soon. - if events.is_empty() { - let _ = room_stream.recv().await.expect("read error"); - } else { - assert_eq!(events.len(), 1); - } + wait_for_initial_events(events, &mut room_stream).await; let outcome = { // Note: events must be presented in reversed order, since this is @@ -279,7 +295,8 @@ async fn test_backpaginate_once() { assert_event_matches_msg(&events[1], "hello"); assert_eq!(events.len(), 2); - assert!(room_stream.is_empty()); + let next = room_stream.recv().now_or_never(); + assert_matches!(next, None); } #[async_test] @@ -305,7 +322,8 @@ async fn test_backpaginate_many_times_with_many_iterations() { // Note to self: a timeline must have at least single event to be properly // serialized. .add_timeline_event(f.text_msg("heyo")) - .set_timeline_prev_batch("prev_batch".to_owned()), + .set_timeline_prev_batch("prev_batch".to_owned()) + .set_timeline_limited(), ) .await; @@ -316,11 +334,7 @@ async fn test_backpaginate_many_times_with_many_iterations() { // This is racy: either the initial message has been processed by the event // cache (and no room updates will happen in this case), or it hasn't, and // the stream will return the next message soon. - if events.is_empty() { - let _ = room_stream.recv().await.expect("read error"); - } else { - assert_eq!(events.len(), 1); - } + wait_for_initial_events(events, &mut room_stream).await; let mut num_iterations = 0; let mut num_paginations = 0; @@ -426,7 +440,8 @@ async fn test_backpaginate_many_times_with_one_iteration() { // Note to self: a timeline must have at least single event to be properly // serialized. .add_timeline_event(f.text_msg("heyo")) - .set_timeline_prev_batch("prev_batch".to_owned()), + .set_timeline_prev_batch("prev_batch".to_owned()) + .set_timeline_limited(), ) .await; @@ -438,11 +453,7 @@ async fn test_backpaginate_many_times_with_one_iteration() { // This is racy: either the initial message has been processed by the event // cache (and no room updates will happen in this case), or it hasn't, and // the stream will return the next message soon. - if events.is_empty() { - let _ = room_stream.recv().await.expect("read error"); - } else { - assert_eq!(events.len(), 1); - } + wait_for_initial_events(events, &mut room_stream).await; let mut num_iterations = 0; let mut num_paginations = 0; @@ -552,7 +563,8 @@ async fn test_reset_while_backpaginating() { // Note to self: a timeline must have at least single event to be properly // serialized. .add_timeline_event(f.text_msg("heyo").into_raw_sync()) - .set_timeline_prev_batch("first_backpagination".to_owned()), + .set_timeline_prev_batch("first_backpagination".to_owned()) + .set_timeline_limited(), ) .await; From 0264e499687ddbe2d7c0d92cd5611d070e45185b Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 11 Dec 2024 11:49:07 +0100 Subject: [PATCH 769/979] task(event cache): rename a few things - rename RawLinkedChunk -> RawChunk - rename RawChunk::id -> RawChunk::identifier - precise that a `RawChunk` is mostly a `Chunk` with different previous/next links. --- .../event_cache/store/integration_tests.rs | 8 +--- .../src/event_cache/store/memory_store.rs | 4 +- .../src/event_cache/store/traits.rs | 6 +-- .../src/linked_chunk/builder.rs | 8 ++-- .../matrix-sdk-common/src/linked_chunk/mod.rs | 32 ++++----------- .../src/linked_chunk/relational.rs | 19 ++++----- .../src/event_cache_store.rs | 39 +++++++++++-------- crates/matrix-sdk/src/event_cache/room/mod.rs | 22 +++++------ 8 files changed, 60 insertions(+), 78 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index 10651d446d9..de2174c2f0a 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -21,9 +21,7 @@ use matrix_sdk_common::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind, VerificationState, }, - linked_chunk::{ - ChunkContent, LinkedChunk, LinkedChunkBuilder, Position, RawLinkedChunk, Update, - }, + linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, Position, RawChunk, Update}, }; use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use ruma::{ @@ -121,9 +119,7 @@ pub trait EventCacheStoreIntegrationTests { async fn test_clear_all_rooms_chunks(&self); } -fn rebuild_linked_chunk( - raws: Vec>, -) -> Option> { +fn rebuild_linked_chunk(raws: Vec>) -> Option> { LinkedChunkBuilder::from_raw_parts(raws).build().unwrap() } diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 02aeba58fe1..60ce6806d46 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -16,7 +16,7 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::{relational::RelationalLinkedChunk, RawLinkedChunk, Update}, + linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update}, ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; @@ -96,7 +96,7 @@ impl EventCacheStore for MemoryStore { async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { let inner = self.inner.read().unwrap(); inner .events diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 4251eb8987e..5eb66d5fbba 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -16,7 +16,7 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::{RawLinkedChunk, Update}, + linked_chunk::{RawChunk, Update}, AsyncTraitDeps, }; use ruma::{MxcUri, RoomId}; @@ -62,7 +62,7 @@ pub trait EventCacheStore: AsyncTraitDeps { async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error>; + ) -> Result>, Self::Error>; /// Clear persisted events for all the rooms. /// @@ -192,7 +192,7 @@ impl EventCacheStore for EraseEventCacheStoreError { async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { self.0.reload_linked_chunk(room_id).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-common/src/linked_chunk/builder.rs b/crates/matrix-sdk-common/src/linked_chunk/builder.rs index 0bcb3e20f31..81459e2d605 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/builder.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/builder.rs @@ -21,7 +21,7 @@ use tracing::error; use super::{ Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk, - ObservableUpdates, RawLinkedChunk, + ObservableUpdates, RawChunk, }; /// A temporary chunk representation in the [`LinkedChunkBuilder`]. @@ -262,15 +262,15 @@ impl LinkedChunkBuilder { } /// Fills a linked chunk builder from all the given raw parts. - pub fn from_raw_parts(raws: Vec>) -> Self { + pub fn from_raw_parts(raws: Vec>) -> Self { let mut this = Self::new(); for raw in raws { match raw.content { ChunkContent::Gap(gap) => { - this.push_gap(raw.previous, raw.id, raw.next, gap); + this.push_gap(raw.previous, raw.identifier, raw.next, gap); } ChunkContent::Items(vec) => { - this.push_items(raw.previous, raw.id, raw.next, vec); + this.push_items(raw.previous, raw.identifier, raw.next, vec); } } } diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index faf92ef2b09..6a67383b014 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -1064,7 +1064,7 @@ impl<'a, const CAP: usize, Item, Gap> Iterator for Iter<'a, CAP, Item, Gap> { } /// This enum represents the content of a [`Chunk`]. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum ChunkContent { /// The chunk represents a gap in the linked chunk, i.e. a hole. It /// means that some items are missing in this location. @@ -1074,15 +1074,6 @@ pub enum ChunkContent { Items(Vec), } -impl Clone for ChunkContent { - fn clone(&self) -> Self { - match self { - Self::Gap(gap) => Self::Gap(gap.clone()), - Self::Items(items) => Self::Items(items.clone()), - } - } -} - /// A chunk is a node in the [`LinkedChunk`]. pub struct Chunk { /// The previous chunk. @@ -1429,8 +1420,12 @@ impl EmptyChunk { } /// The raw representation of a linked chunk, as persisted in storage. -#[derive(Debug)] -pub struct RawLinkedChunk { +/// +/// It may rebuilt into [`Chunk`] and shares the same internal representation, +/// except that links are materialized using [`ChunkIdentifier`] instead of raw +/// pointers to the previous and next chunks. +#[derive(Clone, Debug)] +pub struct RawChunk { /// Content section of the linked chunk. pub content: ChunkContent, @@ -1438,23 +1433,12 @@ pub struct RawLinkedChunk { pub previous: Option, /// Current chunk's identifier. - pub id: ChunkIdentifier, + pub identifier: ChunkIdentifier, /// Link to the next chunk, via its identifier. pub next: Option, } -impl Clone for RawLinkedChunk { - fn clone(&self) -> Self { - Self { - content: self.content.clone(), - previous: self.previous, - id: self.id, - next: self.next, - } - } -} - #[cfg(test)] mod tests { use std::{ diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index e823e927cfc..ec3e297de48 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -17,7 +17,7 @@ use ruma::{OwnedRoomId, RoomId}; -use super::{ChunkContent, RawLinkedChunk}; +use super::{ChunkContent, RawChunk}; use crate::linked_chunk::{ChunkIdentifier, Position, Update}; /// A row of the [`RelationalLinkedChunk::chunks`]. @@ -290,10 +290,7 @@ where /// /// Return an error result if the data was malformed in the struct, with a /// string message explaining details about the error. - pub fn reload_chunks( - &self, - room_id: &RoomId, - ) -> Result>, String> { + pub fn reload_chunks(&self, room_id: &RoomId) -> Result>, String> { let mut result = Vec::new(); for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) { @@ -310,10 +307,10 @@ where let Some(first) = items.peek() else { // The only possibility is that we created an empty items chunk; mark it as // such, and continue. - result.push(RawLinkedChunk { + result.push(RawChunk { content: ChunkContent::Items(Vec::new()), previous: chunk_row.previous_chunk, - id: chunk_row.chunk, + identifier: chunk_row.chunk, next: chunk_row.next_chunk, }); continue; @@ -340,12 +337,12 @@ where // Sort them by their position. collected_items.sort_unstable_by_key(|(_item, index)| *index); - result.push(RawLinkedChunk { + result.push(RawChunk { content: ChunkContent::Items( collected_items.into_iter().map(|(item, _index)| item).collect(), ), previous: chunk_row.previous_chunk, - id: chunk_row.chunk, + identifier: chunk_row.chunk, next: chunk_row.next_chunk, }); } @@ -361,10 +358,10 @@ where )); } - result.push(RawLinkedChunk { + result.push(RawChunk { content: ChunkContent::Gap(gap.clone()), previous: chunk_row.previous_chunk, - id: chunk_row.chunk, + identifier: chunk_row.chunk, next: chunk_row.next_chunk, }); } diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index f78e3c1c8c8..33baf630b41 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -22,7 +22,7 @@ use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ event_cache::{store::EventCacheStore, Event, Gap}, - linked_chunk::{ChunkContent, ChunkIdentifier, RawLinkedChunk, Update}, + linked_chunk::{ChunkContent, ChunkIdentifier, RawChunk, Update}, media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; @@ -148,7 +148,7 @@ impl SqliteEventCacheStore { &self, room_id: &RoomId, chunk_id: ChunkIdentifier, - ) -> Result> { + ) -> Result> { let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, room_id); let this = self.clone(); @@ -176,7 +176,7 @@ trait TransactionExtForLinkedChunks { index: u64, next: Option, chunk_type: &str, - ) -> Result>; + ) -> Result>; fn load_gap_content( &self, @@ -202,7 +202,7 @@ impl TransactionExtForLinkedChunks for Transaction<'_> { id: u64, next: Option, chunk_type: &str, - ) -> Result> { + ) -> Result> { let previous = previous.map(ChunkIdentifier::new); let next = next.map(ChunkIdentifier::new); let id = ChunkIdentifier::new(id); @@ -212,13 +212,18 @@ impl TransactionExtForLinkedChunks for Transaction<'_> { // It's a gap! There's at most one row for it in the database, so a // call to `query_row` is sufficient. let gap = self.load_gap_content(store, room_id, id)?; - Ok(RawLinkedChunk { content: ChunkContent::Gap(gap), previous, id, next }) + Ok(RawChunk { content: ChunkContent::Gap(gap), previous, identifier: id, next }) } CHUNK_TYPE_EVENT_TYPE_STRING => { // It's events! let events = self.load_events_content(store, room_id, id)?; - Ok(RawLinkedChunk { content: ChunkContent::Items(events), previous, id, next }) + Ok(RawChunk { + content: ChunkContent::Items(events), + previous, + identifier: id, + next, + }) } other => { @@ -537,7 +542,7 @@ impl EventCacheStore for SqliteEventCacheStore { async fn reload_linked_chunk( &self, room_id: &RoomId, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { let room_id = room_id.to_owned(); let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, &room_id); @@ -904,7 +909,7 @@ mod tests { { // Chunks are ordered from smaller to bigger IDs. let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(13)); + assert_eq!(c.identifier, ChunkIdentifier::new(13)); assert_eq!(c.previous, Some(ChunkIdentifier::new(42))); assert_eq!(c.next, Some(ChunkIdentifier::new(37))); assert_matches!(c.content, ChunkContent::Items(events) => { @@ -912,7 +917,7 @@ mod tests { }); let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(37)); + assert_eq!(c.identifier, ChunkIdentifier::new(37)); assert_eq!(c.previous, Some(ChunkIdentifier::new(13))); assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { @@ -920,7 +925,7 @@ mod tests { }); let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); assert_eq!(c.previous, None); assert_eq!(c.next, Some(ChunkIdentifier::new(13))); assert_matches!(c.content, ChunkContent::Items(events) => { @@ -954,7 +959,7 @@ mod tests { // Chunks are ordered from smaller to bigger IDs. let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); assert_eq!(c.previous, None); assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Gap(gap) => { @@ -1002,7 +1007,7 @@ mod tests { // Chunks are ordered from smaller to bigger IDs. let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); assert_eq!(c.previous, None); assert_eq!(c.next, Some(ChunkIdentifier::new(44))); assert_matches!(c.content, ChunkContent::Gap(gap) => { @@ -1010,7 +1015,7 @@ mod tests { }); let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(44)); + assert_eq!(c.identifier, ChunkIdentifier::new(44)); assert_eq!(c.previous, Some(ChunkIdentifier::new(42))); assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Gap(gap) => { @@ -1074,7 +1079,7 @@ mod tests { assert_eq!(chunks.len(), 1); let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); assert_eq!(c.previous, None); assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { @@ -1119,7 +1124,7 @@ mod tests { assert_eq!(chunks.len(), 1); let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); assert_eq!(c.previous, None); assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { @@ -1178,7 +1183,7 @@ mod tests { assert_eq!(chunks.len(), 1); let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); assert_eq!(c.previous, None); assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { @@ -1225,7 +1230,7 @@ mod tests { assert_eq!(chunks.len(), 1); let c = chunks.remove(0); - assert_eq!(c.id, ChunkIdentifier::new(42)); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); assert_eq!(c.previous, None); assert_eq!(c.next, None); assert_matches!(c.content, ChunkContent::Items(events) => { diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 626230d842d..21e29594b8f 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -585,7 +585,7 @@ mod private { }, Event, Gap, }, - linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, RawLinkedChunk, Update}, + linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, RawChunk, Update}, }; use once_cell::sync::OnceCell; use ruma::{serde::Raw, OwnedRoomId, RoomId}; @@ -800,13 +800,12 @@ mod private { } /// Create a debug string over multiple lines (one String per line), - /// offering a debug representation of a [`RawLinkedChunk`] loaded from - /// disk. - fn raw_chunks_debug_string(mut raw_chunks: Vec>) -> Vec { + /// offering a debug representation of a [`RawChunk`] loaded from disk. + fn raw_chunks_debug_string(mut raw_chunks: Vec>) -> Vec { let mut result = Vec::new(); // Sort the chunks by id, for the output to be deterministic. - raw_chunks.sort_by_key(|c| c.id.index()); + raw_chunks.sort_by_key(|c| c.identifier.index()); for c in raw_chunks { let content = match c.content { @@ -831,7 +830,8 @@ mod private { c.previous.map_or_else(|| "".to_owned(), |prev| prev.index().to_string()); let next = c.next.map_or_else(|| "".to_owned(), |prev| prev.index().to_string()); - let line = format!("chunk #{} (prev={prev}, next={next}): {content}", c.id.index()); + let line = + format!("chunk #{} (prev={prev}, next={next}): {content}", c.identifier.index()); result.push(line); } @@ -843,7 +843,7 @@ mod private { mod tests { use matrix_sdk_base::{ event_cache::Gap, - linked_chunk::{ChunkContent, ChunkIdentifier as CId, RawLinkedChunk}, + linked_chunk::{ChunkContent, ChunkIdentifier as CId, RawChunk}, }; use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use ruma::event_id; @@ -855,19 +855,19 @@ mod private { let mut raws = Vec::new(); let f = EventFactory::new().room(&DEFAULT_TEST_ROOM_ID).sender(*ALICE); - raws.push(RawLinkedChunk { + raws.push(RawChunk { content: ChunkContent::Items(vec![ f.text_msg("hey").event_id(event_id!("$1")).into_sync(), f.text_msg("you").event_id(event_id!("$2")).into_sync(), ]), - id: CId::new(1), + identifier: CId::new(1), previous: Some(CId::new(0)), next: None, }); - raws.push(RawLinkedChunk { + raws.push(RawChunk { content: ChunkContent::Gap(Gap { prev_token: "prev-token".to_owned() }), - id: CId::new(0), + identifier: CId::new(0), previous: None, next: Some(CId::new(1)), }); From fda374ee81dad3b803cc72d2fb79bd13f261bc5e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 11 Dec 2024 16:52:23 +0000 Subject: [PATCH 770/979] feat(ffi): Add new properties to `UnableToDecryptInfo` Followup to https://github.com/matrix-org/matrix-rust-sdk/pull/4360: expose the new properties via FFI --- bindings/matrix-sdk-ffi/src/sync_service.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/sync_service.rs b/bindings/matrix-sdk-ffi/src/sync_service.rs index 52179678ed7..edddb69625d 100644 --- a/bindings/matrix-sdk-ffi/src/sync_service.rs +++ b/bindings/matrix-sdk-ffi/src/sync_service.rs @@ -201,6 +201,22 @@ pub struct UnableToDecryptInfo { /// What we know about what caused this UTD. E.g. was this event sent when /// we were not a member of this room? pub cause: UtdCause, + + /// The difference between the event creation time (`origin_server_ts`) and + /// the time our device was created. If negative, this event was sent + /// *before* our device was created. + pub event_local_age_millis: i64, + + /// Whether the user had verified their own identity at the point they + /// received the UTD event. + pub user_trusts_own_identity: bool, + + /// The homeserver of the user that sent the undecryptable event. + pub sender_homeserver: String, + + /// Our local user's own homeserver, or `None` if the client is not logged + /// in. + pub own_homeserver: Option, } impl From for UnableToDecryptInfo { @@ -209,6 +225,10 @@ impl From for UnableToDecryptInfo { event_id: value.event_id.to_string(), time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64), cause: value.cause, + event_local_age_millis: value.event_local_age_millis, + user_trusts_own_identity: value.user_trusts_own_identity, + sender_homeserver: value.sender_homeserver.to_string(), + own_homeserver: value.own_homeserver.map(String::from), } } } From 3356e0cc824c843752a7cc1d7fcb494fb1496f48 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 11 Dec 2024 14:16:42 +0100 Subject: [PATCH 771/979] refactor(state store): use a single lock for all memory store accesses The `MemoryStore` implementation of the `StateStore` has grown into a monster, with one lock per field. It's probably overkill, as individual fields don't need fine-grained locks like this; after all, accesses to the store shouldn't be reentrant in general. Fixes #3720. --- .../matrix-sdk-base/src/store/memory_store.rs | 710 ++++++++---------- 1 file changed, 329 insertions(+), 381 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 2c8e1d84943..abc5fca09b4 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -14,7 +14,7 @@ use std::{ collections::{BTreeMap, BTreeSet, HashMap}, - sync::RwLock as StdRwLock, + sync::RwLock, }; use async_trait::async_trait; @@ -33,7 +33,7 @@ use ruma::{ CanonicalJsonObject, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId, }; -use tracing::{debug, instrument, trace, warn}; +use tracing::{debug, instrument, warn}; use super::{ send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey}, @@ -47,50 +47,49 @@ use crate::{ MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue, }; -/// In-memory, non-persistent implementation of the `StateStore`. -/// -/// Default if no other is configured at startup. -#[allow(clippy::type_complexity)] #[derive(Debug, Default)] -pub struct MemoryStore { - recently_visited_rooms: StdRwLock>>, - composer_drafts: StdRwLock>, - user_avatar_url: StdRwLock>, - sync_token: StdRwLock>, - server_capabilities: StdRwLock>, - filters: StdRwLock>, - utd_hook_manager_data: StdRwLock>, - account_data: StdRwLock>>, - profiles: StdRwLock>>, - display_names: StdRwLock>>>, - members: StdRwLock>>, - room_info: StdRwLock>, - room_state: StdRwLock< +#[allow(clippy::type_complexity)] +struct MemoryStoreInner { + recently_visited_rooms: HashMap>, + composer_drafts: HashMap, + user_avatar_url: HashMap, + sync_token: Option, + server_capabilities: Option, + filters: HashMap, + utd_hook_manager_data: Option, + account_data: HashMap>, + profiles: HashMap>, + display_names: HashMap>>, + members: HashMap>, + room_info: HashMap, + room_state: HashMap>>>, - >, - room_account_data: StdRwLock< + room_account_data: HashMap>>, - >, - stripped_room_state: StdRwLock< + stripped_room_state: HashMap>>>, + stripped_members: HashMap>, + presence: HashMap>, + room_user_receipts: HashMap< + OwnedRoomId, + HashMap<(String, Option), HashMap>, >, - stripped_members: StdRwLock>>, - presence: StdRwLock>>, - room_user_receipts: StdRwLock< - HashMap< - OwnedRoomId, - HashMap<(String, Option), HashMap>, - >, - >, - room_event_receipts: StdRwLock< - HashMap< - OwnedRoomId, - HashMap<(String, Option), HashMap>>, - >, + + room_event_receipts: HashMap< + OwnedRoomId, + HashMap<(String, Option), HashMap>>, >, - custom: StdRwLock, Vec>>, - send_queue_events: StdRwLock>>, - dependent_send_queue_events: StdRwLock>>, + custom: HashMap, Vec>, + send_queue_events: BTreeMap>, + dependent_send_queue_events: BTreeMap>, +} + +/// In-memory, non-persistent implementation of the `StateStore`. +/// +/// Default if no other is configured at startup. +#[derive(Debug, Default)] +pub struct MemoryStore { + inner: RwLock, } impl MemoryStore { @@ -106,9 +105,10 @@ impl MemoryStore { thread: ReceiptThread, user_id: &UserId, ) -> Option<(OwnedEventId, Receipt)> { - self.room_user_receipts + self.inner .read() .unwrap() + .room_user_receipts .get(room_id)? .get(&(receipt_type.to_string(), thread.as_str().map(ToOwned::to_owned)))? .get(user_id) @@ -123,9 +123,10 @@ impl MemoryStore { event_id: &EventId, ) -> Option> { Some( - self.room_event_receipts + self.inner .read() .unwrap() + .room_event_receipts .get(room_id)? .get(&(receipt_type.to_string(), thread.as_str().map(ToOwned::to_owned)))? .get(event_id)? @@ -142,50 +143,31 @@ impl StateStore for MemoryStore { type Error = StoreError; async fn get_kv_data(&self, key: StateStoreDataKey<'_>) -> Result> { + let inner = self.inner.read().unwrap(); Ok(match key { StateStoreDataKey::SyncToken => { - self.sync_token.read().unwrap().clone().map(StateStoreDataValue::SyncToken) + inner.sync_token.clone().map(StateStoreDataValue::SyncToken) } - StateStoreDataKey::ServerCapabilities => self - .server_capabilities - .read() - .unwrap() - .clone() - .map(StateStoreDataValue::ServerCapabilities), - StateStoreDataKey::Filter(filter_name) => self - .filters - .read() - .unwrap() - .get(filter_name) - .cloned() - .map(StateStoreDataValue::Filter), - StateStoreDataKey::UserAvatarUrl(user_id) => self - .user_avatar_url - .read() - .unwrap() - .get(user_id) - .cloned() - .map(StateStoreDataValue::UserAvatarUrl), - StateStoreDataKey::RecentlyVisitedRooms(user_id) => self + StateStoreDataKey::ServerCapabilities => { + inner.server_capabilities.clone().map(StateStoreDataValue::ServerCapabilities) + } + StateStoreDataKey::Filter(filter_name) => { + inner.filters.get(filter_name).cloned().map(StateStoreDataValue::Filter) + } + StateStoreDataKey::UserAvatarUrl(user_id) => { + inner.user_avatar_url.get(user_id).cloned().map(StateStoreDataValue::UserAvatarUrl) + } + StateStoreDataKey::RecentlyVisitedRooms(user_id) => inner .recently_visited_rooms - .read() - .unwrap() .get(user_id) .cloned() .map(StateStoreDataValue::RecentlyVisitedRooms), - StateStoreDataKey::UtdHookManagerData => self - .utd_hook_manager_data - .read() - .unwrap() - .clone() - .map(StateStoreDataValue::UtdHookManagerData), - StateStoreDataKey::ComposerDraft(room_id) => self - .composer_drafts - .read() - .unwrap() - .get(room_id) - .cloned() - .map(StateStoreDataValue::ComposerDraft), + StateStoreDataKey::UtdHookManagerData => { + inner.utd_hook_manager_data.clone().map(StateStoreDataValue::UtdHookManagerData) + } + StateStoreDataKey::ComposerDraft(room_id) => { + inner.composer_drafts.get(room_id).cloned().map(StateStoreDataValue::ComposerDraft) + } }) } @@ -194,25 +176,26 @@ impl StateStore for MemoryStore { key: StateStoreDataKey<'_>, value: StateStoreDataValue, ) -> Result<()> { + let mut inner = self.inner.write().unwrap(); match key { StateStoreDataKey::SyncToken => { - *self.sync_token.write().unwrap() = + inner.sync_token = Some(value.into_sync_token().expect("Session data not a sync token")) } StateStoreDataKey::Filter(filter_name) => { - self.filters.write().unwrap().insert( + inner.filters.insert( filter_name.to_owned(), value.into_filter().expect("Session data not a filter"), ); } StateStoreDataKey::UserAvatarUrl(user_id) => { - self.user_avatar_url.write().unwrap().insert( + inner.user_avatar_url.insert( user_id.to_owned(), value.into_user_avatar_url().expect("Session data not a user avatar url"), ); } StateStoreDataKey::RecentlyVisitedRooms(user_id) => { - self.recently_visited_rooms.write().unwrap().insert( + inner.recently_visited_rooms.insert( user_id.to_owned(), value .into_recently_visited_rooms() @@ -220,20 +203,20 @@ impl StateStore for MemoryStore { ); } StateStoreDataKey::UtdHookManagerData => { - *self.utd_hook_manager_data.write().unwrap() = Some( + inner.utd_hook_manager_data = Some( value .into_utd_hook_manager_data() .expect("Session data not the hook manager data"), ); } StateStoreDataKey::ComposerDraft(room_id) => { - self.composer_drafts.write().unwrap().insert( + inner.composer_drafts.insert( room_id.to_owned(), value.into_composer_draft().expect("Session data not a composer draft"), ); } StateStoreDataKey::ServerCapabilities => { - *self.server_capabilities.write().unwrap() = Some( + inner.server_capabilities = Some( value .into_server_capabilities() .expect("Session data not containing server capabilities"), @@ -245,25 +228,22 @@ impl StateStore for MemoryStore { } async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> { + let mut inner = self.inner.write().unwrap(); match key { - StateStoreDataKey::SyncToken => *self.sync_token.write().unwrap() = None, - StateStoreDataKey::ServerCapabilities => { - *self.server_capabilities.write().unwrap() = None - } + StateStoreDataKey::SyncToken => inner.sync_token = None, + StateStoreDataKey::ServerCapabilities => inner.server_capabilities = None, StateStoreDataKey::Filter(filter_name) => { - self.filters.write().unwrap().remove(filter_name); + inner.filters.remove(filter_name); } StateStoreDataKey::UserAvatarUrl(user_id) => { - self.user_avatar_url.write().unwrap().remove(user_id); + inner.user_avatar_url.remove(user_id); } StateStoreDataKey::RecentlyVisitedRooms(user_id) => { - self.recently_visited_rooms.write().unwrap().remove(user_id); - } - StateStoreDataKey::UtdHookManagerData => { - *self.utd_hook_manager_data.write().unwrap() = None + inner.recently_visited_rooms.remove(user_id); } + StateStoreDataKey::UtdHookManagerData => inner.utd_hook_manager_data = None, StateStoreDataKey::ComposerDraft(room_id) => { - self.composer_drafts.write().unwrap().remove(room_id); + inner.composer_drafts.remove(room_id); } } Ok(()) @@ -272,248 +252,202 @@ impl StateStore for MemoryStore { #[instrument(skip(self, changes))] async fn save_changes(&self, changes: &StateChanges) -> Result<()> { let now = Instant::now(); - // these trace calls are to debug https://github.com/matrix-org/complement-crypto/issues/77 - trace!("starting"); + + let mut inner = self.inner.write().unwrap(); if let Some(s) = &changes.sync_token { - *self.sync_token.write().unwrap() = Some(s.to_owned()); - trace!("assigned sync token"); + inner.sync_token = Some(s.to_owned()); } - trace!("profiles"); - { - let mut profiles = self.profiles.write().unwrap(); - - for (room, users) in &changes.profiles_to_delete { - let Some(room_profiles) = profiles.get_mut(room) else { - continue; - }; - for user in users { - room_profiles.remove(user); - } + for (room, users) in &changes.profiles_to_delete { + let Some(room_profiles) = inner.profiles.get_mut(room) else { + continue; + }; + for user in users { + room_profiles.remove(user); } + } - for (room, users) in &changes.profiles { - for (user_id, profile) in users { - profiles - .entry(room.clone()) - .or_default() - .insert(user_id.clone(), profile.clone()); - } + for (room, users) in &changes.profiles { + for (user_id, profile) in users { + inner + .profiles + .entry(room.clone()) + .or_default() + .insert(user_id.clone(), profile.clone()); } } - trace!("ambiguity maps"); for (room, map) in &changes.ambiguity_maps { for (display_name, display_names) in map { - self.display_names - .write() - .unwrap() + inner + .display_names .entry(room.clone()) .or_default() .insert(display_name.clone(), display_names.clone()); } } - trace!("account data"); - { - let mut account_data = self.account_data.write().unwrap(); - for (event_type, event) in &changes.account_data { - account_data.insert(event_type.clone(), event.clone()); + for (event_type, event) in &changes.account_data { + inner.account_data.insert(event_type.clone(), event.clone()); + } + + for (room, events) in &changes.room_account_data { + for (event_type, event) in events { + inner + .room_account_data + .entry(room.clone()) + .or_default() + .insert(event_type.clone(), event.clone()); } } - trace!("room account data"); - { - let mut room_account_data = self.room_account_data.write().unwrap(); - for (room, events) in &changes.room_account_data { - for (event_type, event) in events { - room_account_data + for (room, event_types) in &changes.state { + for (event_type, events) in event_types { + for (state_key, raw_event) in events { + inner + .room_state .entry(room.clone()) .or_default() - .insert(event_type.clone(), event.clone()); - } - } - } + .entry(event_type.clone()) + .or_default() + .insert(state_key.to_owned(), raw_event.clone()); + inner.stripped_room_state.remove(room); + + if *event_type == StateEventType::RoomMember { + let event = match raw_event.deserialize_as::() { + Ok(ev) => ev, + Err(e) => { + let event_id: Option = + raw_event.get_field("event_id").ok().flatten(); + debug!(event_id, "Failed to deserialize member event: {e}"); + continue; + } + }; - trace!("room state"); - { - let mut room_state = self.room_state.write().unwrap(); - trace!("room state: got room_state lock"); - let mut stripped_room_state = self.stripped_room_state.write().unwrap(); - trace!("room state: got stripped_room_state lock"); - let mut members = self.members.write().unwrap(); - trace!("room state: got members lock"); - let mut stripped_members = self.stripped_members.write().unwrap(); - trace!("room state: got stripped_members lock"); - - for (room, event_types) in &changes.state { - for (event_type, events) in event_types { - for (state_key, raw_event) in events { - room_state + inner.stripped_members.remove(room); + + inner + .members .entry(room.clone()) .or_default() - .entry(event_type.clone()) - .or_default() - .insert(state_key.to_owned(), raw_event.clone()); - stripped_room_state.remove(room); - - if *event_type == StateEventType::RoomMember { - let event = match raw_event.deserialize_as::() { - Ok(ev) => ev, - Err(e) => { - let event_id: Option = - raw_event.get_field("event_id").ok().flatten(); - debug!(event_id, "Failed to deserialize member event: {e}"); - continue; - } - }; - - stripped_members.remove(room); - - members - .entry(room.clone()) - .or_default() - .insert(event.state_key().to_owned(), event.membership().clone()); - } + .insert(event.state_key().to_owned(), event.membership().clone()); } } } } - trace!("room info"); - { - let mut room_info = self.room_info.write().unwrap(); - for (room_id, info) in &changes.room_infos { - room_info.insert(room_id.clone(), info.clone()); - } + for (room_id, info) in &changes.room_infos { + inner.room_info.insert(room_id.clone(), info.clone()); } - trace!("presence"); - { - let mut presence = self.presence.write().unwrap(); - for (sender, event) in &changes.presence { - presence.insert(sender.clone(), event.clone()); - } + for (sender, event) in &changes.presence { + inner.presence.insert(sender.clone(), event.clone()); } - trace!("stripped state"); - { - let mut stripped_room_state = self.stripped_room_state.write().unwrap(); - let mut stripped_members = self.stripped_members.write().unwrap(); + for (room, event_types) in &changes.stripped_state { + for (event_type, events) in event_types { + for (state_key, raw_event) in events { + inner + .stripped_room_state + .entry(room.clone()) + .or_default() + .entry(event_type.clone()) + .or_default() + .insert(state_key.to_owned(), raw_event.clone()); + + if *event_type == StateEventType::RoomMember { + let event = match raw_event.deserialize_as::() { + Ok(ev) => ev, + Err(e) => { + let event_id: Option = + raw_event.get_field("event_id").ok().flatten(); + debug!( + event_id, + "Failed to deserialize stripped member event: {e}" + ); + continue; + } + }; - for (room, event_types) in &changes.stripped_state { - for (event_type, events) in event_types { - for (state_key, raw_event) in events { - stripped_room_state + inner + .stripped_members .entry(room.clone()) .or_default() - .entry(event_type.clone()) - .or_default() - .insert(state_key.to_owned(), raw_event.clone()); - - if *event_type == StateEventType::RoomMember { - let event = match raw_event.deserialize_as::() - { - Ok(ev) => ev, - Err(e) => { - let event_id: Option = - raw_event.get_field("event_id").ok().flatten(); - debug!( - event_id, - "Failed to deserialize stripped member event: {e}" - ); - continue; - } - }; - - stripped_members - .entry(room.clone()) - .or_default() - .insert(event.state_key, event.content.membership.clone()); - } + .insert(event.state_key, event.content.membership.clone()); } } } } - trace!("receipts"); - { - let mut room_user_receipts = self.room_user_receipts.write().unwrap(); - let mut room_event_receipts = self.room_event_receipts.write().unwrap(); - - for (room, content) in &changes.receipts { - for (event_id, receipts) in &content.0 { - for (receipt_type, receipts) in receipts { - for (user_id, receipt) in receipts { - let thread = receipt.thread.as_str().map(ToOwned::to_owned); - // Add the receipt to the room user receipts - if let Some((old_event, _)) = room_user_receipts - .entry(room.clone()) - .or_default() - .entry((receipt_type.to_string(), thread.clone())) - .or_default() - .insert(user_id.clone(), (event_id.clone(), receipt.clone())) - { - // Remove the old receipt from the room event receipts - if let Some(receipt_map) = room_event_receipts.get_mut(room) { - if let Some(event_map) = receipt_map - .get_mut(&(receipt_type.to_string(), thread.clone())) - { - if let Some(user_map) = event_map.get_mut(&old_event) { - user_map.remove(user_id); - } + for (room, content) in &changes.receipts { + for (event_id, receipts) in &content.0 { + for (receipt_type, receipts) in receipts { + for (user_id, receipt) in receipts { + let thread = receipt.thread.as_str().map(ToOwned::to_owned); + // Add the receipt to the room user receipts + if let Some((old_event, _)) = inner + .room_user_receipts + .entry(room.clone()) + .or_default() + .entry((receipt_type.to_string(), thread.clone())) + .or_default() + .insert(user_id.clone(), (event_id.clone(), receipt.clone())) + { + // Remove the old receipt from the room event receipts + if let Some(receipt_map) = inner.room_event_receipts.get_mut(room) { + if let Some(event_map) = + receipt_map.get_mut(&(receipt_type.to_string(), thread.clone())) + { + if let Some(user_map) = event_map.get_mut(&old_event) { + user_map.remove(user_id); } } } - - // Add the receipt to the room event receipts - room_event_receipts - .entry(room.clone()) - .or_default() - .entry((receipt_type.to_string(), thread)) - .or_default() - .entry(event_id.clone()) - .or_default() - .insert(user_id.clone(), receipt.clone()); } + + // Add the receipt to the room event receipts + inner + .room_event_receipts + .entry(room.clone()) + .or_default() + .entry((receipt_type.to_string(), thread)) + .or_default() + .entry(event_id.clone()) + .or_default() + .insert(user_id.clone(), receipt.clone()); } } } } - trace!("room info/state"); - { - let room_info = self.room_info.read().unwrap(); - let mut room_state = self.room_state.write().unwrap(); - - let make_room_version = |room_id| { - room_info.get(room_id).and_then(|info| info.room_version().cloned()).unwrap_or_else( - || { - warn!(?room_id, "Unable to find the room version, assuming version 9"); - RoomVersionId::V9 - }, - ) - }; + let make_room_version = |room_info: &HashMap, room_id| { + room_info.get(room_id).and_then(|info| info.room_version().cloned()).unwrap_or_else( + || { + warn!(?room_id, "Unable to find the room version, assuming version 9"); + RoomVersionId::V9 + }, + ) + }; - for (room_id, redactions) in &changes.redactions { - let mut room_version = None; - if let Some(room) = room_state.get_mut(room_id) { - for ref_room_mu in room.values_mut() { - for raw_evt in ref_room_mu.values_mut() { - if let Ok(Some(event_id)) = - raw_evt.get_field::("event_id") - { - if let Some(redaction) = redactions.get(&event_id) { - let redacted = redact( - raw_evt.deserialize_as::()?, - room_version - .get_or_insert_with(|| make_room_version(room_id)), - Some(RedactedBecause::from_raw_event(redaction)?), - ) - .map_err(StoreError::Redaction)?; - *raw_evt = Raw::new(&redacted)?.cast(); - } + let inner = &mut *inner; + for (room_id, redactions) in &changes.redactions { + let mut room_version = None; + + if let Some(room) = inner.room_state.get_mut(room_id) { + for ref_room_mu in room.values_mut() { + for raw_evt in ref_room_mu.values_mut() { + if let Ok(Some(event_id)) = raw_evt.get_field::("event_id") { + if let Some(redaction) = redactions.get(&event_id) { + let redacted = redact( + raw_evt.deserialize_as::()?, + room_version.get_or_insert_with(|| { + make_room_version(&inner.room_info, room_id) + }), + Some(RedactedBecause::from_raw_event(redaction)?), + ) + .map_err(StoreError::Redaction)?; + *raw_evt = Raw::new(&redacted)?.cast(); } } } @@ -527,14 +461,14 @@ impl StateStore for MemoryStore { } async fn get_presence_event(&self, user_id: &UserId) -> Result>> { - Ok(self.presence.read().unwrap().get(user_id).cloned()) + Ok(self.inner.read().unwrap().presence.get(user_id).cloned()) } async fn get_presence_events( &self, user_ids: &[OwnedUserId], ) -> Result>> { - let presence = self.presence.read().unwrap(); + let presence = &self.inner.read().unwrap().presence; Ok(user_ids.iter().filter_map(|user_id| presence.get(user_id).cloned()).collect()) } @@ -566,18 +500,17 @@ impl StateStore for MemoryStore { Some(state_events.values().cloned().map(to_enum).collect()) } - let state_map = self.stripped_room_state.read().unwrap(); - Ok(get_events(&state_map, room_id, &event_type, RawAnySyncOrStrippedState::Stripped) - .or_else(|| { - drop(state_map); // release the lock on stripped_room_state - get_events( - &self.room_state.read().unwrap(), - room_id, - &event_type, - RawAnySyncOrStrippedState::Sync, - ) - }) - .unwrap_or_default()) + let inner = self.inner.read().unwrap(); + Ok(get_events( + &inner.stripped_room_state, + room_id, + &event_type, + RawAnySyncOrStrippedState::Stripped, + ) + .or_else(|| { + get_events(&inner.room_state, room_id, &event_type, RawAnySyncOrStrippedState::Sync) + }) + .unwrap_or_default()) } async fn get_state_events_for_keys( @@ -586,41 +519,31 @@ impl StateStore for MemoryStore { event_type: StateEventType, state_keys: &[&str], ) -> Result, Self::Error> { - Ok( - if let Some(stripped_state_events) = self - .stripped_room_state - .read() - .unwrap() - .get(room_id) - .and_then(|events| events.get(&event_type)) - { - state_keys - .iter() - .filter_map(|k| { - stripped_state_events - .get(*k) - .map(|e| RawAnySyncOrStrippedState::Stripped(e.clone())) - }) - .collect() - } else if let Some(sync_state_events) = self - .room_state - .read() - .unwrap() - .get(room_id) - .and_then(|events| events.get(&event_type)) - { - state_keys - .iter() - .filter_map(|k| { - sync_state_events - .get(*k) - .map(|e| RawAnySyncOrStrippedState::Sync(e.clone())) - }) - .collect() - } else { - Vec::new() - }, - ) + let inner = self.inner.read().unwrap(); + + if let Some(stripped_state_events) = + inner.stripped_room_state.get(room_id).and_then(|events| events.get(&event_type)) + { + Ok(state_keys + .iter() + .filter_map(|k| { + stripped_state_events + .get(*k) + .map(|e| RawAnySyncOrStrippedState::Stripped(e.clone())) + }) + .collect()) + } else if let Some(sync_state_events) = + inner.room_state.get(room_id).and_then(|events| events.get(&event_type)) + { + Ok(state_keys + .iter() + .filter_map(|k| { + sync_state_events.get(*k).map(|e| RawAnySyncOrStrippedState::Sync(e.clone())) + }) + .collect()) + } else { + Ok(Vec::new()) + } } async fn get_profile( @@ -629,9 +552,10 @@ impl StateStore for MemoryStore { user_id: &UserId, ) -> Result> { Ok(self - .profiles + .inner .read() .unwrap() + .profiles .get(room_id) .and_then(|room_profiles| room_profiles.get(user_id)) .cloned()) @@ -646,7 +570,7 @@ impl StateStore for MemoryStore { return Ok(BTreeMap::new()); } - let profiles = self.profiles.read().unwrap(); + let profiles = &self.inner.read().unwrap().profiles; let Some(room_profiles) = profiles.get(room_id) else { return Ok(BTreeMap::new()); }; @@ -686,17 +610,16 @@ impl StateStore for MemoryStore { }) .unwrap_or_default() } - let state_map = self.stripped_members.read().unwrap(); - let v = get_user_ids_inner(&state_map, room_id, memberships); + let inner = self.inner.read().unwrap(); + let v = get_user_ids_inner(&inner.stripped_members, room_id, memberships); if !v.is_empty() { return Ok(v); } - drop(state_map); // release the stripped_members lock - Ok(get_user_ids_inner(&self.members.read().unwrap(), room_id, memberships)) + Ok(get_user_ids_inner(&inner.members, room_id, memberships)) } async fn get_room_infos(&self) -> Result> { - Ok(self.room_info.read().unwrap().values().cloned().collect()) + Ok(self.inner.read().unwrap().room_info.values().cloned().collect()) } async fn get_users_with_display_name( @@ -705,9 +628,10 @@ impl StateStore for MemoryStore { display_name: &DisplayName, ) -> Result> { Ok(self - .display_names + .inner .read() .unwrap() + .display_names .get(room_id) .and_then(|room_names| room_names.get(display_name).cloned()) .unwrap_or_default()) @@ -722,8 +646,8 @@ impl StateStore for MemoryStore { return Ok(HashMap::new()); } - let read_guard = &self.display_names.read().unwrap(); - let Some(room_names) = read_guard.get(room_id) else { + let inner = self.inner.read().unwrap(); + let Some(room_names) = inner.display_names.get(room_id) else { return Ok(HashMap::new()); }; @@ -734,7 +658,7 @@ impl StateStore for MemoryStore { &self, event_type: GlobalAccountDataEventType, ) -> Result>> { - Ok(self.account_data.read().unwrap().get(&event_type).cloned()) + Ok(self.inner.read().unwrap().account_data.get(&event_type).cloned()) } async fn get_room_account_data_event( @@ -743,9 +667,10 @@ impl StateStore for MemoryStore { event_type: RoomAccountDataEventType, ) -> Result>> { Ok(self - .room_account_data + .inner .read() .unwrap() + .room_account_data .get(room_id) .and_then(|m| m.get(&event_type)) .cloned()) @@ -774,30 +699,32 @@ impl StateStore for MemoryStore { } async fn get_custom_value(&self, key: &[u8]) -> Result>> { - Ok(self.custom.read().unwrap().get(key).cloned()) + Ok(self.inner.read().unwrap().custom.get(key).cloned()) } async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { - Ok(self.custom.write().unwrap().insert(key.to_vec(), value)) + Ok(self.inner.write().unwrap().custom.insert(key.to_vec(), value)) } async fn remove_custom_value(&self, key: &[u8]) -> Result>> { - Ok(self.custom.write().unwrap().remove(key)) + Ok(self.inner.write().unwrap().custom.remove(key)) } async fn remove_room(&self, room_id: &RoomId) -> Result<()> { - self.profiles.write().unwrap().remove(room_id); - self.display_names.write().unwrap().remove(room_id); - self.members.write().unwrap().remove(room_id); - self.room_info.write().unwrap().remove(room_id); - self.room_state.write().unwrap().remove(room_id); - self.room_account_data.write().unwrap().remove(room_id); - self.stripped_room_state.write().unwrap().remove(room_id); - self.stripped_members.write().unwrap().remove(room_id); - self.room_user_receipts.write().unwrap().remove(room_id); - self.room_event_receipts.write().unwrap().remove(room_id); - self.send_queue_events.write().unwrap().remove(room_id); - self.dependent_send_queue_events.write().unwrap().remove(room_id); + let mut inner = self.inner.write().unwrap(); + + inner.profiles.remove(room_id); + inner.display_names.remove(room_id); + inner.members.remove(room_id); + inner.room_info.remove(room_id); + inner.room_state.remove(room_id); + inner.room_account_data.remove(room_id); + inner.stripped_room_state.remove(room_id); + inner.stripped_members.remove(room_id); + inner.room_user_receipts.remove(room_id); + inner.room_event_receipts.remove(room_id); + inner.send_queue_events.remove(room_id); + inner.dependent_send_queue_events.remove(room_id); Ok(()) } @@ -809,9 +736,10 @@ impl StateStore for MemoryStore { kind: QueuedRequestKind, priority: usize, ) -> Result<(), Self::Error> { - self.send_queue_events + self.inner .write() .unwrap() + .send_queue_events .entry(room_id.to_owned()) .or_default() .push(QueuedRequest { kind, transaction_id, error: None, priority }); @@ -825,9 +753,10 @@ impl StateStore for MemoryStore { kind: QueuedRequestKind, ) -> Result { if let Some(entry) = self - .send_queue_events + .inner .write() .unwrap() + .send_queue_events .entry(room_id.to_owned()) .or_default() .iter_mut() @@ -846,7 +775,8 @@ impl StateStore for MemoryStore { room_id: &RoomId, transaction_id: &TransactionId, ) -> Result { - let mut q = self.send_queue_events.write().unwrap(); + let mut inner = self.inner.write().unwrap(); + let q = &mut inner.send_queue_events; let entry = q.get_mut(room_id); if let Some(entry) = entry { @@ -868,8 +798,14 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, ) -> Result, Self::Error> { - let mut ret = - self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone(); + let mut ret = self + .inner + .write() + .unwrap() + .send_queue_events + .entry(room_id.to_owned()) + .or_default() + .clone(); // Inverted order of priority, use stable sort to keep insertion order. ret.sort_by(|lhs, rhs| rhs.priority.cmp(&lhs.priority)); Ok(ret) @@ -882,9 +818,10 @@ impl StateStore for MemoryStore { error: Option, ) -> Result<(), Self::Error> { if let Some(entry) = self - .send_queue_events + .inner .write() .unwrap() + .send_queue_events .entry(room_id.to_owned()) .or_default() .iter_mut() @@ -896,7 +833,7 @@ impl StateStore for MemoryStore { } async fn load_rooms_with_unsent_requests(&self) -> Result, Self::Error> { - Ok(self.send_queue_events.read().unwrap().keys().cloned().collect()) + Ok(self.inner.read().unwrap().send_queue_events.keys().cloned().collect()) } async fn save_dependent_queued_request( @@ -906,14 +843,18 @@ impl StateStore for MemoryStore { own_transaction_id: ChildTransactionId, content: DependentQueuedRequestKind, ) -> Result<(), Self::Error> { - self.dependent_send_queue_events.write().unwrap().entry(room.to_owned()).or_default().push( - DependentQueuedRequest { + self.inner + .write() + .unwrap() + .dependent_send_queue_events + .entry(room.to_owned()) + .or_default() + .push(DependentQueuedRequest { kind: content, parent_transaction_id: parent_transaction_id.to_owned(), own_transaction_id, parent_key: None, - }, - ); + }); Ok(()) } @@ -923,8 +864,8 @@ impl StateStore for MemoryStore { parent_txn_id: &TransactionId, sent_parent_key: SentRequestKey, ) -> Result { - let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap(); - let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default(); + let mut inner = self.inner.write().unwrap(); + let dependents = inner.dependent_send_queue_events.entry(room.to_owned()).or_default(); let mut num_updated = 0; for d in dependents.iter_mut().filter(|item| item.parent_transaction_id == parent_txn_id) { d.parent_key = Some(sent_parent_key.clone()); @@ -939,8 +880,8 @@ impl StateStore for MemoryStore { own_transaction_id: &ChildTransactionId, new_content: DependentQueuedRequestKind, ) -> Result { - let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap(); - let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default(); + let mut inner = self.inner.write().unwrap(); + let dependents = inner.dependent_send_queue_events.entry(room.to_owned()).or_default(); for d in dependents.iter_mut() { if d.own_transaction_id == *own_transaction_id { d.kind = new_content; @@ -955,8 +896,8 @@ impl StateStore for MemoryStore { room: &RoomId, txn_id: &ChildTransactionId, ) -> Result { - let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap(); - let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default(); + let mut inner = self.inner.write().unwrap(); + let dependents = inner.dependent_send_queue_events.entry(room.to_owned()).or_default(); if let Some(pos) = dependents.iter().position(|item| item.own_transaction_id == *txn_id) { dependents.remove(pos); Ok(true) @@ -973,7 +914,14 @@ impl StateStore for MemoryStore { &self, room: &RoomId, ) -> Result, Self::Error> { - Ok(self.dependent_send_queue_events.read().unwrap().get(room).cloned().unwrap_or_default()) + Ok(self + .inner + .read() + .unwrap() + .dependent_send_queue_events + .get(room) + .cloned() + .unwrap_or_default()) } } From 780a4630e4dae9a0cb958a526520eeee429bab3c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 11 Dec 2024 16:55:25 +0000 Subject: [PATCH 772/979] chore(ffi): avoid hardcoding clang version Update the workaround for https://github.com/rust-lang/rust/issues/109717 to avoid hardcoding the clang version; instead, run `clang -dumpversion` to figure it out. While we're there, use the `CC_x86_64-linux-android` env var, which should point to clang, rather than relying on `ANDROID_NDK_HOME` to be set. --- bindings/matrix-sdk-crypto-ffi/build.rs | 53 +++++++++++++++++-------- bindings/matrix-sdk-ffi/build.rs | 53 +++++++++++++++++-------- 2 files changed, 72 insertions(+), 34 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/build.rs b/bindings/matrix-sdk-crypto-ffi/build.rs index e66920150e3..14f2d11e10b 100644 --- a/bindings/matrix-sdk-crypto-ffi/build.rs +++ b/bindings/matrix-sdk-crypto-ffi/build.rs @@ -1,10 +1,10 @@ -use std::{env, error::Error}; +use std::{env, error::Error, path::PathBuf, process::Command}; use vergen::EmitBuilder; /// Adds a temporary workaround for an issue with the Rust compiler and Android /// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717. -/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442 +/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442 /// /// IMPORTANT: if you modify this, make sure to modify /// [../matrix-sdk-ffi/build.rs] too! @@ -12,26 +12,45 @@ fn setup_x86_64_android_workaround() { let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set"); if target_arch == "x86_64" && target_os == "android" { - let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set"); - let build_os = match env::consts::OS { - "linux" => "linux", - "macos" => "darwin", - "windows" => "windows", - _ => panic!( - "Unsupported OS. You must use either Linux, MacOS or Windows to build the crate." - ), - }; - const DEFAULT_CLANG_VERSION: &str = "18"; - let clang_version = - env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned()); - let linux_x86_64_lib_dir = format!( - "toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/" + // Configure rust to statically link against the `libclang_rt.builtins` supplied + // with clang. + + // cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the + // Android NDK. + let clang_path = PathBuf::from( + env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"), ); - println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}"); + + // clang_path should now look something like + // `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`. + // We strip `/bin/clang` from the end to get the toolchain path. + let toolchain_path = clang_path + .ancestors() + .nth(2) + .expect("could not find NDK toolchain path") + .to_str() + .expect("NDK toolchain path is not valid UTF-8"); + + let clang_version = get_clang_major_version(&clang_path); + + println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/"); println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android"); } } +/// Run the clang binary at `clang_path`, and return its major version number +fn get_clang_major_version(clang_path: &PathBuf) -> String { + let clang_output = + Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang"); + + if !clang_output.status.success() { + panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr)); + } + + let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8"); + clang_version.split('.').next().expect("could not parse clang output").to_owned() +} + fn main() -> Result<(), Box> { setup_x86_64_android_workaround(); diff --git a/bindings/matrix-sdk-ffi/build.rs b/bindings/matrix-sdk-ffi/build.rs index 30a6b178a9f..2605a8fe514 100644 --- a/bindings/matrix-sdk-ffi/build.rs +++ b/bindings/matrix-sdk-ffi/build.rs @@ -1,10 +1,10 @@ -use std::{env, error::Error}; +use std::{env, error::Error, path::PathBuf, process::Command}; use vergen::EmitBuilder; /// Adds a temporary workaround for an issue with the Rust compiler and Android /// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717. -/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442 +/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442 /// /// IMPORTANT: if you modify this, make sure to modify /// [../matrix-sdk-crypto-ffi/build.rs] too! @@ -12,26 +12,45 @@ fn setup_x86_64_android_workaround() { let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set"); if target_arch == "x86_64" && target_os == "android" { - let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set"); - let build_os = match env::consts::OS { - "linux" => "linux", - "macos" => "darwin", - "windows" => "windows", - _ => panic!( - "Unsupported OS. You must use either Linux, MacOS or Windows to build the crate." - ), - }; - const DEFAULT_CLANG_VERSION: &str = "18"; - let clang_version = - env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned()); - let linux_x86_64_lib_dir = format!( - "toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/" + // Configure rust to statically link against the `libclang_rt.builtins` supplied + // with clang. + + // cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the + // Android NDK. + let clang_path = PathBuf::from( + env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"), ); - println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}"); + + // clang_path should now look something like + // `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`. + // We strip `/bin/clang` from the end to get the toolchain path. + let toolchain_path = clang_path + .ancestors() + .nth(2) + .expect("could not find NDK toolchain path") + .to_str() + .expect("NDK toolchain path is not valid UTF-8"); + + let clang_version = get_clang_major_version(&clang_path); + + println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/"); println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android"); } } +/// Run the clang binary at `clang_path`, and return its major version number +fn get_clang_major_version(clang_path: &PathBuf) -> String { + let clang_output = + Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang"); + + if !clang_output.status.success() { + panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr)); + } + + let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8"); + clang_version.split('.').next().expect("could not parse clang output").to_owned() +} + fn main() -> Result<(), Box> { setup_x86_64_android_workaround(); uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed"); From f7f58dfd7104afa6e0bcf2bb86d30ab71472a515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 12 Dec 2024 10:31:53 +0100 Subject: [PATCH 773/979] feat(ui): Add the MemberHints state event type to the required state This state event allows us to correctly calculate the room display name according to MSC4171. MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4171 --- crates/matrix-sdk-ui/src/room_list_service/mod.rs | 2 ++ crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 441c79944bb..37a36145c66 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -92,6 +92,8 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[ // Those two events are required to properly compute room previews. (StateEventType::RoomCreate, ""), (StateEventType::RoomHistoryVisibility, ""), + // Required to correctly calculate the room display name. + (StateEventType::MemberHints, ""), ]; /// The default `required_state` constant value for sliding sync room 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 b28df20f150..9eb84c7bf7c 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -365,6 +365,7 @@ async fn test_sync_all_states() -> Result<(), Error> { ["m.room.join_rules", ""], ["m.room.create", ""], ["m.room.history_visibility", ""], + ["io.element.functional_members", ""], ], "include_heroes": true, "filters": { @@ -2232,6 +2233,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["m.room.join_rules", ""], ["m.room.create", ""], ["m.room.history_visibility", ""], + ["io.element.functional_members", ""], ["m.room.pinned_events", ""], ], "timeline_limit": 20, @@ -2272,6 +2274,7 @@ async fn test_room_subscription() -> Result<(), Error> { ["m.room.join_rules", ""], ["m.room.create", ""], ["m.room.history_visibility", ""], + ["io.element.functional_members", ""], ["m.room.pinned_events", ""], ], "timeline_limit": 20, From 7ae31d0cb17a5caed0e1744cf0f417dc2f6228da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 12 Dec 2024 10:34:44 +0100 Subject: [PATCH 774/979] fix(base): Subtract the number of service members from the number joined members This patch fixes an edge case where the member is alone in the room with a service member. We already subtracted the number of service members in the case we calculated the room summary ourselves, but we did not do so when the server provided the room summary. This lead to the room, instead of being called `Empty`, being called `Foo and N others`. --- crates/matrix-sdk-base/src/rooms/normal.rs | 119 ++++++++++++++++++--- 1 file changed, 103 insertions(+), 16 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 1cdfa7dba21..9e0fa59a763 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -119,6 +119,23 @@ bitflags! { } } +/// The result of a room summary computation. +/// +/// If the homeserver does not provide a room summary, we perform a best-effort +/// computation to generate one ourselves. If the homeserver does provide the +/// summary, we augment it with additional information about the service members +/// in the room. +struct ComputedSummary { + /// The list of display names that will be used to calculate the room + /// display name. + heroes: Vec, + /// The number of joined service members in the room. + num_service_members: u64, + /// The number of joined and invited members, not including any service + /// members. + num_joined_invited_guess: Option, +} + impl Default for RoomInfoNotableUpdateReasons { fn default() -> Self { Self::empty() @@ -646,16 +663,18 @@ impl Room { &self, summary: RoomSummary, ) -> StoreResult { - let summary_member_count = summary.joined_member_count + summary.invited_member_count; - - let (heroes, num_joined_invited_guess) = if !summary.room_heroes.is_empty() { - let heroes = self.extract_heroes(&summary.room_heroes).await?; - (heroes, None) + let computed_summary = if !summary.room_heroes.is_empty() { + self.extract_and_augment_summary(&summary).await? } else { - let (heroes, num_joined_invited) = self.compute_summary().await?; - (heroes, Some(num_joined_invited)) + self.compute_summary().await? }; + let ComputedSummary { heroes, num_service_members, num_joined_invited_guess } = + computed_summary; + + let summary_member_count = (summary.joined_member_count + summary.invited_member_count) + .saturating_sub(num_service_members); + let num_joined_invited = if self.state() == RoomState::Invited { // when we were invited we don't have a proper summary, we have to do best // guessing @@ -689,15 +708,34 @@ impl Room { Ok(display_name) } - /// Extract and collect the display names of the room heroes from a - /// [`RoomSummary`]. + /// Extracts and enhances the [`RoomSummary`] provided by the homeserver. /// - /// Returns the display names as a list of strings. - async fn extract_heroes(&self, heroes: &[RoomHero]) -> StoreResult> { + /// This method extracts the relevant data from the [`RoomSummary`] and + /// augments it with additional information that may not be included in + /// the initial response, such as details about service members in the + /// room. + /// + /// Returns a [`ComputedSummary`] with the + /// [`ComputedSummary::num_joined_invited_guess`] field set to `None`. + async fn extract_and_augment_summary( + &self, + summary: &RoomSummary, + ) -> StoreResult { + let heroes = &summary.room_heroes; + let mut names = Vec::with_capacity(heroes.len()); let own_user_id = self.own_user_id(); let member_hints = self.get_member_hints().await?; + // If we have some service members in the heroes, that means that they are also + // part of the joined member counts. They shouldn't be so, otherwise + // we'll wrongly assume that there are more members in the room than + // they are for the "Bob and 2 others" case. + let num_service_members = heroes + .iter() + .filter(|hero| member_hints.service_members.contains(&hero.user_id)) + .count() as u64; + // Construct a filter that is specific to this own user id, set of member hints, // and accepts a `RoomHero` type. let heroes_filter = heroes_filter(own_user_id, &member_hints); @@ -721,15 +759,15 @@ impl Room { } } - Ok(names) + Ok(ComputedSummary { heroes: names, num_service_members, num_joined_invited_guess: None }) } /// Compute the room summary with the data present in the store. /// /// The summary might be incorrect if the database info is outdated. /// - /// Returns a `(heroes_names, num_joined_invited)` tuple. - async fn compute_summary(&self) -> StoreResult<(Vec, u64)> { + /// Returns the [`ComputedSummary`]. + async fn compute_summary(&self) -> StoreResult { let member_hints = self.get_member_hints().await?; // Construct a filter that is specific to this own user id, set of member hints, @@ -779,7 +817,14 @@ impl Room { "Computed a room summary since we didn't receive one." ); - Ok((heroes, num_joined_invited as u64)) + let num_service_members = num_service_members as u64; + let num_joined_invited = num_joined_invited as u64; + + Ok(ComputedSummary { + heroes, + num_service_members, + num_joined_invited_guess: Some(num_joined_invited), + }) } async fn get_member_hints(&self) -> StoreResult { @@ -2619,7 +2664,7 @@ mod tests { let mut changes = StateChanges::new("".to_owned()); let summary = assign!(RumaSummary::new(), { - joined_member_count: Some(2u32.into()), + joined_member_count: Some(3u32.into()), heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()], }); @@ -2655,6 +2700,48 @@ mod tests { ); } + #[async_test] + async fn test_display_name_dm_joined_alone_with_service_members() { + let (store, room) = make_room_test_helper(RoomState::Joined); + let room_id = room_id!("!test:localhost"); + + let me = user_id!("@me:example.org"); + let bot = user_id!("@bot:example.org"); + + let mut changes = StateChanges::new("".to_owned()); + let summary = assign!(RumaSummary::new(), { + joined_member_count: Some(2u32.into()), + heroes: vec![me.to_owned(), bot.to_owned()], + }); + + let f = EventFactory::new().room(room_id!("!test:localhost")); + + let members = changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::RoomMember) + .or_default(); + members.insert(me.into(), f.member(me).display_name("Me").into_raw()); + members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw()); + + let member_hints_content = + f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw(); + changes + .state + .entry(room_id.to_owned()) + .or_default() + .entry(StateEventType::MemberHints) + .or_default() + .insert("".to_owned(), member_hints_content); + + store.save_changes(&changes).await.unwrap(); + + room.inner.update_if(|info| info.update_from_ruma_summary(&summary)); + // Bot should not contribute to the display name. + assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty); + } + #[async_test] async fn test_display_name_dm_joined_no_heroes() { let (store, room) = make_room_test_helper(RoomState::Joined); From 54bd1d793151d1d1b0ce9348781e25558a8eb968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 12 Dec 2024 13:56:15 +0100 Subject: [PATCH 775/979] refactor(base): Move the joined member count logic into its respective sub-functions --- crates/matrix-sdk-base/src/rooms/normal.rs | 41 ++++++++++++---------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 9e0fa59a763..1c8e1f1ea64 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -133,7 +133,7 @@ struct ComputedSummary { num_service_members: u64, /// The number of joined and invited members, not including any service /// members. - num_joined_invited_guess: Option, + num_joined_invited_guess: u64, } impl Default for RoomInfoNotableUpdateReasons { @@ -680,14 +680,7 @@ impl Room { // guessing heroes.len() as u64 + 1 } else if summary_member_count == 0 { - if let Some(num_joined_invited) = num_joined_invited_guess { - num_joined_invited - } else { - self.store - .get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE) - .await? - .len() as u64 - } + num_joined_invited_guess } else { summary_member_count }; @@ -715,8 +708,7 @@ impl Room { /// the initial response, such as details about service members in the /// room. /// - /// Returns a [`ComputedSummary`] with the - /// [`ComputedSummary::num_joined_invited_guess`] field set to `None`. + /// Returns a [`ComputedSummary`]. async fn extract_and_augment_summary( &self, summary: &RoomSummary, @@ -759,7 +751,24 @@ impl Room { } } - Ok(ComputedSummary { heroes: names, num_service_members, num_joined_invited_guess: None }) + let num_joined_invited_guess = summary.joined_member_count + summary.invited_member_count; + + // If the summary doesn't provide the number of joined/invited members, let's + // guess something. + let num_joined_invited_guess = if num_joined_invited_guess == 0 { + let guess = self + .store + .get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE) + .await? + .len() as u64; + + guess.saturating_sub(num_service_members) + } else { + // Otherwise, accept the numbers provided by the summary as the guess. + num_joined_invited_guess + }; + + Ok(ComputedSummary { heroes: names, num_service_members, num_joined_invited_guess }) } /// Compute the room summary with the data present in the store. @@ -818,13 +827,9 @@ impl Room { ); let num_service_members = num_service_members as u64; - let num_joined_invited = num_joined_invited as u64; + let num_joined_invited_guess = num_joined_invited as u64; - Ok(ComputedSummary { - heroes, - num_service_members, - num_joined_invited_guess: Some(num_joined_invited), - }) + Ok(ComputedSummary { heroes, num_service_members, num_joined_invited_guess }) } async fn get_member_hints(&self) -> StoreResult { From 150d9e4b050395bac1981dd57075de645b28c806 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 12 Dec 2024 15:09:02 +0100 Subject: [PATCH 776/979] fix(event cache store): always use immediate mode when handling linked chunk updates If a linked chunk update starts with a RemoveChunk update, then the transaction may start with a SELECT query and be considered a read transaction. Soon enough, it will be upgraded into a write transaction, because of the next UPDATE/DELETE operations that happen thereafter. If there's another write transaction already happening, this may result in a SQLITE_BUSY error, according to https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions One solution is to always start the transaction in immediate mode. This may also fail with SQLITE_BUSY according to the documentation, but it's unclear whether it will happen in general, since we're using WAL mode too. Let's try it out. --- .../src/event_cache_store.rs | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 33baf630b41..fd7657684ef 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -27,7 +27,7 @@ use matrix_sdk_base::{ }; use matrix_sdk_store_encryption::StoreCipher; use ruma::{MilliSecondsSinceUnixEpoch, RoomId}; -use rusqlite::{OptionalExtension, Transaction}; +use rusqlite::{OptionalExtension, Transaction, TransactionBehavior}; use tokio::fs; use tracing::{debug, trace}; @@ -378,9 +378,7 @@ impl EventCacheStore for SqliteEventCacheStore { let room_id = room_id.to_owned(); let this = self.clone(); - self.acquire() - .await? - .with_transaction(move |txn| -> Result<_, Self::Error> { + with_immediate_transaction(self.acquire().await?, move |txn| { for up in updates { match up { Update::NewItemsChunk { previous, new, next } => { @@ -709,6 +707,43 @@ impl EventCacheStore for SqliteEventCacheStore { } } +/// Like `deadpool::managed::Object::with_transaction`, but starts the +/// transaction in immediate (write) mode from the beginning, precluding errors +/// of the kind SQLITE_BUSY from happening, for transactions that may involve +/// both reads and writes, and start with a write. +async fn with_immediate_transaction< + T: Send + 'static, + F: FnOnce(&Transaction<'_>) -> Result + Send + 'static, +>( + conn: SqliteAsyncConn, + f: F, +) -> Result { + conn.interact(move |conn| -> Result { + // Start the transaction in IMMEDIATE mode since all updates may cause writes, + // to avoid read transactions upgrading to write mode and causing + // SQLITE_BUSY errors. See also: https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions + conn.set_transaction_behavior(TransactionBehavior::Immediate); + + let code = || -> Result { + let txn = conn.transaction()?; + let res = f(&txn)?; + txn.commit()?; + Ok(res) + }; + + let res = code(); + + // Reset the transaction behavior to use Deferred, after this transaction has + // been run, whether it was successful or not. + conn.set_transaction_behavior(TransactionBehavior::Deferred); + + res + }) + .await + // SAFETY: same logic as in [`deadpool::managed::Object::with_transaction`].` + .unwrap() +} + fn insert_chunk( txn: &Transaction<'_>, room_id: &Key, From 6dcefe49c256f1cc1874e98b4740633663dfb922 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Dec 2024 15:27:15 +0000 Subject: [PATCH 777/979] feat(utds): Provide the reason why an event was an expected UTD --- crates/matrix-sdk-crypto/CHANGELOG.md | 7 + .../src/types/events/utd_cause.rs | 320 ++++++++++++------ .../matrix-sdk-ui/src/timeline/tests/mod.rs | 2 + crates/matrix-sdk/src/room/mod.rs | 13 + 4 files changed, 238 insertions(+), 104 deletions(-) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 92e18098859..32716cf914a 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -11,6 +11,13 @@ All notable changes to this project will be documented in this file. when the sender either did not wish to share or was unable to share the room_key. ([#4305](https://github.com/matrix-org/matrix-rust-sdk/pull/4305)) +- `UtdCause` has two new variants that replace the existing `HistoricalMessage`: + `HistoricalMessageAndBackupIsDisabled` and `HistoricalMessageAndDeviceIsUnverified`. + These give more detail about what went wrong and allow us to suggest to users + what actions they can take to fix the problem. See the doc comments on these + variants for suggested wording. + ([#4384](https://github.com/matrix-org/matrix-rust-sdk/pull/4384)) + ## [0.8.0] - 2024-11-19 ### Features diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index bef568ea10c..42be623f858 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -24,6 +24,15 @@ use serde::Deserialize; pub enum UtdCause { /// We don't have an explanation for why this UTD happened - it is probably /// a bug, or a network split between the two homeservers. + /// + /// For example: + /// + /// - the keys for this event are missing, but a key storage backup exists + /// and is working, so we should be able to find the keys in the backup. + /// + /// - the keys for this event are missing, and a key storage backup exists + /// on the server, but that backup is not working on this client even + /// though this device is verified. #[default] Unknown = 0, @@ -49,14 +58,17 @@ pub enum UtdCause { UnknownDevice = 4, /// We are missing the keys for this event, but it is a "device-historical" - /// message and no backup is accessible or usable. + /// message and there is no key storage backup on the server, presumably + /// because the user has turned it off. /// /// Device-historical means that the message was sent before the current /// device existed (but the current user was probably a member of the room /// at the time the message was sent). Not to /// be confused with pre-join or pre-invite messages (see /// [`UtdCause::SentBeforeWeJoined`] for that). - HistoricalMessage = 5, + /// + /// Expected message to user: "History is not available on this device". + HistoricalMessageAndBackupIsDisabled = 5, /// The keys for this event are intentionally withheld. /// @@ -70,6 +82,19 @@ pub enum UtdCause { /// this device by cherry-picking and blocking it, in which case, no action /// can be taken on our side. WithheldBySender = 7, + + /// We are missing the keys for this event, but it is a "device-historical" + /// message, and even though a key storage backup does exist, we can't use + /// it because our device is unverified. + /// + /// Device-historical means that the message was sent before the current + /// device existed (but the current user was probably a member of the room + /// at the time the message was sent). Not to + /// be confused with pre-join or pre-invite messages (see + /// [`UtdCause::SentBeforeWeJoined`] for that). + /// + /// Expected message to user: "You need to verify this device". + HistoricalMessageAndDeviceIsUnverified = 8, } /// MSC4115 membership info in the unsigned area. @@ -97,6 +122,14 @@ pub struct CryptoContextInfo { /// if an event is device-historical or not (sent before the current device /// existed). pub device_creation_ts: MilliSecondsSinceUnixEpoch, + + /// True if this device is secure because it has been verified by us + pub this_device_is_verified: bool, + + /// True if key storage exists on the server, even if we are unable to use + /// it + pub backup_exists_on_server: bool, + /// True if key storage is correctly set up and can be used by the current /// client to download and decrypt message keys. pub is_backup_configured: bool, @@ -134,14 +167,9 @@ impl UtdCause { } if let Ok(timeline_event) = raw_event.deserialize() { - if crypto_context_info.is_backup_configured - && timeline_event.origin_server_ts() - < crypto_context_info.device_creation_ts - { - // It's a device-historical message and there is no accessible - // backup. The key is missing and it - // is expected. - return UtdCause::HistoricalMessage; + if timeline_event.origin_server_ts() < crypto_context_info.device_creation_ts { + // This event was sent before this device existed, so it is "historical" + return UtdCause::determine_historical(crypto_context_info); } } @@ -163,6 +191,50 @@ impl UtdCause { _ => UtdCause::Unknown, } } + + /** + * Below is the flow chart we follow for deciding whether historical + * UTDs are expected. This function starts at position `B`. + * + * ```text + * A: Is the message newer than the device? + * No -> B + * Yes - Normal UTD error + * + * B: Is there a backup on the server? + * No -> History is not available on this device + * Yes -> C + * + * C: Is backup working on this device? + * No -> D + * Yes -> Normal UTD error + * + * D: Is this device verified? + * No -> You need to verify this device + * Yes -> Normal UTD error + * ``` + */ + fn determine_historical(crypto_context_info: CryptoContextInfo) -> UtdCause { + let backup_disabled = !crypto_context_info.backup_exists_on_server; + let backup_failing = !crypto_context_info.is_backup_configured; + let unverified = !crypto_context_info.this_device_is_verified; + + if backup_disabled { + UtdCause::HistoricalMessageAndBackupIsDisabled + } else if backup_failing && unverified { + UtdCause::HistoricalMessageAndDeviceIsUnverified + } else { + // We didn't get the key from key storage backup, but we think we should have, + // because either: + // + // * backup is working (so why didn't we get it?), or + // * backup is not working for an unknown reason (because the device is + // verified, and that is the only reason we check). + // + // In either case, we shrug and give an `Unknown` cause. + UtdCause::Unknown + } + } } #[cfg(test)] @@ -183,11 +255,7 @@ mod tests { fn test_if_there_is_no_membership_info_we_guess_unknown() { // If our JSON contains no membership info, then we guess the UTD is unknown. assert_eq!( - UtdCause::determine( - &raw_event(json!({})), - device_old_no_backup(), - &missing_megolm_session() - ), + UtdCause::determine(&raw_event(json!({})), device_old(), &missing_megolm_session()), UtdCause::Unknown ); } @@ -199,7 +267,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": 3 } })), - device_new_with_backup(), + device_old(), &missing_megolm_session() ), UtdCause::Unknown @@ -213,7 +281,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "invite" } }),), - device_new_with_backup(), + device_old(), &missing_megolm_session() ), UtdCause::Unknown @@ -227,7 +295,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "join" } })), - device_new_with_backup(), + device_old(), &missing_megolm_session() ), UtdCause::Unknown @@ -241,7 +309,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "leave" } })), - device_new_with_backup(), + device_old(), &missing_megolm_session() ), UtdCause::SentBeforeWeJoined @@ -256,7 +324,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "membership": "leave" } })), - device_new_with_backup(), + device_old(), &malformed_encrypted_event() ), UtdCause::Unknown @@ -269,7 +337,7 @@ mod tests { assert_eq!( UtdCause::determine( &raw_event(json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })), - device_new_with_backup(), + device_old(), &missing_megolm_session() ), UtdCause::SentBeforeWeJoined @@ -279,11 +347,7 @@ mod tests { #[test] fn test_verification_violation_is_passed_through() { assert_eq!( - UtdCause::determine( - &raw_event(json!({})), - device_new_with_backup(), - &verification_violation() - ), + UtdCause::determine(&raw_event(json!({})), device_old(), &verification_violation()), UtdCause::VerificationViolation ); } @@ -291,11 +355,7 @@ mod tests { #[test] fn test_unsigned_device_is_passed_through() { assert_eq!( - UtdCause::determine( - &raw_event(json!({})), - device_new_with_backup(), - &unsigned_device() - ), + UtdCause::determine(&raw_event(json!({})), device_old(), &unsigned_device()), UtdCause::UnsignedDevice ); } @@ -303,97 +363,159 @@ mod tests { #[test] fn test_unknown_device_is_passed_through() { assert_eq!( - UtdCause::determine(&raw_event(json!({})), device_new_with_backup(), &missing_device()), + UtdCause::determine(&raw_event(json!({})), device_old(), &missing_device()), UtdCause::UnknownDevice ); } #[test] fn test_old_devices_dont_cause_historical_utds() { - // If the device is old, we say this UTD is unexpected (missing megolm session) - assert_eq!( - UtdCause::determine(&utd_event(), device_old_with_backup(), &missing_megolm_session()), - UtdCause::Unknown - ); + // Message key is missing. + let info = missing_megolm_session(); + + // The device is old. + let context = device_old(); + + // So we have no explanation for this UTD. + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); // Same for unknown megolm message index - assert_eq!( - UtdCause::determine( - &utd_event(), - device_old_with_backup(), - &unknown_megolm_message_index() - ), - UtdCause::Unknown - ); + let info = unknown_megolm_message_index(); + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); } #[test] - fn test_new_devices_cause_historical_utds() { - // If the device is old, we say this UTD is expected historical (missing megolm - // session) + fn test_if_backup_is_disabled_historical_utd_is_expected() { + // Message key is missing. + let info = missing_megolm_session(); + + // The device is new. + let mut context = device_new(); + + // There is no key storage backup on the server. + context.backup_exists_on_server = false; + + // So this UTD is expected, and the solution (for future messages!) is to turn + // on key storage backups. assert_eq!( - UtdCause::determine(&utd_event(), device_new_with_backup(), &missing_megolm_session()), - UtdCause::HistoricalMessage + UtdCause::determine(&utd_event(), context, &info), + UtdCause::HistoricalMessageAndBackupIsDisabled ); // Same for unknown megolm message index + let info = unknown_megolm_message_index(); assert_eq!( - UtdCause::determine( - &utd_event(), - device_new_with_backup(), - &unknown_megolm_message_index() - ), - UtdCause::HistoricalMessage + UtdCause::determine(&utd_event(), context, &info), + UtdCause::HistoricalMessageAndBackupIsDisabled ); } #[test] fn test_malformed_events_are_never_expected_utds() { - // Even if the device is new, if the reason for the UTD is a malformed event, - // it's an unexpected UTD. - assert_eq!( - UtdCause::determine( - &utd_event(), - device_new_with_backup(), - &malformed_encrypted_event() - ), - UtdCause::Unknown - ); + // The event was malformed. + let info = malformed_encrypted_event(); + + // The device is new. + let mut context = device_new(); + + // There is no key storage backup on the server. + context.backup_exists_on_server = false; + + // So this could be expected historical like the previous test, but because the + // encrypted event is malformed, that takes precedence, and it's unexpected. + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); // Same for decryption failures - assert_eq!( - UtdCause::determine( - &utd_event(), - device_new_with_backup(), - &megolm_decryption_failure() - ), - UtdCause::Unknown - ); + let info = megolm_decryption_failure(); + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); } #[test] - fn test_if_backup_is_disabled_this_utd_is_unexpected() { - // If the backup is disables, even if the device is new and the reason for the - // UTD is missing keys, we still treat this UTD as unexpected. - // - // TODO: I (AJB) think this is wrong, but it will be addressed as part of - // https://github.com/element-hq/element-meta/issues/2638 + fn test_new_devices_with_nonworking_backups_because_unverified_cause_expected_utds() { + // Message key is missing. + let info = missing_megolm_session(); + + // The device is new. + let mut context = device_new(); + + // There is a key storage backup on the server. + context.backup_exists_on_server = true; + + // The key storage backup is not working, + context.is_backup_configured = false; + + // probably because... + // Our device is not verified. + context.this_device_is_verified = false; + + // So this UTD is expected, and the solution is (hopefully) to verify. assert_eq!( - UtdCause::determine(&utd_event(), device_new_no_backup(), &missing_megolm_session()), - UtdCause::Unknown + UtdCause::determine(&utd_event(), context, &info), + UtdCause::HistoricalMessageAndDeviceIsUnverified ); // Same for unknown megolm message index + let info = unknown_megolm_message_index(); assert_eq!( - UtdCause::determine( - &utd_event(), - device_new_no_backup(), - &unknown_megolm_message_index() - ), - UtdCause::Unknown + UtdCause::determine(&utd_event(), context, &info), + UtdCause::HistoricalMessageAndDeviceIsUnverified ); } + #[test] + fn test_if_backup_is_working_then_historical_utd_is_unexpected() { + // Message key is missing. + let info = missing_megolm_session(); + + // The device is new. + let mut context = device_new(); + + // There is a key storage backup on the server. + context.backup_exists_on_server = true; + + // The key storage backup is working. + context.is_backup_configured = true; + + // So this UTD is unexpected since we should be able to fetch the key from + // storage. + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); + + // Same for unknown megolm message index + let info = unknown_megolm_message_index(); + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); + } + + #[test] + fn test_if_backup_is_not_working_even_though_verified_then_historical_utd_is_unexpected() { + // Message key is missing. + let info = missing_megolm_session(); + + // The device is new. + let mut context = device_new(); + + // There is a key storage backup on the server. + context.backup_exists_on_server = true; + + // The key storage backup is working. + context.is_backup_configured = false; + + // even though... + // Our device is verified. + context.this_device_is_verified = true; + + // So this UTD is unexpected since we can't explain why our backup is not + // working. + // + // TODO: it might be nice to tell the user that our backup is not working! + // Currently we don't distinguish between Unknown cases, since we want + // to make sure they are all reported as unexpected UTDs. + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); + + // Same for unknown megolm message index + let info = unknown_megolm_message_index(); + assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown); + } + fn utd_event() -> Raw { raw_event(json!({ "type": "m.room.encrypted", @@ -416,31 +538,21 @@ mod tests { Raw::from_json(to_raw_value(&value).unwrap()) } - fn device_old_no_backup() -> CryptoContextInfo { + fn device_old() -> CryptoContextInfo { CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch((BEFORE_EVENT_TIME).try_into().unwrap()), + this_device_is_verified: false, is_backup_configured: false, + backup_exists_on_server: false, } } - fn device_old_with_backup() -> CryptoContextInfo { - CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch((BEFORE_EVENT_TIME).try_into().unwrap()), - is_backup_configured: true, - } - } - - fn device_new_no_backup() -> CryptoContextInfo { + fn device_new() -> CryptoContextInfo { CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch((AFTER_EVENT_TIME).try_into().unwrap()), + this_device_is_verified: false, is_backup_configured: false, - } - } - - fn device_new_with_backup() -> CryptoContextInfo { - CryptoContextInfo { - device_creation_ts: MilliSecondsSinceUnixEpoch((AFTER_EVENT_TIME).try_into().unwrap()), - is_backup_configured: true, + backup_exists_on_server: false, } } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index f708da78664..eb6601c167d 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -387,6 +387,8 @@ impl RoomDataProvider for TestRoomDataProvider { ) .unwrap_or(MilliSecondsSinceUnixEpoch::now()), is_backup_configured: false, + this_device_is_verified: true, + backup_exists_on_server: true, }) .boxed() } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 438a28c4091..224b64ad352 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -617,9 +617,22 @@ impl Room { #[cfg(feature = "e2e-encryption")] pub async fn crypto_context_info(&self) -> CryptoContextInfo { let encryption = self.client.encryption(); + + let this_device_is_verified = match encryption.get_own_device().await { + Ok(Some(device)) => device.is_verified_with_cross_signing(), + + // Should not happen, there will always be an own device + _ => true, + }; + + let backup_exists_on_server = + encryption.backups().exists_on_server().await.unwrap_or(false); + CryptoContextInfo { device_creation_ts: encryption.device_creation_timestamp().await, + this_device_is_verified, is_backup_configured: encryption.backups().state() == BackupState::Enabled, + backup_exists_on_server, } } From 2b39476d9b2f49b0ebedf4c08665ea7a3127aefa Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 13 Dec 2024 13:03:43 +0100 Subject: [PATCH 778/979] feat(crypto): Support storing the dehydrated device pickle key --- .../src/dehydrated_devices.rs | 17 +-- crates/matrix-sdk-crypto/CHANGELOG.md | 7 ++ .../src/dehydrated_devices.rs | 116 +++++++++++++++--- .../src/store/integration_tests.rs | 49 +++++++- .../src/store/memorystore.rs | 36 +++++- crates/matrix-sdk-crypto/src/store/mod.rs | 47 +++++++ crates/matrix-sdk-crypto/src/store/traits.rs | 19 ++- .../src/crypto_store/mod.rs | 37 +++++- crates/matrix-sdk-sqlite/src/crypto_store.rs | 28 ++++- crates/matrix-sdk-sqlite/src/utils.rs | 18 +++ 10 files changed, 342 insertions(+), 32 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs index 585eb7a9be1..ae05014c943 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs @@ -1,8 +1,11 @@ use std::{mem::ManuallyDrop, sync::Arc}; -use matrix_sdk_crypto::dehydrated_devices::{ - DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices, - RehydratedDevice as InnerRehydratedDevice, +use matrix_sdk_crypto::{ + dehydrated_devices::{ + DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices, + RehydratedDevice as InnerRehydratedDevice, + }, + store::DehydratedDeviceKey, }; use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId}; use serde_json::json; @@ -177,13 +180,13 @@ impl From } } -fn get_pickle_key(pickle_key: &[u8]) -> Result, DehydrationError> { +fn get_pickle_key(pickle_key: &[u8]) -> Result { let pickle_key_length = pickle_key.len(); if pickle_key_length == 32 { - let mut key = Box::new([0u8; 32]); - key.copy_from_slice(pickle_key); - + let mut raw_bytes = [0u8; 32]; + raw_bytes.copy_from_slice(pickle_key); + let key = DehydratedDeviceKey::from_bytes(&raw_bytes); Ok(key) } else { Err(DehydrationError::PickleKeyLength(pickle_key_length)) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 32716cf914a..9a22e9f8d56 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +- Expose new API `DehydratedDevices::get_dehydrated_device_pickle_key`, `DehydratedDevices::save_dehydrated_device_pickle_key` + and `DehydratedDevices::delete_dehydrated_device_pickle_key` to store/load the dehydrated device pickle key. + This allows client to automatically rotate the dehydrated device to avoid one-time-keys exhaustion and to_device accumulation. + [**breaking**] `DehydratedDevices::keys_for_upload` and `DehydratedDevices::rehydrate` now use the `DehydratedDeviceKey` + as parameter instead of a raw byte array. Use `DehydratedDeviceKey::from_bytes` to migrate. + ([#4383](https://github.com/matrix-org/matrix-rust-sdk/pull/4383)) + - Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`. These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors when the sender either did not wish to share or was unable to share the room_key. diff --git a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs index f10a180fc63..5231e1f4418 100644 --- a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs +++ b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs @@ -57,7 +57,7 @@ use tracing::{instrument, trace}; use vodozemac::LibolmPickleError; use crate::{ - store::{CryptoStoreWrapper, MemoryStore, RoomKeyInfo, Store}, + store::{Changes, CryptoStoreWrapper, DehydratedDeviceKey, MemoryStore, RoomKeyInfo, Store}, verification::VerificationMachine, Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError, }; @@ -132,15 +132,49 @@ impl DehydratedDevices { /// private keys of the device. pub async fn rehydrate( &self, - pickle_key: &[u8; 32], + pickle_key: &DehydratedDeviceKey, device_id: &DeviceId, device_data: Raw, ) -> Result { - let pickle_key = expand_pickle_key(pickle_key, device_id); + let pickle_key = expand_pickle_key(pickle_key.inner.as_ref(), device_id); let rehydrated = self.inner.rehydrate(&pickle_key, device_id, device_data).await?; Ok(RehydratedDevice { rehydrated, original: self.inner.to_owned() }) } + + /// Get the cached dehydrated device pickle key if any. + /// + /// None if the key was not previously cached (via + /// [`DehydratedDevices::save_dehydrated_device_pickle_key`]). + /// + /// Should be used to periodically rotate the dehydrated device to avoid + /// one-time keys exhaustion and accumulation of to_device messages. + pub async fn get_dehydrated_device_pickle_key( + &self, + ) -> Result, DehydrationError> { + Ok(self.inner.store().load_dehydrated_device_pickle_key().await?) + } + + /// Store the dehydrated device pickle key in the crypto store. + /// + /// This is useful if the client wants to periodically rotate dehydrated + /// devices to avoid one-time keys exhaustion and accumulated to_device + /// problems. + pub async fn save_dehydrated_device_pickle_key( + &self, + dehydrated_device_pickle_key: &DehydratedDeviceKey, + ) -> Result<(), DehydrationError> { + let changes = Changes { + dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key.clone()), + ..Default::default() + }; + Ok(self.inner.store().save_changes(changes).await?) + } + + /// Deletes the previously stored dehydrated device pickle key. + pub async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), DehydrationError> { + Ok(self.inner.store().delete_dehydrated_device_pickle_key().await?) + } } /// A rehydraded device. @@ -170,7 +204,7 @@ impl RehydratedDevice { /// /// ```no_run /// # use anyhow::Result; - /// # use matrix_sdk_crypto::OlmMachine; + /// # use matrix_sdk_crypto::{ OlmMachine, store::DehydratedDeviceKey }; /// # use ruma::{api::client::dehydrated_device, DeviceId}; /// # async fn example() -> Result<()> { /// # let machine: OlmMachine = unimplemented!(); @@ -184,9 +218,9 @@ impl RehydratedDevice { /// ) -> Result { /// todo!("Download the to-device events of the dehydrated device"); /// } - /// - /// // Don't use a zero key for real. - /// let pickle_key = [0u8; 32]; + /// // Get the cached dehydrated key (got it after verification/recovery) + /// let pickle_key = machine + /// .dehydrated_devices().get_dehydrated_device_pickle_key().await?.unwrap(); /// /// // Fetch the dehydrated device from the server. /// let response = get_dehydrated_device().await?; @@ -285,11 +319,13 @@ impl DehydratedDevice { /// # Examples /// /// ```no_run - /// # use matrix_sdk_crypto::OlmMachine; - /// # async fn example() -> anyhow::Result<()> { + /// # use matrix_sdk_crypto::OlmMachine; /// # + /// use matrix_sdk_crypto::store::DehydratedDeviceKey; + /// + /// async fn example() -> anyhow::Result<()> { /// # let machine: OlmMachine = unimplemented!(); - /// // Don't use a zero key for real. - /// let pickle_key = [0u8; 32]; + /// // Create a new random key + /// let pickle_key = DehydratedDeviceKey::new()?; /// /// // Create the dehydrated device. /// let device = machine.dehydrated_devices().create().await?; @@ -299,6 +335,9 @@ impl DehydratedDevice { /// .keys_for_upload("Dehydrated device".to_owned(), &pickle_key) /// .await?; /// + /// // Save the key if you want to later one rotate the dehydrated device + /// machine.dehydrated_devices().save_dehydrated_device_pickle_key(&pickle_key).await.unwrap(); + /// /// // Send the request out using your HTTP client. /// // client.send(request).await?; /// # Ok(()) @@ -314,7 +353,7 @@ impl DehydratedDevice { pub async fn keys_for_upload( &self, initial_device_display_name: String, - pickle_key: &[u8; 32], + pickle_key: &DehydratedDeviceKey, ) -> Result { let mut transaction = self.store.transaction().await; @@ -330,7 +369,8 @@ impl DehydratedDevice { trace!("Creating an upload request for a dehydrated device"); - let pickle_key = expand_pickle_key(pickle_key, &self.store.static_account().device_id); + let pickle_key = + expand_pickle_key(pickle_key.inner.as_ref(), &self.store.static_account().device_id); let device_id = self.store.static_account().device_id.clone(); let device_data = account.dehydrate(&pickle_key); let initial_device_display_name = Some(initial_device_display_name); @@ -393,12 +433,15 @@ mod tests { tests::to_device_requests_to_content, }, olm::OutboundGroupSession, + store::DehydratedDeviceKey, types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType}, utilities::json_convert, EncryptionSettings, OlmMachine, }; - const PICKLE_KEY: &[u8; 32] = &[0u8; 32]; + fn pickle_key() -> DehydratedDeviceKey { + DehydratedDeviceKey::from_bytes(&[0u8; 32]) + } fn user_id() -> &'static UserId { user_id!("@alice:localhost") @@ -467,7 +510,7 @@ mod tests { let dehydrated_device = olm_machine.dehydrated_devices().create().await.unwrap(); let request = dehydrated_device - .keys_for_upload("Foo".to_owned(), PICKLE_KEY) + .keys_for_upload("Foo".to_owned(), &pickle_key()) .await .expect("We should be able to create a request to upload a dehydrated device"); @@ -497,7 +540,7 @@ mod tests { let dehydrated_device = alice.dehydrated_devices().create().await.unwrap(); let mut request = dehydrated_device - .keys_for_upload("Foo".to_owned(), PICKLE_KEY) + .keys_for_upload("Foo".to_owned(), &pickle_key()) .await .expect("We should be able to create a request to upload a dehydrated device"); @@ -531,7 +574,7 @@ mod tests { // Rehydrate the device. let rehydrated = bob .dehydrated_devices() - .rehydrate(PICKLE_KEY, &request.device_id, request.device_data) + .rehydrate(&pickle_key(), &request.device_id, request.device_data) .await .expect("We should be able to rehydrate the device"); @@ -561,4 +604,43 @@ mod tests { "The session ids of the imported room key and the outbound group session should match" ); } + + #[async_test] + async fn test_dehydrated_device_pickle_key_cache() { + let alice = get_olm_machine().await; + + let dehydrated_manager = alice.dehydrated_devices(); + + let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap(); + assert!(stored_key.is_none()); + + let pickle_key = DehydratedDeviceKey::new().unwrap(); + + dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap(); + + let stored_key = + dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap().unwrap(); + assert_eq!(stored_key.to_base64(), pickle_key.to_base64()); + + let dehydrated_device = dehydrated_manager.create().await.unwrap(); + + let request = dehydrated_device + .keys_for_upload("Foo".to_owned(), &stored_key) + .await + .expect("We should be able to create a request to upload a dehydrated device"); + + // Rehydrate the device. + dehydrated_manager + .rehydrate(&stored_key, &request.device_id, request.device_data) + .await + .expect("We should be able to rehydrate the device"); + + dehydrated_manager + .delete_dehydrated_device_pickle_key() + .await + .expect("Should be able to delete the dehydrated device key"); + + let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap(); + assert!(stored_key.is_none()); + } } diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index a84442f4525..410d8dbde04 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -51,7 +51,7 @@ macro_rules! cryptostore_integration_tests { PrivateCrossSigningIdentity, SenderData, SenderDataType, Session }, store::{ - BackupDecryptionKey, Changes, CryptoStore, DeviceChanges, GossipRequest, + BackupDecryptionKey, Changes, CryptoStore, DehydratedDeviceKey, DeviceChanges, GossipRequest, IdentityChanges, PendingChanges, RoomSettings, }, testing::{get_device, get_other_identity, get_own_identity}, @@ -1217,6 +1217,53 @@ macro_rules! cryptostore_integration_tests { assert!(restored.backup_version.is_some(), "The backup version should now be Some as well"); } + #[async_test] + async fn test_dehydration_pickle_key_saving() { + let (_account, store) = get_loaded_store("dehydration_key_saving").await; + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_none(), "Initially no pickle key should be present"); + + let dehydrated_device_pickle_key = Some(DehydratedDeviceKey::new().unwrap()); + let exported_base64 = dehydrated_device_pickle_key.clone().unwrap().to_base64(); + + let changes = Changes { dehydrated_device_pickle_key, ..Default::default() }; + store.save_changes(changes).await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_some(), "We should be able to restore a pickle key"); + assert_eq!(restored.unwrap().to_base64(), exported_base64); + + // If None, should not clear the existing saved key + let changes = Changes { dehydrated_device_pickle_key: None, ..Default::default() }; + store.save_changes(changes).await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_some(), "We should be able to restore a pickle key"); + assert_eq!(restored.unwrap().to_base64(), exported_base64); + + } + + #[async_test] + async fn test_delete_dehydration_pickle_key() { + let (_account, store) = get_loaded_store("dehydration_key_saving").await; + + let dehydrated_device_pickle_key = DehydratedDeviceKey::new().unwrap(); + + let changes = Changes { dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key), ..Default::default() }; + store.save_changes(changes).await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_some(), "We should be able to restore a pickle key"); + + store.delete_dehydrated_device_pickle_key().await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_none(), "The previously saved key should be deleted"); + + } + + #[async_test] async fn test_custom_value_saving() { let (_, store) = get_loaded_store("custom_value_saving").await; diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 8d5da350300..90557acf660 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -31,8 +31,8 @@ use vodozemac::Curve25519PublicKey; use super::{ caches::{DeviceStore, GroupSessionStore}, - Account, BackupKeys, Changes, CryptoStore, InboundGroupSession, PendingChanges, RoomKeyCounts, - RoomSettings, Session, + Account, BackupKeys, Changes, CryptoStore, DehydratedDeviceKey, InboundGroupSession, + PendingChanges, RoomKeyCounts, RoomSettings, Session, }; use crate::{ gossiping::{GossipRequest, GossippedSecret, SecretInfo}, @@ -93,6 +93,7 @@ pub struct MemoryStore { leases: StdRwLock>, secret_inbox: StdRwLock>>, backup_keys: RwLock, + dehydrated_device_pickle_key: RwLock>, next_batch_token: RwLock>, room_settings: StdRwLock>, } @@ -116,6 +117,7 @@ impl Default for MemoryStore { custom_values: Default::default(), leases: Default::default(), backup_keys: Default::default(), + dehydrated_device_pickle_key: Default::default(), secret_inbox: Default::default(), next_batch_token: Default::default(), room_settings: Default::default(), @@ -268,6 +270,11 @@ impl CryptoStore for MemoryStore { self.backup_keys.write().await.backup_version = Some(version); } + if let Some(pickle_key) = changes.dehydrated_device_pickle_key { + let mut lock = self.dehydrated_device_pickle_key.write().await; + *lock = Some(pickle_key); + } + { let mut secret_inbox = self.secret_inbox.write().unwrap(); for secret in changes.secrets { @@ -486,6 +493,16 @@ impl CryptoStore for MemoryStore { Ok(self.backup_keys.read().await.to_owned()) } + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + Ok(self.dehydrated_device_pickle_key.read().await.to_owned()) + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> { + let mut lock = self.dehydrated_device_pickle_key.write().await; + *lock = None; + Ok(()) + } + async fn get_outbound_group_session( &self, room_id: &RoomId, @@ -1125,7 +1142,10 @@ mod integration_tests { InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, StaticAccountData, }, - store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings}, + store::{ + BackupKeys, Changes, CryptoStore, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, + RoomSettings, + }, types::events::room_key_withheld::RoomKeyWithheldEvent, Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, Session, TrackedUser, UserIdentityData, @@ -1288,6 +1308,16 @@ mod integration_tests { self.0.load_backup_keys().await } + async fn load_dehydrated_device_pickle_key( + &self, + ) -> Result, Self::Error> { + self.0.load_dehydrated_device_pickle_key().await + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error> { + self.0.delete_dehydrated_device_pickle_key().await + } + async fn get_outbound_group_session( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 61427b13895..0fa133824c8 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -518,6 +518,7 @@ pub struct Changes { pub private_identity: Option, pub backup_version: Option, pub backup_decryption_key: Option, + pub dehydrated_device_pickle_key: Option, pub sessions: Vec, pub message_hashes: Vec, pub inbound_group_sessions: Vec, @@ -550,6 +551,7 @@ impl Changes { self.private_identity.is_none() && self.backup_version.is_none() && self.backup_decryption_key.is_none() + && self.dehydrated_device_pickle_key.is_none() && self.sessions.is_empty() && self.message_hashes.is_empty() && self.inbound_group_sessions.is_empty() @@ -749,6 +751,51 @@ impl Debug for BackupDecryptionKey { } } +/// The pickle key used to safely store the dehydrated device pickle. +/// +/// This input key material will be expanded using HKDF into an AES key, MAC +/// key, and an initialization vector (IV). +#[derive(Clone, Zeroize, ZeroizeOnDrop, Deserialize, Serialize)] +#[serde(transparent)] +pub struct DehydratedDeviceKey { + pub(crate) inner: Box<[u8; DehydratedDeviceKey::KEY_SIZE]>, +} + +impl DehydratedDeviceKey { + /// The number of bytes the encryption key will hold. + pub const KEY_SIZE: usize = 32; + + /// Generates a new random pickle key. + pub fn new() -> Result { + let mut rng = rand::thread_rng(); + + let mut key = Box::new([0u8; Self::KEY_SIZE]); + rand::Fill::try_fill(key.as_mut_slice(), &mut rng)?; + + Ok(Self { inner: key }) + } + + /// Creates a dehydration pickle key from the given bytes. + pub fn from_bytes(raw_key: &[u8; 32]) -> Self { + let mut inner = Box::new([0u8; Self::KEY_SIZE]); + inner.copy_from_slice(raw_key); + + Self { inner } + } + + /// Export the [`DehydratedDeviceKey`] as a base64 encoded string. + pub fn to_base64(&self) -> String { + base64_encode(self.inner.as_slice()) + } +} + +#[cfg(not(tarpaulin_include))] +impl Debug for DehydratedDeviceKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("DehydratedDeviceKey").field(&"...").finish() + } +} + impl DeviceChanges { /// Merge the given `DeviceChanges` into this instance of `DeviceChanges`. pub fn extend(&mut self, other: DeviceChanges) { diff --git a/crates/matrix-sdk-crypto/src/store/traits.rs b/crates/matrix-sdk-crypto/src/store/traits.rs index 3031e33ef8d..3e651061d49 100644 --- a/crates/matrix-sdk-crypto/src/store/traits.rs +++ b/crates/matrix-sdk-crypto/src/store/traits.rs @@ -22,7 +22,8 @@ use ruma::{ use vodozemac::Curve25519PublicKey; use super::{ - BackupKeys, Changes, CryptoStoreError, PendingChanges, Result, RoomKeyCounts, RoomSettings, + BackupKeys, Changes, CryptoStoreError, DehydratedDeviceKey, PendingChanges, Result, + RoomKeyCounts, RoomSettings, }; #[cfg(doc)] use crate::olm::SenderData; @@ -195,6 +196,14 @@ pub trait CryptoStore: AsyncTraitDeps { /// Get the backup keys we have stored. async fn load_backup_keys(&self) -> Result; + /// Get the dehydrated device pickle key we have stored. + async fn load_dehydrated_device_pickle_key( + &self, + ) -> Result, Self::Error>; + + /// Deletes the previously stored dehydrated device pickle key. + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error>; + /// Get the outbound group session we have stored that is used for the /// given room. async fn get_outbound_group_session( @@ -465,6 +474,14 @@ impl CryptoStore for EraseCryptoStoreError { self.0.load_backup_keys().await.map_err(Into::into) } + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + self.0.load_dehydrated_device_pickle_key().await.map_err(Into::into) + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error> { + self.0.delete_dehydrated_device_pickle_key().await.map_err(Into::into) + } + async fn get_outbound_group_session( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 5f4c05532c6..94136c6b601 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -29,8 +29,8 @@ use matrix_sdk_crypto::{ StaticAccountData, }, store::{ - BackupKeys, Changes, CryptoStore, CryptoStoreError, PendingChanges, RoomKeyCounts, - RoomSettings, + BackupKeys, Changes, CryptoStore, CryptoStoreError, DehydratedDeviceKey, PendingChanges, + RoomKeyCounts, RoomSettings, }, types::events::room_key_withheld::RoomKeyWithheldEvent, vodozemac::base64_encode, @@ -104,6 +104,9 @@ mod keys { /// with the client-side recovery key, which is actually an AES key for use /// with SSSS. pub const RECOVERY_KEY_V1: &str = "recovery_key_v1"; + + /// Indexeddb key for the dehydrated device pickle key. + pub const DEHYDRATION_PICKLE_KEY: &str = "dehydration_pickle_key"; } /// An implementation of [CryptoStore] that uses [IndexedDB] for persistent @@ -471,6 +474,7 @@ impl IndexeddbCryptoStore { let decryption_key_pickle = &changes.backup_decryption_key; let backup_version = &changes.backup_version; + let dehydration_pickle_key = &changes.dehydrated_device_pickle_key; let mut core = indexeddb_changes.get(keys::CORE); if let Some(next_batch) = &changes.next_batch_token { @@ -487,6 +491,13 @@ impl IndexeddbCryptoStore { ); } + if let Some(i) = &dehydration_pickle_key { + core.put( + JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY), + self.serializer.serialize_value(i)?, + ); + } + if let Some(a) = &decryption_key_pickle { indexeddb_changes.get(keys::BACKUP_KEYS).put( JsValue::from_str(keys::RECOVERY_KEY_V1), @@ -1291,6 +1302,28 @@ impl_crypto_store! { Ok(key) } + + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + if let Some(pickle) = self + .inner + .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)? + .object_store(keys::CORE)? + .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))? + .await? + { + let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?; + + Ok(Some(pickle)) + } else { + Ok(None) + } + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> { + self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?; + Ok(()) + } + async fn get_withheld_info( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index b16a05e1800..aa4e3e9d211 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -27,7 +27,10 @@ use matrix_sdk_crypto::{ InboundGroupSession, OutboundGroupSession, PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session, StaticAccountData, }, - store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings}, + store::{ + BackupKeys, Changes, CryptoStore, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, + RoomSettings, + }, types::events::room_key_withheld::RoomKeyWithheldEvent, Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData, }; @@ -189,6 +192,9 @@ impl SqliteCryptoStore { const DATABASE_VERSION: u8 = 9; +/// key for the dehydrated device pickle key in the key/value table. +const DEHYDRATED_DEVICE_PICKLE_KEY: &str = "dehydrated_device_pickle_key"; + /// Run migrations for the given version of the database. async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { if version == 0 { @@ -846,6 +852,11 @@ impl CryptoStore for SqliteCryptoStore { txn.set_kv("backup_version_v1", &serialized_backup_version)?; } + if let Some(pickle_key) = &changes.dehydrated_device_pickle_key { + let serialized_pickle_key = this.serialize_value(pickle_key)?; + txn.set_kv(DEHYDRATED_DEVICE_PICKLE_KEY, &serialized_pickle_key)?; + } + for device in changes.devices.new.iter().chain(&changes.devices.changed) { let user_id = this.encode_key("device", device.user_id().as_bytes()); let device_id = this.encode_key("device", device.device_id().as_bytes()); @@ -1091,6 +1102,21 @@ impl CryptoStore for SqliteCryptoStore { Ok(BackupKeys { backup_version, decryption_key }) } + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + let conn = self.acquire().await?; + + conn.get_kv(DEHYDRATED_DEVICE_PICKLE_KEY) + .await? + .map(|value| self.deserialize_value(&value)) + .transpose() + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error> { + let conn = self.acquire().await?; + conn.clear_kv(DEHYDRATED_DEVICE_PICKLE_KEY).await?; + + Ok(()) + } async fn get_outbound_group_session( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-sqlite/src/utils.rs b/crates/matrix-sdk-sqlite/src/utils.rs index 04f549f8f3d..7daa9358baf 100644 --- a/crates/matrix-sdk-sqlite/src/utils.rs +++ b/crates/matrix-sdk-sqlite/src/utils.rs @@ -257,6 +257,9 @@ pub(crate) trait SqliteKeyValueStoreConnExt { /// Store the given value for the given key. fn set_kv(&self, key: &str, value: &[u8]) -> rusqlite::Result<()>; + /// Removes the current key and value if exists. + fn clear_kv(&self, key: &str) -> rusqlite::Result<()>; + /// Set the version of the database. fn set_db_version(&self, version: u8) -> rusqlite::Result<()> { self.set_kv("version", &[version]) @@ -271,6 +274,11 @@ impl SqliteKeyValueStoreConnExt for rusqlite::Connection { )?; Ok(()) } + + fn clear_kv(&self, key: &str) -> rusqlite::Result<()> { + self.execute("DELETE FROM kv WHERE key = ?1", (key,))?; + Ok(()) + } } /// Extension trait for an [`SqliteAsyncConn`] that contains a key-value @@ -307,6 +315,9 @@ pub(crate) trait SqliteKeyValueStoreAsyncConnExt: SqliteAsyncConnExt { /// Store the given value for the given key. async fn set_kv(&self, key: &str, value: Vec) -> rusqlite::Result<()>; + /// Clears the given value for the given key. + async fn clear_kv(&self, key: &str) -> rusqlite::Result<()>; + /// Get the version of the database. async fn db_version(&self) -> Result { let kv_exists = self.kv_table_exists().await.map_err(OpenStoreError::LoadVersion)?; @@ -353,6 +364,13 @@ impl SqliteKeyValueStoreAsyncConnExt for SqliteAsyncConn { Ok(()) } + + async fn clear_kv(&self, key: &str) -> rusqlite::Result<()> { + let key = key.to_owned(); + self.interact(move |conn| conn.clear_kv(&key)).await.unwrap()?; + + Ok(()) + } } /// Repeat `?` n times, where n is defined by `count`. `?` are comma-separated. From a573b650c9ad7b3ce44e6c218e896095cbdce79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 15 Dec 2024 12:22:53 +0100 Subject: [PATCH 779/979] chore(sdk): Remove image-rayon cargo feature check from build.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cargo feature was removed, but the build script was forgotten. Signed-off-by: Kévin Commaille --- crates/matrix-sdk/build.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/matrix-sdk/build.rs b/crates/matrix-sdk/build.rs index 1a25f041e4e..b95a1276cb1 100644 --- a/crates/matrix-sdk/build.rs +++ b/crates/matrix-sdk/build.rs @@ -39,9 +39,5 @@ fn main() { !env_is_set("CARGO_FEATURE_SSO_LOGIN"), "feature 'sso-login' is not available on target arch 'wasm32'", ); - ensure( - !env_is_set("CARGO_FEATURE_IMAGE_RAYON"), - "feature 'image-rayon' is not available on target arch 'wasm32'", - ); } } From b6542477bbd32a4f832a04c2a301aec410adbb13 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 11 Dec 2024 12:45:28 +0100 Subject: [PATCH 780/979] task(event cache): make the code more concise in back-pagination --- .../matrix-sdk/src/event_cache/pagination.rs | 148 ++++++++---------- 1 file changed, 65 insertions(+), 83 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 793c7d700af..2eb8a7d2c88 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -137,105 +137,87 @@ impl RoomPagination { // Check that the previous token still exists; otherwise it's a sign that the // room's timeline has been cleared. - let gap_identifier = if let Some(token) = prev_token { - let gap_identifier = state.events().chunk_identifier(|chunk| { + let prev_gap_id = if let Some(token) = prev_token { + let gap_id = state.events().chunk_identifier(|chunk| { matches!(chunk.content(), ChunkContent::Gap(Gap { ref prev_token }) if *prev_token == token) }); - // The method has been called with `token` but it doesn't exist in `RoomEvents`, - // it's an error. - if gap_identifier.is_none() { + // We got a previous-batch token from the linked chunk *before* running the + // request, which is missing from the linked chunk *after* + // completing the request. It may be a sign the linked chunk has + // been reset, and it's an error in any case. + if gap_id.is_none() { return Ok(None); } - gap_identifier + gap_id } else { None }; - let prev_token = paginator.prev_batch_token().map(|prev_token| Gap { prev_token }); - - Ok(Some(state.with_events_mut(move |room_events| { - // Note: The chunk could be empty. - // - // If there's any event, they are presented in reverse order (i.e. the first one - // should be prepended first). - - let sync_events = events - .iter() - // Reverse the order of the events as `/messages` has been called with `dir=b` - // (backward). The `RoomEvents` API expects the first event to be the oldest. - .rev() - .cloned() - .map(SyncTimelineEvent::from); - - - // There is a `token`/gap, let's replace it by new events! - if let Some(gap_identifier) = gap_identifier { - let new_position = { - // Replace the gap by new events. - let new_chunk = room_events - .replace_gap_at(sync_events, gap_identifier) - // SAFETY: we are sure that `gap_identifier` represents a valid - // `ChunkIdentifier` for a `Gap` chunk. - .expect("The `gap_identifier` must represent a `Gap`"); - - new_chunk.first_position() - }; - - // And insert a new gap if there is any `prev_token`. - if let Some(prev_token_gap) = prev_token { - room_events - .insert_gap_at(prev_token_gap, new_position) - // SAFETY: we are sure that `new_position` represents a valid - // `ChunkIdentifier` for an `Item` chunk. - .expect("The `new_position` must represent an `Item`"); - } - - trace!("replaced gap with new events from backpagination"); + let new_gap = paginator.prev_batch_token().map(|prev_token| Gap { prev_token }); + + Ok(Some( + state + .with_events_mut(move |room_events| { + // Note: The chunk could be empty. + // + // If there's any event, they are presented in reverse order (i.e. the first one + // should be prepended first). + + let sync_events = events + .iter() + // Reverse the order of the events as `/messages` has been called with + // `dir=b` (backward). The `RoomEvents` API expects + // the first event to be the oldest. + .rev() + .cloned() + .map(SyncTimelineEvent::from); + + // There is a prior gap, let's replace it by new events! + if let Some(gap_id) = prev_gap_id { + // Replace the gap chunk with an items chunk containing the new events. + let events_chunk_pos = room_events + .replace_gap_at(sync_events, gap_id) + .expect("gap_identifier is a valid chunk id we read previously") + .first_position(); + + // And insert a new gap if needs be. + if let Some(new_gap) = new_gap { + room_events + .insert_gap_at(new_gap, events_chunk_pos) + .expect("events_chunk_pos represents a valid chunk position"); + } + + trace!("replaced gap with new events from backpagination"); + return BackPaginationOutcome { events, reached_start }; + } - // TODO: implement smarter reconciliation later - //let _ = self.sender.send(RoomEventCacheUpdate::Prepend { events }); + // There is no known previous gap. Let's assume we must prepend the new events. + let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); - return BackPaginationOutcome { events, reached_start }; - } + if let Some(pos) = first_event_pos { + if let Some(new_gap) = new_gap { + room_events + .insert_gap_at(new_gap, pos) + .expect("pos is a valid position we just read above"); + } - // There is no `token`/gap identifier. Let's assume we must prepend the new - // events. - let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); - - match first_event_pos { - // Is there a first item? Insert at this position. - Some(first_event_pos) => { - if let Some(prev_token_gap) = prev_token { room_events - .insert_gap_at(prev_token_gap, first_event_pos) - // SAFETY: The `first_event_pos` can only be an `Item` chunk, it's - // an invariant of `LinkedChunk`. Also, it can only represent a valid - // `ChunkIdentifier` as the data structure isn't modified yet. - .expect("`first_event_pos` must point to a valid `Item` chunk when inserting a gap"); + .insert_events_at(sync_events, pos) + .expect("pos is a valid position we just read above"); + } else { + if let Some(prev_token_gap) = new_gap { + room_events.push_gap(prev_token_gap); + } + + room_events.push_events(sync_events); } - room_events - .insert_events_at(sync_events, first_event_pos) - // SAFETY: The `first_event_pos` can only be an `Item` chunk, it's - // an invariant of `LinkedChunk`. The chunk it points to has not been - // removed. - .expect("The `first_event_pos` must point to a valid `Item` chunk when inserting events"); - } - - // There is no first item. Let's simply push. - None => { - if let Some(prev_token_gap) = prev_token { - room_events.push_gap(prev_token_gap); - } - - room_events.push_events(sync_events); - } - } - - BackPaginationOutcome { events, reached_start } - }).await?)) + BackPaginationOutcome { events, reached_start } + }) + .await?, + )) } /// Get the latest pagination token, as stored in the room events linked From a052a79aaf0d79a1340403b1877569ab02456143 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 11 Dec 2024 13:09:51 +0100 Subject: [PATCH 781/979] fix(event cache): store the gap *before* events, after back-paginating The conditions required to cause the bug might have been impossible to reach in the real world, because it assumes a mix of: - events present in the linked chunk - no prev-batch token However: now that we have storage, we could end up in this situation, when reaching the start of the timeline (since there'll be no previous gap in that case). We need to handle that better in the linked chunk representation itself, but in the meanwhile, we should insert the gap and the events in a relative correct order. --- .../matrix-sdk/src/event_cache/pagination.rs | 9 +- .../tests/integration/event_cache.rs | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 2eb8a7d2c88..129acca5989 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -197,15 +197,16 @@ impl RoomPagination { let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); if let Some(pos) = first_event_pos { + room_events + .insert_events_at(sync_events, pos) + .expect("pos is a valid position we just read above"); + if let Some(new_gap) = new_gap { + // Insert the gap, before the events we just inserted at pos. room_events .insert_gap_at(new_gap, pos) .expect("pos is a valid position we just read above"); } - - room_events - .insert_events_at(sync_events, pos) - .expect("pos is a valid position we just read above"); } else { if let Some(prev_token_gap) = new_gap { room_events.push_gap(prev_token_gap); diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 95490990741..856e3d24022 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -857,3 +857,85 @@ async fn test_limited_timeline_with_storage() { // That's all, folks! assert!(subscriber.is_empty()); } + +#[async_test] +async fn test_backpaginate_with_no_initial_events() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + + let room_id = room_id!("!omelette:fromage.fr"); + + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + + // Start with a room with an event, but no prev-batch token. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("hello").event_id(event_id!("$3"))), + ) + .await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); + wait_for_initial_events(events, &mut stream).await; + + // The first back-pagination will return these two events. + // + // Note: it's important to return the same event that came from sync: since we + // will back-paginate without a prev-batch token first, we'll back-paginate + // from the end of the timeline, which must include the event we got from + // sync. + + server + .mock_room_messages() + .ok( + "start-token-unused1".to_owned(), + Some("prev_batch".to_owned()), + vec![ + f.text_msg("world").event_id(event_id!("$2")), + f.text_msg("hello").event_id(event_id!("$3")), + ], + Vec::new(), + ) + .mock_once() + .mount() + .await; + + // The second round of back-pagination will return this one. + server + .mock_room_messages() + .from("prev_batch") + .ok( + "start-token-unused2".to_owned(), + None, + vec![f.text_msg("oh well").event_id(event_id!("$1"))], + Vec::new(), + ) + .mock_once() + .mount() + .await; + + let pagination = room_event_cache.pagination(); + + // Run pagination: since there's no token, we'll wait a bit for a sync to return + // one, and since there's none, we'll end up starting from the end of the + // timeline. + pagination.run_backwards(20, once).await.unwrap(); + // Second pagination will be instant. + pagination.run_backwards(20, once).await.unwrap(); + + // The linked chunk should contain the events in the correct order. + let (events, _stream) = room_event_cache.subscribe().await.unwrap(); + + assert_event_matches_msg(&events[0], "oh well"); + assert_event_matches_msg(&events[1], "hello"); + assert_event_matches_msg(&events[2], "world"); + assert_eq!(events.len(), 3); +} From ed34719295b18bdd7664399a54ccc149f54651fa Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 11 Dec 2024 13:20:16 +0100 Subject: [PATCH 782/979] task(event cache): simplify handling a back-pagination result --- .../matrix-sdk/src/event_cache/pagination.rs | 61 +++++++++---------- .../matrix-sdk/src/event_cache/room/events.rs | 2 +- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 129acca5989..2b219c25e8c 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -174,45 +174,44 @@ impl RoomPagination { .cloned() .map(SyncTimelineEvent::from); - // There is a prior gap, let's replace it by new events! - if let Some(gap_id) = prev_gap_id { - // Replace the gap chunk with an items chunk containing the new events. - let events_chunk_pos = room_events - .replace_gap_at(sync_events, gap_id) - .expect("gap_identifier is a valid chunk id we read previously") - .first_position(); - - // And insert a new gap if needs be. - if let Some(new_gap) = new_gap { - room_events - .insert_gap_at(new_gap, events_chunk_pos) - .expect("events_chunk_pos represents a valid chunk position"); - } - - trace!("replaced gap with new events from backpagination"); - return BackPaginationOutcome { events, reached_start }; - } - - // There is no known previous gap. Let's assume we must prepend the new events. let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); - if let Some(pos) = first_event_pos { + // First, insert events. + let insert_new_gap_pos = if let Some(gap_id) = prev_gap_id { + // There is a prior gap, let's replace it by new events! + trace!("replaced gap with new events from backpagination"); + Some( + room_events + .replace_gap_at(sync_events, gap_id) + .expect("gap_identifier is a valid chunk id we read previously") + .first_position(), + ) + } else if let Some(pos) = first_event_pos + { + // No prior gap, but we had some events: assume we need to prepend events + // before those. + trace!("inserted events before the first known event"); room_events .insert_events_at(sync_events, pos) .expect("pos is a valid position we just read above"); + Some(pos) + } else { + // No prior gap, and no prior events: push the events. + trace!("pushing events received from back-pagination, as there were no previous events"); + room_events.push_events(sync_events); + // A new gap may be inserted before the new events, if there are any. + room_events.events().next().map(|(item_pos, _)| item_pos) + }; - if let Some(new_gap) = new_gap { - // Insert the gap, before the events we just inserted at pos. + // And insert the new gap if needs be. + if let Some(new_gap) = new_gap { + if let Some(new_pos) = insert_new_gap_pos { room_events - .insert_gap_at(new_gap, pos) - .expect("pos is a valid position we just read above"); - } - } else { - if let Some(prev_token_gap) = new_gap { - room_events.push_gap(prev_token_gap); + .insert_gap_at(new_gap, new_pos) + .expect("events_chunk_pos represents a valid chunk position"); + } else { + room_events.push_gap(new_gap); } - - room_events.push_events(sync_events); } BackPaginationOutcome { events, reached_start } diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index f194349ff35..24c1d9ba3b7 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -111,7 +111,7 @@ impl RoomEvents { /// Push a gap after all events or gaps. pub fn push_gap(&mut self, gap: Gap) { - self.chunks.push_gap_back(gap) + self.chunks.push_gap_back(gap); } /// Insert events at a specified position. From c197808b428b575d47be532e32262092c0b9bf50 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 11 Dec 2024 13:22:18 +0100 Subject: [PATCH 783/979] task(event cache): get rid of one level of indent --- .../matrix-sdk/src/event_cache/pagination.rs | 112 +++++++++--------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 2b219c25e8c..064acd5ae00 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -157,67 +157,65 @@ impl RoomPagination { let new_gap = paginator.prev_batch_token().map(|prev_token| Gap { prev_token }); - Ok(Some( - state - .with_events_mut(move |room_events| { - // Note: The chunk could be empty. - // - // If there's any event, they are presented in reverse order (i.e. the first one - // should be prepended first). - - let sync_events = events - .iter() - // Reverse the order of the events as `/messages` has been called with - // `dir=b` (backward). The `RoomEvents` API expects - // the first event to be the oldest. - .rev() - .cloned() - .map(SyncTimelineEvent::from); - - let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); - - // First, insert events. - let insert_new_gap_pos = if let Some(gap_id) = prev_gap_id { - // There is a prior gap, let's replace it by new events! - trace!("replaced gap with new events from backpagination"); - Some( - room_events - .replace_gap_at(sync_events, gap_id) - .expect("gap_identifier is a valid chunk id we read previously") - .first_position(), - ) - } else if let Some(pos) = first_event_pos - { - // No prior gap, but we had some events: assume we need to prepend events - // before those. - trace!("inserted events before the first known event"); + let result = state + .with_events_mut(move |room_events| { + // Note: The chunk could be empty. + // + // If there's any event, they are presented in reverse order (i.e. the first one + // should be prepended first). + + let sync_events = events + .iter() + // Reverse the order of the events as `/messages` has been called with `dir=b` + // (backward). The `RoomEvents` API expects the first event to be the oldest. + .rev() + .cloned() + .map(SyncTimelineEvent::from); + + let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); + + // First, insert events. + let insert_new_gap_pos = if let Some(gap_id) = prev_gap_id { + // There is a prior gap, let's replace it by new events! + trace!("replaced gap with new events from backpagination"); + Some( room_events - .insert_events_at(sync_events, pos) - .expect("pos is a valid position we just read above"); - Some(pos) + .replace_gap_at(sync_events, gap_id) + .expect("gap_identifier is a valid chunk id we read previously") + .first_position(), + ) + } else if let Some(pos) = first_event_pos { + // No prior gap, but we had some events: assume we need to prepend events + // before those. + trace!("inserted events before the first known event"); + room_events + .insert_events_at(sync_events, pos) + .expect("pos is a valid position we just read above"); + Some(pos) + } else { + // No prior gap, and no prior events: push the events. + trace!("pushing events received from back-pagination"); + room_events.push_events(sync_events); + // A new gap may be inserted before the new events, if there are any. + room_events.events().next().map(|(item_pos, _)| item_pos) + }; + + // And insert the new gap if needs be. + if let Some(new_gap) = new_gap { + if let Some(new_pos) = insert_new_gap_pos { + room_events + .insert_gap_at(new_gap, new_pos) + .expect("events_chunk_pos represents a valid chunk position"); } else { - // No prior gap, and no prior events: push the events. - trace!("pushing events received from back-pagination, as there were no previous events"); - room_events.push_events(sync_events); - // A new gap may be inserted before the new events, if there are any. - room_events.events().next().map(|(item_pos, _)| item_pos) - }; - - // And insert the new gap if needs be. - if let Some(new_gap) = new_gap { - if let Some(new_pos) = insert_new_gap_pos { - room_events - .insert_gap_at(new_gap, new_pos) - .expect("events_chunk_pos represents a valid chunk position"); - } else { - room_events.push_gap(new_gap); - } + room_events.push_gap(new_gap); } + } - BackPaginationOutcome { events, reached_start } - }) - .await?, - )) + BackPaginationOutcome { events, reached_start } + }) + .await?; + + Ok(Some(result)) } /// Get the latest pagination token, as stored in the room events linked From 5a25e65da3faa6188c966083f9ea82614cb8ebc5 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 11 Dec 2024 13:29:11 +0100 Subject: [PATCH 784/979] test(event cache): use the MatrixMockServer --- .../tests/integration/event_cache.rs | 63 ++++++------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 856e3d24022..0832fea4b5d 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -9,19 +9,16 @@ use matrix_sdk::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, - test_utils::{assert_event_matches_msg, logged_in_client_with_server, mocks::MatrixMockServer}, + test_utils::{assert_event_matches_msg, mocks::MatrixMockServer}, }; use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, - SyncResponseBuilder, }; use ruma::{event_id, room_id, user_id}; use serde_json::json; use tokio::{spawn, sync::broadcast}; use wiremock::ResponseTemplate; -use crate::mock_sync; - async fn once( outcome: BackPaginationOutcome, _timeline_has_been_reset: TimelineHasBeenResetWhilePaginating, @@ -31,24 +28,14 @@ async fn once( #[async_test] async fn test_must_explicitly_subscribe() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; let room_id = room_id!("!omelette:fromage.fr"); - { - // Make sure the client is aware of the room. - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; - } - // If I create a room event subscriber for a room before subscribing the event // cache, - let room = client.get_room(room_id).unwrap(); + let room = server.sync_joined_room(&client, room_id).await; let result = room.event_cache().await; // Then it fails, because one must explicitly call `.subscribe()` on the event @@ -58,25 +45,17 @@ async fn test_must_explicitly_subscribe() { #[async_test] async fn test_event_cache_receives_events() { - let (client, server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; // Immediately subscribe the event cache to sync updates. client.event_cache().subscribe().unwrap(); // If I sync and get informed I've joined The Room, but with no events, let room_id = room_id!("!omelette:fromage.fr"); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; + let room = server.sync_joined_room(&client, room_id).await; // If I create a room event subscriber, - - let room = client.get_room(room_id).unwrap(); let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, mut subscriber) = room_event_cache.subscribe().await.unwrap(); @@ -84,23 +63,21 @@ async fn test_event_cache_receives_events() { assert!(events.is_empty()); assert!(subscriber.is_empty()); - let ev_factory = EventFactory::new().sender(user_id!("@dexter:lab.org")); + let f = EventFactory::new().sender(user_id!("@dexter:lab.org")); // And after a sync, yielding updates to two rooms, - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event(ev_factory.text_msg("bonjour monde")), - ); - - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id!("!parallel:universe.uk")) - .add_timeline_event(ev_factory.text_msg("hi i'm learning French")), - ); - - let response_body = sync_builder.build_json_sync_response(); - - mock_sync(&server, response_body, None).await; - client.sync_once(Default::default()).await.unwrap(); - server.reset().await; + server + .mock_sync() + .ok_and_run(&client, |sync_builder| { + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("bonjour monde")), + ); + sync_builder.add_joined_room( + JoinedRoomBuilder::new(room_id!("!parallel:universe.uk")) + .add_timeline_event(f.text_msg("hi i'm learning French")), + ); + }) + .await; // It does receive one update, assert_let_timeout!( From 8d2e672996cdd8700effa7e0588a6bdbdfc0e6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 16 Dec 2024 11:30:30 +0100 Subject: [PATCH 785/979] feat!: Upgrade Ruma to 0.12.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- Cargo.lock | 56 +++++++++++-------------- Cargo.toml | 9 ++-- bindings/matrix-sdk-ffi/src/client.rs | 2 +- crates/matrix-sdk-base/src/rooms/mod.rs | 1 - examples/custom_events/src/main.rs | 2 - 5 files changed, 32 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a44bc67e1c0..80776af1992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3755,7 +3755,7 @@ version = "5.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d385da3c602d29036d2f70beed71c36604df7570be17fed4c5b839616785bf" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "getrandom", "http", @@ -4752,8 +4752,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.11.1" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5100fcaf13d18b9c5c2dfdee5632c428e3201b04ddefd82c930953b461d000a" dependencies = [ "assign", "js_int", @@ -4763,14 +4764,14 @@ dependencies = [ "ruma-events", "ruma-federation-api", "ruma-html", - "ruma-push-gateway-api", "web-time", ] [[package]] name = "ruma-client-api" -version = "0.19.0" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f5929f675a96adb22dcfbab1c527862d7f92a6346a280f2ddcfc6380b19391" dependencies = [ "as_variant", "assign", @@ -4792,8 +4793,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.14.1" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c537899b20312655aa9bf4cd825aaf00dd13203f215df2007bc4fbbeac8d8ba" dependencies = [ "as_variant", "base64 0.22.1", @@ -4824,8 +4826,9 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.29.1" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e4f72eb598c62f51a199bd9218f3fc36a5d50361ecc7a30d864df7bfcef220" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4849,8 +4852,9 @@ dependencies = [ [[package]] name = "ruma-federation-api" -version = "0.10.0" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d70c3d37a8e42992aeaa5786cb406ad302bcd05c0e7e3073d5316b4574340dd" dependencies = [ "http", "js_int", @@ -4863,8 +4867,9 @@ dependencies = [ [[package]] name = "ruma-html" -version = "0.3.0" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3257ce3398e171ff15245767b1a3d201cfc5cce75f5af7ec7f6b8b5e1d2bdb" dependencies = [ "as_variant", "html5ever", @@ -4876,19 +4881,20 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.0" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7f9b534a65698d7db3c08d94bf91de0046fe6c7893a7b360502f65e7011ac4" dependencies = [ "js_int", - "thiserror 2.0.3", + "thiserror 1.0.63", ] [[package]] name = "ruma-macros" -version = "0.14.0" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7bc55ea278668253c9898dd905325bf1f72df4bf2abddd04ff1c99b7b3c4fb" dependencies = [ "cfg-if", - "once_cell", "proc-macro-crate", "proc-macro2", "quote", @@ -4898,18 +4904,6 @@ dependencies = [ "toml 0.8.15", ] -[[package]] -name = "ruma-push-gateway-api" -version = "0.10.0" -source = "git+https://github.com/ruma/ruma?rev=c91499fc464adc865a7c99d0ce0b35982ad96711#c91499fc464adc865a7c99d0ce0b35982ad96711" -dependencies = [ - "js_int", - "ruma-common", - "ruma-events", - "serde", - "serde_json", -] - [[package]] name = "rusqlite" version = "0.32.1" diff --git a/Cargo.toml b/Cargo.toml index 79ae1c835cc..d9f068ae18b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ proptest = { version = "1.5.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "c91499fc464adc865a7c99d0ce0b35982ad96711", features = [ +ruma = { version = "0.12.0", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -71,7 +71,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "c91499fc464adc865a7c99d0ce "unstable-msc4140", "unstable-msc4171", ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "c91499fc464adc865a7c99d0ce0b35982ad96711" } +ruma-common = "0.15.0" serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" @@ -149,7 +149,10 @@ paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", [workspace.lints.rust] rust_2018_idioms = "warn" semicolon_in_expressions_from_macros = "warn" -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage) + 'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro +] } unused_extern_crates = "warn" unused_import_braces = "warn" unused_qualifications = "warn" diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 848d5544c16..54e6ac79bda 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -120,7 +120,7 @@ impl TryFrom for RumaPusherKind { let mut ruma_data = RumaHttpPusherData::new(data.url); if let Some(payload) = data.default_payload { let json: Value = serde_json::from_str(&payload)?; - ruma_data.default_payload = json; + ruma_data.data.insert("default_payload".to_owned(), json); } ruma_data.format = data.format.map(Into::into); Ok(Self::Http(ruma_data)) diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index d86ba3f665b..a426ca7133e 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -1,5 +1,4 @@ #![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage -#![allow(unexpected_cfgs)] // Triggered by the `EventContent` macro usage mod members; pub(crate) mod normal; diff --git a/examples/custom_events/src/main.rs b/examples/custom_events/src/main.rs index c2c54a76923..d88dc5ffa43 100644 --- a/examples/custom_events/src/main.rs +++ b/examples/custom_events/src/main.rs @@ -1,5 +1,3 @@ -#![allow(unexpected_cfgs)] // Triggered by the `EventContent` macro usage - /// /// This is an example showcasing how to build a very simple bot with custom /// events using the matrix-sdk. To try it, you need a rust build setup, then From 2703f7f7d42539bb79b9b476776d4295a950be61 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 16 Dec 2024 12:42:21 +0000 Subject: [PATCH 786/979] crypto: extra logging in `OtherUserIdentity` Add some extra logging in these two methids, to try to narrow down a bug report we received. --- crates/matrix-sdk-crypto/CHANGELOG.md | 6 +++++- crates/matrix-sdk-crypto/src/identities/user.rs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 9a22e9f8d56..af8c51f7e33 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -13,8 +13,12 @@ All notable changes to this project will be documented in this file. as parameter instead of a raw byte array. Use `DehydratedDeviceKey::from_bytes` to migrate. ([#4383](https://github.com/matrix-org/matrix-rust-sdk/pull/4383)) +- Add extra logging in `OtherUserIdentity::pin_current_master_key` and + `OtherUserIdentity::withdraw_verification`. + ([#4415](https://github.com/matrix-org/matrix-rust-sdk/pull/4415)) + - Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`. - These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors + These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors when the sender either did not wish to share or was unable to share the room_key. ([#4305](https://github.com/matrix-org/matrix-rust-sdk/pull/4305)) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 45436c29128..810be3257de 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -31,7 +31,7 @@ use ruma::{ }; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use tracing::error; +use tracing::{error, info}; use crate::{ error::SignatureError, @@ -392,6 +392,7 @@ impl OtherUserIdentity { /// Pin the current identity (public part of the master signing key). pub async fn pin_current_master_key(&self) -> Result<(), CryptoStoreError> { + info!(master_key = ?self.master_key.get_first_key(), "Pinning current identity for user '{}'", self.user_id()); self.inner.pin(); let to_save = UserIdentityData::Other(self.inner.clone()); let changes = Changes { @@ -427,6 +428,7 @@ impl OtherUserIdentity { /// Remove the requirement for this identity to be verified. pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> { + info!(master_key = ?self.master_key.get_first_key(), "Withdrawing verification status and pinning current identity for user '{}'", self.user_id()); self.inner.withdraw_verification(); let to_save = UserIdentityData::Other(self.inner.clone()); let changes = Changes { From 9a899c1cb1fab1e5d83e7df6378743fc3a182d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 10 Dec 2024 16:57:00 +0100 Subject: [PATCH 787/979] feat(room): add 'seen request to join ids' to the stores This will allow us to keep track of which join room requests are marked as 'seen' by the current user and return them as such. Also, add some methods to `Room` to mark new join requests as seen and to get the current ids for the seen join requests. --- Cargo.lock | 12 +++ crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-base/src/rooms/normal.rs | 14 +++- .../matrix-sdk-base/src/store/memory_store.rs | 17 +++++ crates/matrix-sdk-base/src/store/traits.rs | 15 ++++ .../src/state_store/mod.rs | 12 +++ crates/matrix-sdk-sqlite/src/state_store.rs | 11 +++ crates/matrix-sdk/src/room/mod.rs | 74 ++++++++++++++++++- 8 files changed, 150 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80776af1992..36ee61dd332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1676,6 +1676,9 @@ checksum = "d93bd0ebf93d61d6332d3c09a96e97975968a44e19a64c947bde06e6baff383f" dependencies = [ "futures-core", "readlock", + "readlock-tokio", + "tokio", + "tokio-util", "tracing", ] @@ -4540,6 +4543,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "072cfe5b1d2dcd38d20e18f85e9c9978b6cc08f0b373e9f1fff1541335622974" +[[package]] +name = "readlock-tokio" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "867fac64d07214a87e5cf4e88b4ce855844a1cea243534392377d1ac2c911653" +dependencies = [ + "tokio", +] + [[package]] name = "redox_syscall" version = "0.5.3" diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 656beca6ef8..58a9c50e64c 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -51,7 +51,7 @@ assert_matches2 = { workspace = true, optional = true } async-trait = { workspace = true } bitflags = { version = "2.6.0", features = ["serde"] } decancer = "3.2.8" -eyeball = { workspace = true } +eyeball = { workspace = true, features = ["async-lock"] } eyeball-im = { workspace = true } futures-util = { workspace = true } growable-bloom-filter = { workspace = true } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 1c8e1f1ea64..a9c5b90aedf 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -22,7 +22,7 @@ use std::{ use as_variant::as_variant; use bitflags::bitflags; -use eyeball::{SharedObservable, Subscriber}; +use eyeball::{AsyncLock, ObservableWriteGuard, SharedObservable, Subscriber}; use futures_util::{Stream, StreamExt}; #[cfg(feature = "experimental-sliding-sync")] use matrix_sdk_common::deserialized_responses::TimelineEventKind; @@ -52,7 +52,7 @@ use ruma::{ }, tag::{TagEventContent, Tags}, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, - RoomAccountDataEventType, SyncStateEvent, + RoomAccountDataEventType, StateEventType, SyncStateEvent, }, room::RoomType, serde::Raw, @@ -77,7 +77,8 @@ use crate::{ read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, sync::UnreadNotificationsCount, - Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, + Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, StateStoreDataKey, + StateStoreDataValue, StoreError, }; /// Indicates that a notable update of `RoomInfo` has been applied, and why. @@ -167,6 +168,12 @@ pub struct Room { /// to disk but held in memory. #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] pub latest_encrypted_events: Arc>>>, + + /// A map for ids of room membership events in the knocking state linked to + /// the user id of the user affected by the member event, that the current + /// user has marked as seen so they can be ignored. + pub seen_knock_request_ids_map: + SharedObservable>, AsyncLock>, } /// The room summary containing member counts and members that should be used to @@ -289,6 +296,7 @@ impl Room { Self::MAX_ENCRYPTED_EVENTS, ))), room_info_notable_update_sender, + seen_knock_request_ids_map: SharedObservable::new_async(None), } } diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index abc5fca09b4..4f98c42d4e8 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -82,6 +82,7 @@ struct MemoryStoreInner { custom: HashMap, Vec>, send_queue_events: BTreeMap>, dependent_send_queue_events: BTreeMap>, + seen_knock_requests: BTreeMap>, } /// In-memory, non-persistent implementation of the `StateStore`. @@ -168,6 +169,11 @@ impl StateStore for MemoryStore { StateStoreDataKey::ComposerDraft(room_id) => { inner.composer_drafts.get(room_id).cloned().map(StateStoreDataValue::ComposerDraft) } + StateStoreDataKey::SeenKnockRequests(room_id) => inner + .seen_knock_requests + .get(room_id) + .cloned() + .map(StateStoreDataValue::SeenKnockRequests), }) } @@ -222,6 +228,14 @@ impl StateStore for MemoryStore { .expect("Session data not containing server capabilities"), ); } + StateStoreDataKey::SeenKnockRequests(room_id) => { + inner.seen_knock_requests.insert( + room_id.to_owned(), + value + .into_seen_join_requests() + .expect("Session data is not a set of seen join request ids"), + ); + } } Ok(()) @@ -245,6 +259,9 @@ impl StateStore for MemoryStore { StateStoreDataKey::ComposerDraft(room_id) => { inner.composer_drafts.remove(room_id); } + StateStoreDataKey::SeenKnockRequests(room_id) => { + inner.seen_knock_requests.remove(room_id); + } } Ok(()) } diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 6e34f4fe263..8e2447d4866 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -1022,6 +1022,9 @@ pub enum StateStoreDataValue { /// /// [`ComposerDraft`]: Self::ComposerDraft ComposerDraft(ComposerDraft), + + /// A list of knock request ids marked as seen in a room. + SeenKnockRequests(BTreeMap), } /// Current draft of the composer for the room. @@ -1088,6 +1091,11 @@ impl StateStoreDataValue { pub fn into_server_capabilities(self) -> Option { as_variant!(self, Self::ServerCapabilities) } + + /// Get this value if it is the data for the ignored join requests. + pub fn into_seen_join_requests(self) -> Option> { + as_variant!(self, Self::SeenKnockRequests) + } } /// A key for key-value data. @@ -1117,6 +1125,9 @@ pub enum StateStoreDataKey<'a> { /// /// [`ComposerDraft`]: Self::ComposerDraft ComposerDraft(&'a RoomId), + + /// A list of requests to join in a room marked as seen. + SeenKnockRequests(&'a RoomId), } impl StateStoreDataKey<'_> { @@ -1142,6 +1153,10 @@ impl StateStoreDataKey<'_> { /// Key prefix to use for the [`ComposerDraft`][Self::ComposerDraft] /// variant. pub const COMPOSER_DRAFT: &'static str = "composer_draft"; + + /// Key prefix to use for the + /// [`SeenKnockRequests`][Self::SeenKnockRequests] variant. + pub const SEEN_KNOCK_REQUESTS: &'static str = "seen_knock_requests"; } #[cfg(test)] diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index b8ca7442b27..8de8d22efd0 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -419,6 +419,9 @@ impl IndexeddbStateStore { StateStoreDataKey::ComposerDraft(room_id) => { self.encode_key(keys::KV, (StateStoreDataKey::COMPOSER_DRAFT, room_id)) } + StateStoreDataKey::SeenKnockRequests(room_id) => { + self.encode_key(keys::KV, (StateStoreDataKey::SEEN_KNOCK_REQUESTS, room_id)) + } } } } @@ -537,6 +540,10 @@ impl_state_store!({ .map(|f| self.deserialize_value::(&f)) .transpose()? .map(StateStoreDataValue::ComposerDraft), + StateStoreDataKey::SeenKnockRequests(_) => value + .map(|f| self.deserialize_value::>(&f)) + .transpose()? + .map(StateStoreDataValue::SeenKnockRequests), }; Ok(value) @@ -574,6 +581,11 @@ impl_state_store!({ StateStoreDataKey::ComposerDraft(_) => self.serialize_value( &value.into_composer_draft().expect("Session data not a composer draft"), ), + StateStoreDataKey::SeenKnockRequests(_) => self.serialize_value( + &value + .into_seen_join_requests() + .expect("Session data is not a set of seen join request ids"), + ), }; let tx = diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 36ff843cc71..a8a2e792961 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -390,6 +390,9 @@ impl SqliteStateStore { StateStoreDataKey::ComposerDraft(room_id) => { Cow::Owned(format!("{}:{room_id}", StateStoreDataKey::COMPOSER_DRAFT)) } + StateStoreDataKey::SeenKnockRequests(room_id) => { + Cow::Owned(format!("{}:{room_id}", StateStoreDataKey::SEEN_KNOCK_REQUESTS)) + } }; self.encode_key(keys::KV_BLOB, &*key_s) @@ -995,6 +998,9 @@ impl StateStore for SqliteStateStore { StateStoreDataKey::ComposerDraft(_) => { StateStoreDataValue::ComposerDraft(self.deserialize_value(&data)?) } + StateStoreDataKey::SeenKnockRequests(_) => { + StateStoreDataValue::SeenKnockRequests(self.deserialize_value(&data)?) + } }) }) .transpose() @@ -1029,6 +1035,11 @@ impl StateStore for SqliteStateStore { StateStoreDataKey::ComposerDraft(_) => self.serialize_value( &value.into_composer_draft().expect("Session data not a composer draft"), )?, + StateStoreDataKey::SeenKnockRequests(_) => self.serialize_value( + &value + .into_seen_join_requests() + .expect("Session data is not a set of seen join request ids"), + )?, }; self.acquire() diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 224b64ad352..abff96c2655 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -16,7 +16,7 @@ use std::{ borrow::Borrow, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, ops::Deref, sync::Arc, time::Duration, @@ -3205,6 +3205,48 @@ impl Room { pub fn observe_live_location_shares(&self) -> ObservableLiveLocation { ObservableLiveLocation::new(&self.client, self.room_id()) } + + /// Mark a list of requests to join the room as seen, given their state + /// event ids. + pub async fn mark_join_requests_as_seen(&self, event_ids: &[OwnedEventId]) -> Result<()> { + let mut current_seen_events = self.get_seen_join_request_ids().await?; + + for event_id in event_ids { + current_seen_events.insert(event_id.to_owned()); + } + + self.seen_join_request_ids.set(Some(current_seen_events.clone())); + + self.client + .store() + .set_kv_data( + StateStoreDataKey::SeenJoinRequests(self.room_id()), + StateStoreDataValue::SeenJoinRequests(current_seen_events), + ) + .await + .map_err(Into::into) + } + + /// Get the list of seen requests to join event ids in this room. + pub async fn get_seen_join_request_ids(&self) -> Result> { + let current_join_request_ids = self.seen_join_request_ids.get(); + let current_join_request_ids: HashSet = + if let Some(requests) = current_join_request_ids.as_ref() { + requests.clone() + } else { + let requests = self + .client + .store() + .get_kv_data(StateStoreDataKey::SeenJoinRequests(self.room_id())) + .await? + .and_then(|v| v.into_seen_join_requests()) + .unwrap_or_default(); + + self.seen_join_request_ids.set(Some(requests.clone())); + requests + }; + Ok(current_join_request_ids) + } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] @@ -3495,8 +3537,9 @@ mod tests { use matrix_sdk_base::{store::ComposerDraftType, ComposerDraft, SessionMeta}; use matrix_sdk_test::{ async_test, test_json, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, + DEFAULT_TEST_ROOM_ID, }; - use ruma::{device_id, int, user_id}; + use ruma::{device_id, event_id, int, user_id}; use wiremock::{ matchers::{header, method, path_regex}, Mock, MockServer, ResponseTemplate, @@ -3506,7 +3549,7 @@ mod tests { use crate::{ config::RequestConfig, matrix_auth::{MatrixSession, MatrixSessionTokens}, - test_utils::logged_in_client, + test_utils::{logged_in_client, mocks::MatrixMockServer}, Client, }; @@ -3681,4 +3724,29 @@ mod tests { room.clear_composer_draft().await.unwrap(); assert_eq!(room.load_composer_draft().await.unwrap(), None); } + + #[async_test] + async fn test_mark_join_requests_as_seen() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let event_id = event_id!("$a:b.c"); + + let room = server.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; + + // When loading the initial seen ids, there are none + let seen_ids = + room.get_seen_join_request_ids().await.expect("Couldn't load seen join request ids"); + assert!(seen_ids.is_empty()); + + // We mark a random event id as seen + room.mark_join_requests_as_seen(&[event_id.to_owned()]) + .await + .expect("Couldn't mark join request as seen"); + + // Then we can check it was successfully marked as seen + let seen_ids = + room.get_seen_join_request_ids().await.expect("Couldn't load seen join request ids"); + assert_eq!(seen_ids.len(), 1); + assert_eq!(seen_ids.into_iter().next().expect("No next value"), event_id) + } } From 780c264e593738b0e2a6446b5cf7d3ebf140a30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 10 Dec 2024 17:47:29 +0100 Subject: [PATCH 788/979] feat(room): add `JoinRequest` abstraction This struct is an abstraction over a room member or state event with knock membership. --- .../src/deserialized_responses.rs | 19 +- crates/matrix-sdk/src/room/mod.rs | 2 + crates/matrix-sdk/src/room/request_to_join.rs | 205 ++++++++++++++++++ crates/matrix-sdk/src/test_utils/mocks.rs | 123 +++++++++++ 4 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 crates/matrix-sdk/src/room/request_to_join.rs diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index 183a02da531..1f4bac92903 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -30,7 +30,7 @@ use ruma::{ StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent, }, serde::Raw, - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId, }; use serde::Serialize; use unicode_normalization::UnicodeNormalization; @@ -476,6 +476,23 @@ impl MemberEvent { .unwrap_or_else(|| self.user_id().localpart()), ) } + + /// The optional reason why the membership changed. + pub fn reason(&self) -> Option<&str> { + match self { + MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(), + MemberEvent::Stripped(e) => e.content.reason.as_deref(), + _ => None, + } + } + + /// The optional timestamp for this member event. + pub fn timestamp(&self) -> Option { + match self { + MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0), + _ => None, + } + } } impl SyncOrStrippedState { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index abff96c2655..49a31676791 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -149,6 +149,8 @@ pub mod identity_status_changes; mod member; mod messages; pub mod power_levels; +/// Contains code related to requests to join a room. +pub mod request_to_join; /// A struct containing methods that are common for Joined, Invited and Left /// Rooms diff --git a/crates/matrix-sdk/src/room/request_to_join.rs b/crates/matrix-sdk/src/room/request_to_join.rs new file mode 100644 index 00000000000..ff1b8dfe5af --- /dev/null +++ b/crates/matrix-sdk/src/room/request_to_join.rs @@ -0,0 +1,205 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use js_int::UInt; +use ruma::{EventId, OwnedEventId, OwnedMxcUri, OwnedUserId, RoomId}; + +use crate::{room::RoomMember, Error, Room}; + +/// A request to join a room with `knock` join rule. +#[derive(Debug, Clone)] +pub struct JoinRequest { + room: Room, + /// The event id of the event containing knock membership change. + pub event_id: OwnedEventId, + /// The timestamp when this request was created. + pub timestamp: Option, + /// Some general room member info to display. + pub member_info: RequestToJoinMemberInfo, + /// Whether it's been marked as 'seen' by the client. + pub is_seen: bool, +} + +impl JoinRequest { + pub(crate) fn new( + room: &Room, + event_id: &EventId, + timestamp: Option, + member: RequestToJoinMemberInfo, + is_seen: bool, + ) -> Self { + Self { + room: room.clone(), + event_id: event_id.to_owned(), + timestamp, + member_info: member, + is_seen, + } + } + + /// The room id for the `Room` form whose access is requested. + pub fn room_id(&self) -> &RoomId { + self.room.room_id() + } + + /// Marks the request to join as 'seen' so the client can ignore it in the + /// future. + pub async fn mark_as_seen(&self) -> Result<(), Error> { + self.room.mark_join_requests_as_seen(&[self.event_id.to_owned()]).await?; + Ok(()) + } + + /// Accepts the request to join by inviting the user to the room. + pub async fn accept(&self) -> Result<(), Error> { + self.room.invite_user_by_id(&self.member_info.user_id).await + } + + /// Declines the request to join by kicking the user from the room, with an + /// optional reason. + pub async fn decline(&self, reason: Option<&str>) -> Result<(), Error> { + self.room.kick_user(&self.member_info.user_id, reason).await + } + + /// Declines the request to join by banning the user from the room, with an + /// optional reason. + pub async fn decline_and_ban(&self, reason: Option<&str>) -> Result<(), Error> { + self.room.ban_user(&self.member_info.user_id, reason).await + } +} + +/// General room member info to display along with the join request. +#[derive(Debug, Clone)] +pub struct RequestToJoinMemberInfo { + /// The user id for the room member requesting access. + pub user_id: OwnedUserId, + /// The optional display name of the room member requesting access. + pub display_name: Option, + /// The optional avatar url of the room member requesting access. + pub avatar_url: Option, + /// An optional reason why the user wants access to the room. + pub reason: Option, +} + +impl From for RequestToJoinMemberInfo { + fn from(member: RoomMember) -> Self { + Self { + user_id: member.user_id().to_owned(), + display_name: member.display_name().map(ToOwned::to_owned), + avatar_url: member.avatar_url().map(ToOwned::to_owned), + reason: member.event().reason().map(ToOwned::to_owned), + } + } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use matrix_sdk_test::async_test; + use ruma::{event_id, owned_user_id, room_id, EventId}; + + use crate::{ + room::request_to_join::{JoinRequest, RequestToJoinMemberInfo}, + test_utils::mocks::MatrixMockServer, + Room, + }; + + #[async_test] + async fn test_mark_as_seen() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + let event_id = event_id!("$a:b.c"); + + let room = server.sync_joined_room(&client, room_id).await; + + let join_request = mock_join_request(&room, Some(event_id)); + + // When we mark the join request as seen + join_request.mark_as_seen().await.expect("Failed to mark as seen"); + + // Then we can check it was successfully marked as seen from the room + let seen_ids = + room.get_seen_join_request_ids().await.expect("Failed to get seen join request ids"); + assert_eq!(seen_ids.len(), 1); + assert_eq!(seen_ids.into_iter().next().expect("Couldn't load next item"), event_id); + } + + #[async_test] + async fn test_accept() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + + let room = server.sync_joined_room(&client, room_id).await; + + let join_request = mock_join_request(&room, None); + + // The /invite endpoint must be called once + server.mock_invite_user_by_id().ok().mock_once().mount().await; + + // When we accept the join request + join_request.accept().await.expect("Failed to accept the request"); + } + + #[async_test] + async fn test_decline() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + + let room = server.sync_joined_room(&client, room_id).await; + + let join_request = mock_join_request(&room, None); + + // The /kick endpoint must be called once + server.mock_kick_user().ok().mock_once().mount().await; + + // When we decline the join request + join_request.decline(None).await.expect("Failed to decline the request"); + } + + #[async_test] + async fn test_decline_and_ban() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + + let room = server.sync_joined_room(&client, room_id).await; + + let join_request = mock_join_request(&room, None); + + // The /ban endpoint must be called once + server.mock_ban_user().ok().mock_once().mount().await; + + // When we decline the join request and ban the user from the room + join_request + .decline_and_ban(None) + .await + .expect("Failed to decline the request and ban the user"); + } + + fn mock_join_request(room: &Room, event_id: Option<&EventId>) -> JoinRequest { + JoinRequest::new( + room, + event_id.unwrap_or(event_id!("$a:b.c")), + None, + RequestToJoinMemberInfo { + user_id: owned_user_id!("@alice:b.c"), + display_name: None, + avatar_url: None, + reason: None, + }, + false, + ) + } +} diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 74d25a5ac01..28fbf9b91e7 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -607,6 +607,95 @@ impl MatrixMockServer { .and(header("authorization", "Bearer 1234")); MockEndpoint { mock, server: &self.server, endpoint: DeleteRoomKeysVersionEndpoint } } + + /// Creates a prebuilt mock for inviting a user to a room by its id. + /// + /// # Examples + /// + /// ``` + /// # use ruma::user_id; + /// tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::room_id, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_invite_user_by_id().ok().mock_once().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// room.invite_user_by_id(user_id!("@alice:localhost")).await.unwrap(); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_invite_user_by_id(&self) -> MockEndpoint<'_, InviteUserByIdEndpoint> { + let mock = + Mock::given(method("POST")).and(path_regex(r"^/_matrix/client/v3/rooms/.*/invite$")); + MockEndpoint { mock, server: &self.server, endpoint: InviteUserByIdEndpoint } + } + + /// Creates a prebuilt mock for kicking a user from a room. + /// + /// # Examples + /// + /// ``` + /// # use ruma::user_id; + /// tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::room_id, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_kick_user().ok().mock_once().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// room.kick_user(user_id!("@alice:localhost"), None).await.unwrap(); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_kick_user(&self) -> MockEndpoint<'_, KickUserEndpoint> { + let mock = + Mock::given(method("POST")).and(path_regex(r"^/_matrix/client/v3/rooms/.*/kick")); + MockEndpoint { mock, server: &self.server, endpoint: KickUserEndpoint } + } + + /// Creates a prebuilt mock for banning a user from a room. + /// + /// # Examples + /// + /// ``` + /// # use ruma::user_id; + /// tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::room_id, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_ban_user().ok().mock_once().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// room.ban_user(user_id!("@alice:localhost"), None).await.unwrap(); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_ban_user(&self) -> MockEndpoint<'_, BanUserEndpoint> { + let mock = Mock::given(method("POST")).and(path_regex(r"^/_matrix/client/v3/rooms/.*/ban")); + MockEndpoint { mock, server: &self.server, endpoint: BanUserEndpoint } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -1761,3 +1850,37 @@ impl<'a> MockEndpoint<'a, DeleteRoomKeysVersionEndpoint> { MatrixMock { server: self.server, mock } } } + + +/// A prebuilt mock for `POST /invite` request. +pub struct InviteUserByIdEndpoint; + +impl<'a> MockEndpoint<'a, InviteUserByIdEndpoint> { + /// Returns a successful invite user by id request. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for `POST /kick` request. +pub struct KickUserEndpoint; + +impl<'a> MockEndpoint<'a, KickUserEndpoint> { + /// Returns a successful kick user request. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for `POST /ban` request. +pub struct BanUserEndpoint; + +impl<'a> MockEndpoint<'a, BanUserEndpoint> { + /// Returns a successful ban user request. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + MatrixMock { server: self.server, mock } + } +} From 93ebae66019a6e9408598ec1009c761a523e3138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 10 Dec 2024 17:50:14 +0100 Subject: [PATCH 789/979] feat(room): allow subscribing to requests to join a room This subscription will combine 3 streams: one notifying the members in the room have changed, another notifying the seen join requests have changed, and finally a third one notifying when the room members are no longer synced. With this info we can track when we need to generate a new list of join requests to be emitted so the client can always have an up to date list. --- crates/matrix-sdk-base/src/rooms/normal.rs | 5 + crates/matrix-sdk/src/room/mod.rs | 148 +++++++++++++++++- crates/matrix-sdk/src/test_utils/mocks.rs | 62 +++++++- .../tests/integration/room/joined.rs | 116 +++++++++++++- 4 files changed, 326 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index a9c5b90aedf..55d604591ba 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1356,6 +1356,11 @@ impl RoomInfo { self.members_synced = false; } + /// Returns whether the room members are synced. + pub fn are_members_synced(&self) -> bool { + self.members_synced + } + /// Mark this Room as still missing some state information. pub fn mark_state_partially_synced(&mut self) { self.sync_info = SyncInfo::PartiallySynced; diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 49a31676791..48830fd33cb 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -22,6 +22,7 @@ use std::{ time::Duration, }; +use async_stream::stream; #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] use async_trait::async_trait; use eyeball::SharedObservable; @@ -85,6 +86,7 @@ use ruma::{ avatar::{self, RoomAvatarEventContent}, encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility, + member::{MembershipChange, SyncRoomMemberEvent}, message::{ AudioInfo, AudioMessageEventContent, FileInfo, FileMessageEventContent, FormattedBody, ImageMessageEventContent, MessageType, RoomMessageEventContent, @@ -116,6 +118,7 @@ use ruma::{ use serde::de::DeserializeOwned; use thiserror::Error; use tokio::sync::broadcast; +use tokio_stream::StreamExt; use tracing::{debug, info, instrument, warn}; use self::futures::{SendAttachment, SendMessageLikeEvent, SendRawMessageLikeEvent}; @@ -135,7 +138,10 @@ use crate::{ live_location_share::ObservableLiveLocation, media::{MediaFormat, MediaRequestParameters}, notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode}, - room::power_levels::{RoomPowerLevelChanges, RoomPowerLevelsExt}, + room::{ + power_levels::{RoomPowerLevelChanges, RoomPowerLevelsExt}, + request_to_join::JoinRequest, + }, sync::RoomUpdate, utils::{IntoRawMessageLikeEventContent, IntoRawStateEventContent}, BaseRoom, Client, Error, HttpResult, Result, RoomState, TransmissionProgress, @@ -3208,6 +3214,135 @@ impl Room { ObservableLiveLocation::new(&self.client, self.room_id()) } + /// Helper to requests to join this `Room`. + /// + /// The current requests to join the room will be emitted immediately + /// when subscribing. When a new membership event is received, a request is + /// marked as seen or there is a limited sync, a new set of requests + /// will be emitted. + pub async fn subscribe_to_join_requests(&self) -> Result>> { + let this = Arc::new(self.clone()); + + let requests_observable = + this.client.observe_room_events::(this.room_id()); + + let (current_seen_ids, mut seen_request_ids_stream) = + this.subscribe_to_seen_join_request_ids().await?; + + let mut room_info_stream = this.subscribe_info(); + + let combined_stream = stream! { + // Emit current requests to join + match this.clone().get_current_join_requests(¤t_seen_ids).await { + Ok(initial_requests) => yield initial_requests, + Err(e) => warn!("Failed to get initial requests to join: {e:?}") + } + + let mut requests_stream = requests_observable.subscribe(); + + let mut new_event: Option = None; + let mut seen_ids = current_seen_ids.clone(); + let mut prev_seen_ids = current_seen_ids; + let mut prev_missing_room_members: bool = false; + let mut missing_room_members: bool = false; + + loop { + // This is equivalent to a combine stream operation, triggering a new emission + // when any of the branches changes + tokio::select! { + Some((next, _)) = requests_stream.next() => { new_event = Some(next); } + Some(next) = seen_request_ids_stream.next() => { seen_ids = next; } + Some(next) = room_info_stream.next() => { + missing_room_members = !next.are_members_synced() + } + else => break, + } + + // We need to emit new items when we may have missing room members: + // this usually happens after a gappy (limited) sync + let has_missing_room_members = prev_missing_room_members != missing_room_members; + if has_missing_room_members { + prev_missing_room_members = missing_room_members; + } + + // We need to emit new items if the seen join request ids have changed + let has_new_seen_ids = prev_seen_ids != seen_ids; + if has_new_seen_ids { + prev_seen_ids = seen_ids.clone(); + } + + if let Some(SyncStateEvent::Original(event)) = new_event.clone() { + // Reset the new event value so we can check this again in the next loop + new_event = None; + + // If we can calculate the membership change, try to emit only when needed + if event.prev_content().is_some() { + match event.membership_change() { + MembershipChange::Banned | + MembershipChange::Knocked | + MembershipChange::KnockAccepted | + MembershipChange::KnockDenied | + MembershipChange::KnockRetracted => { + match this.clone().get_current_join_requests(&seen_ids).await { + Ok(requests) => yield requests, + Err(e) => { + warn!("Failed to get updated requests to join on membership change: {e:?}") + } + } + } + _ => (), + } + } else { + // If we can't calculate the membership change, assume we need to + // emit updated values + match this.clone().get_current_join_requests(&seen_ids).await { + Ok(requests) => yield requests, + Err(e) => { + warn!("Failed to get updated requests to join on new member event: {e:?}") + } + } + } + } else if has_new_seen_ids || has_missing_room_members { + // If seen requests have changed or we have missing room members, + // we need to recalculate all the requests to join + match this.clone().get_current_join_requests(&seen_ids).await { + Ok(requests) => yield requests, + Err(e) => { + warn!("Failed to get updated requests to join on seen ids changed: {e:?}") + } + } + } + } + }; + + Ok(combined_stream) + } + + async fn get_current_join_requests( + &self, + seen_request_ids: &HashSet, + ) -> Result> { + Ok(self + .members(RoomMemberships::KNOCK) + .await? + .into_iter() + .filter_map(|member| { + if let Some(event_id) = member.event().event_id() { + let event_id = event_id.to_owned(); + Some(JoinRequest::new( + self, + &event_id, + member.event().timestamp(), + member.into(), + seen_request_ids.contains(&event_id), + )) + } else { + None + } + }) + .collect()) + } + /// Mark a list of requests to join the room as seen, given their state /// event ids. pub async fn mark_join_requests_as_seen(&self, event_ids: &[OwnedEventId]) -> Result<()> { @@ -3249,6 +3384,17 @@ impl Room { }; Ok(current_join_request_ids) } + + /// Subscribes to the set of requests to join that have been marked as + /// 'seen'. + pub async fn subscribe_to_seen_join_request_ids( + &self, + ) -> Result<(HashSet, impl Stream>)> { + let current = self.get_seen_join_request_ids().await?; + let subscriber = + self.seen_join_request_ids.subscribe().map(|values| values.unwrap_or_default()); + Ok((current, subscriber)) + } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 28fbf9b91e7..19ffee007ea 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -29,7 +29,10 @@ use matrix_sdk_test::{ }; use ruma::{ directory::PublicRoomsChunk, - events::{AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType}, + events::{ + room::member::RoomMemberEvent, AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, + StateEventType, + }, serde::Raw, time::Duration, MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName, @@ -608,6 +611,49 @@ impl MatrixMockServer { MockEndpoint { mock, server: &self.server, endpoint: DeleteRoomKeysVersionEndpoint } } + /// Create a prebuilt mock for getting the room members in a room. + /// + /// # Examples + /// + /// ``` # + /// tokio_test::block_on(async { + /// use matrix_sdk_base::RoomMemberships; + /// use ruma::events::room::member::MembershipState; + /// use ruma::events::room::member::RoomMemberEventContent; + /// use ruma::user_id; + /// use matrix_sdk_test::event_factory::EventFactory; + /// use matrix_sdk::{ + /// ruma::{event_id, room_id}, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// let event_id = event_id!("$id"); + /// let room_id = room_id!("!room_id:localhost"); + /// + /// let f = EventFactory::new().room(room_id); + /// let alice_user_id = user_id!("@alice:b.c"); + /// let alice_knock_event = f + /// .event(RoomMemberEventContent::new(MembershipState::Knock)) + /// .event_id(event_id) + /// .sender(alice_user_id) + /// .state_key(alice_user_id) + /// .into_raw_timeline() + /// .cast(); + /// + /// mock_server.mock_get_members().ok(vec![alice_knock_event]).mock_once().mount().await; + /// let room = mock_server.sync_joined_room(&client, room_id).await; + /// + /// let members = room.members(RoomMemberships::all()).await.unwrap(); + /// assert_eq!(members.len(), 1); + /// # }); + /// ``` + pub fn mock_get_members(&self) -> MockEndpoint<'_, GetRoomMembersEndpoint> { + let mock = + Mock::given(method("GET")).and(path_regex(r"^/_matrix/client/v3/rooms/.*/members$")); + MockEndpoint { mock, server: &self.server, endpoint: GetRoomMembersEndpoint } + } + /// Creates a prebuilt mock for inviting a user to a room by its id. /// /// # Examples @@ -1112,7 +1158,7 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// /// let response = room.client().send(r, None).await.unwrap(); /// // The delayed `m.room.message` event type should be mocked by the server. - /// assert_eq!("$some_id", response.delay_id); + /// assert_eq!("$some_id", response.delay_id); /// # anyhow::Ok(()) }); /// ``` pub fn with_delay(self, delay: Duration) -> Self { @@ -1851,6 +1897,18 @@ impl<'a> MockEndpoint<'a, DeleteRoomKeysVersionEndpoint> { } } +/// A prebuilt mock for `GET /members` request. +pub struct GetRoomMembersEndpoint; + +impl<'a> MockEndpoint<'a, GetRoomMembersEndpoint> { + /// Returns a successful get members request with a list of members. + pub fn ok(self, members: Vec>) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "chunk": members, + }))); + MatrixMock { server: self.server, mock } + } +} /// A prebuilt mock for `POST /invite` request. pub struct InviteUserByIdEndpoint; diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index fa0b3f66af2..a6b5f38949e 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -3,8 +3,9 @@ use std::{ time::Duration, }; -use futures_util::future::join_all; +use futures_util::{future::join_all, pin_mut}; use matrix_sdk::{ + assert_next_with_timeout, config::SyncSettings, room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, test_utils::mocks::MatrixMockServer, @@ -24,7 +25,10 @@ use ruma::{ events::{ direct::DirectUserIdentifier, receipt::ReceiptThread, - room::message::{RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, + room::{ + member::{MembershipState, RoomMemberEventContent}, + message::{RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, + }, TimelineEventType, }, int, mxc_uri, owned_event_id, room_id, thirdparty, user_id, OwnedUserId, TransactionId, @@ -833,3 +837,111 @@ async fn test_enable_encryption_doesnt_stay_unencrypted() { assert!(room.is_encrypted().await.unwrap()); } + +#[async_test] +async fn test_subscribe_to_requests_to_join() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a:b.c"); + let f = EventFactory::new().room(room_id); + + let alice_user_id = user_id!("@alice:b.c"); + let alice_knock_event_id = event_id!("$alice-knock:b.c"); + let alice_knock_event = f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(alice_knock_event_id) + .sender(alice_user_id) + .state_key(alice_user_id) + .into_raw_timeline() + .cast(); + + server.mock_get_members().ok(vec![alice_knock_event]).mock_once().mount().await; + + let room = server.sync_joined_room(&client, room_id).await; + let stream = room.subscribe_to_join_requests().await.unwrap(); + + pin_mut!(stream); + + // We receive an initial request to join from Alice + let initial = assert_next_with_timeout!(stream, 100); + assert!(!initial.is_empty()); + + let alices_request_to_join = &initial[0]; + assert_eq!(alices_request_to_join.event_id, alice_knock_event_id); + assert!(!alices_request_to_join.is_seen); + + // We then mark the request to join as seen + room.mark_join_requests_as_seen(&[alice_knock_event_id.to_owned()]).await.unwrap(); + + // Now it's received again as seen + let seen = assert_next_with_timeout!(stream, 100); + assert!(!seen.is_empty()); + let alices_seen_request_to_join = &seen[0]; + assert_eq!(alices_seen_request_to_join.event_id, alice_knock_event_id); + assert!(alices_seen_request_to_join.is_seen); + + // If we then receive a new member event for Alice that's not 'knock' + let alice_join_event_id = event_id!("$alice-join:b.c"); + let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_bulk(vec![f + .event(RoomMemberEventContent::new(MembershipState::Invite)) + .event_id(alice_join_event_id) + .sender(alice_user_id) + .state_key(alice_user_id) + .into_raw_timeline() + .cast()]); + server.sync_room(&client, joined_room_builder).await; + + // The requests to join are now empty + let updated_requests = assert_next_with_timeout!(stream, 100); + assert!(updated_requests.is_empty()); +} + +#[async_test] +async fn test_subscribe_to_requests_to_join_reloads_members_on_limited_sync() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a:b.c"); + let f = EventFactory::new().room(room_id); + + let alice_user_id = user_id!("@alice:b.c"); + let alice_knock_event_id = event_id!("$alice-knock:b.c"); + let alice_knock_event = f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(alice_knock_event_id) + .sender(alice_user_id) + .state_key(alice_user_id) + .into_raw_timeline() + .cast(); + + server + .mock_get_members() + .ok(vec![alice_knock_event]) + // The endpoint will be called twice: + // 1. For the initial loading of room members. + // 2. When a gappy (limited) sync is received. + .expect(2) + .mount() + .await; + + let room = server.sync_joined_room(&client, room_id).await; + let stream = room.subscribe_to_join_requests().await.unwrap(); + + pin_mut!(stream); + + // We receive an initial request to join from Alice + let initial = assert_next_with_timeout!(stream, 500); + assert!(!initial.is_empty()); + + // This limited sync should trigger a new emission of join requests, with a + // reloading of the room members + server.sync_room(&client, JoinedRoomBuilder::new(room_id).set_timeline_limited()).await; + + // We should receive a new list of join requests + assert_next_with_timeout!(stream, 500); +} From 338769508e02a46ca55b7a3a26657b45e7d9cbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 10 Dec 2024 17:50:48 +0100 Subject: [PATCH 790/979] feat(ffi): add bindings for subscribing to the join requests --- bindings/matrix-sdk-ffi/src/room.rs | 104 +++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index fd0f6aef3fa..6cf0a53e9e3 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, pin::pin, sync::Arc}; use anyhow::{Context, Result}; -use futures_util::StreamExt; +use futures_util::{pin_mut, StreamExt}; use matrix_sdk::{ crypto::LocalTrust, event_cache::paginator::PaginatorError, @@ -911,6 +911,108 @@ impl Room { room_event_cache.clear().await?; Ok(()) } + + /// Subscribes to requests to join this room, using a `listener` to be + /// notified of the changes. + /// + /// The current requests to join the room will be emitted immediately + /// when subscribing, along with a [`TaskHandle`] to cancel the + /// subscription. + pub async fn subscribe_to_join_requests( + self: Arc, + listener: Box, + ) -> Result, ClientError> { + let stream = self.inner.subscribe_to_join_requests().await?; + + let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move { + pin_mut!(stream); + while let Some(requests) = stream.next().await { + listener.call(requests.into_iter().map(Into::into).collect()); + } + }))); + + Ok(handle) + } +} + +impl From for JoinRequest { + fn from(request: matrix_sdk::room::request_to_join::JoinRequest) -> Self { + Self { + event_id: request.event_id.to_string(), + user_id: request.member_info.user_id.to_string(), + room_id: request.room_id().to_string(), + display_name: request.member_info.display_name.clone(), + avatar_url: request.member_info.avatar_url.as_ref().map(|url| url.to_string()), + reason: request.member_info.reason.clone(), + timestamp: request.timestamp.map(|ts| ts.into()), + is_seen: request.is_seen, + actions: Arc::new(JoinRequestActions { inner: request }), + } + } +} + +/// A listener for receiving new requests to a join a room. +#[matrix_sdk_ffi_macros::export(callback_interface)] +pub trait JoinRequestsListener: Send + Sync { + fn call(&self, join_requests: Vec); +} + +/// An FFI representation of a request to join a room. +#[derive(Debug, Clone, uniffi::Record)] +pub struct JoinRequest { + /// The event id of the event that contains the `knock` membership change. + pub event_id: String, + /// The user id of the user who's requesting to join the room. + pub user_id: String, + /// The room id of the room whose access was requested. + pub room_id: String, + /// The optional display name of the user who's requesting to join the room. + pub display_name: Option, + /// The optional avatar url of the user who's requesting to join the room. + pub avatar_url: Option, + /// An optional reason why the user wants join the room. + pub reason: Option, + /// The timestamp when this request was created. + pub timestamp: Option, + /// Whether the request to join has been marked as `seen` so it can be + /// filtered by the client. + pub is_seen: bool, + /// A set of actions to perform for this request to join. + pub actions: Arc, +} + +/// A set of actions to perform for a request to join. +#[derive(Debug, Clone, uniffi::Object)] +pub struct JoinRequestActions { + inner: matrix_sdk::room::request_to_join::JoinRequest, +} + +#[matrix_sdk_ffi_macros::export] +impl JoinRequestActions { + /// Accepts the request to join by inviting the user to the room. + pub async fn accept(&self) -> Result<(), ClientError> { + self.inner.accept().await.map_err(Into::into) + } + + /// Declines the request to join by kicking the user from the room with an + /// optional reason. + pub async fn decline(&self, reason: Option) -> Result<(), ClientError> { + self.inner.decline(reason.as_deref()).await.map_err(Into::into) + } + + /// Declines the request to join by banning the user from the room with an + /// optional reason. + pub async fn decline_and_ban(&self, reason: Option) -> Result<(), ClientError> { + self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into) + } + + /// Marks the request as 'seen'. + /// + /// **IMPORTANT**: this won't update the current reference to this request, + /// a new one with the updated value should be emitted instead. + pub async fn mark_as_seen(&self) -> Result<(), ClientError> { + self.inner.clone().mark_as_seen().await.map_err(Into::into) + } } /// Generates a `matrix.to` permalink to the given room alias. From 05d46e6027074a501b1810a5cab06131d2921769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 13 Dec 2024 12:22:02 +0100 Subject: [PATCH 791/979] Rename `JoinRequest` in the SDK crates to `KnockRequest`, make `Room::mark_knock_requests_as_seen` thread safe and pass `user_ids` instead of `event_ids`: the user ids will be used to get the related member state events and they'll only be marked as seen if they're in a knock state. Also, add extra checks to the integration tests. --- bindings/matrix-sdk-ffi/src/room.rs | 46 ++-- crates/matrix-sdk-base/src/rooms/normal.rs | 82 ++++++ .../matrix-sdk-base/src/store/memory_store.rs | 2 +- crates/matrix-sdk-base/src/store/traits.rs | 4 +- .../src/state_store/mod.rs | 4 +- crates/matrix-sdk-sqlite/src/state_store.rs | 4 +- .../{request_to_join.rs => knock_requests.rs} | 85 +++--- crates/matrix-sdk/src/room/mod.rs | 256 ++++++++---------- .../tests/integration/room/joined.rs | 73 ++--- 9 files changed, 309 insertions(+), 247 deletions(-) rename crates/matrix-sdk/src/room/{request_to_join.rs => knock_requests.rs} (65%) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 6cf0a53e9e3..c1233b51151 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -912,17 +912,17 @@ impl Room { Ok(()) } - /// Subscribes to requests to join this room, using a `listener` to be - /// notified of the changes. + /// Subscribes to requests to join this room (knock member events), using a + /// `listener` to be notified of the changes. /// /// The current requests to join the room will be emitted immediately /// when subscribing, along with a [`TaskHandle`] to cancel the /// subscription. - pub async fn subscribe_to_join_requests( + pub async fn subscribe_to_knock_requests( self: Arc, - listener: Box, + listener: Box, ) -> Result, ClientError> { - let stream = self.inner.subscribe_to_join_requests().await?; + let stream = self.inner.subscribe_to_knock_requests().await?; let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move { pin_mut!(stream); @@ -935,8 +935,8 @@ impl Room { } } -impl From for JoinRequest { - fn from(request: matrix_sdk::room::request_to_join::JoinRequest) -> Self { +impl From for KnockRequest { + fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self { Self { event_id: request.event_id.to_string(), user_id: request.member_info.user_id.to_string(), @@ -946,20 +946,20 @@ impl From for JoinRequest { reason: request.member_info.reason.clone(), timestamp: request.timestamp.map(|ts| ts.into()), is_seen: request.is_seen, - actions: Arc::new(JoinRequestActions { inner: request }), + actions: Arc::new(KnockRequestActions { inner: request }), } } } /// A listener for receiving new requests to a join a room. #[matrix_sdk_ffi_macros::export(callback_interface)] -pub trait JoinRequestsListener: Send + Sync { - fn call(&self, join_requests: Vec); +pub trait KnockRequestsListener: Send + Sync { + fn call(&self, join_requests: Vec); } /// An FFI representation of a request to join a room. #[derive(Debug, Clone, uniffi::Record)] -pub struct JoinRequest { +pub struct KnockRequest { /// The event id of the event that contains the `knock` membership change. pub event_id: String, /// The user id of the user who's requesting to join the room. @@ -974,44 +974,44 @@ pub struct JoinRequest { pub reason: Option, /// The timestamp when this request was created. pub timestamp: Option, - /// Whether the request to join has been marked as `seen` so it can be + /// Whether the knock request has been marked as `seen` so it can be /// filtered by the client. pub is_seen: bool, - /// A set of actions to perform for this request to join. - pub actions: Arc, + /// A set of actions to perform for this knock request. + pub actions: Arc, } -/// A set of actions to perform for a request to join. +/// A set of actions to perform for a knock request. #[derive(Debug, Clone, uniffi::Object)] -pub struct JoinRequestActions { - inner: matrix_sdk::room::request_to_join::JoinRequest, +pub struct KnockRequestActions { + inner: matrix_sdk::room::knock_requests::KnockRequest, } #[matrix_sdk_ffi_macros::export] -impl JoinRequestActions { - /// Accepts the request to join by inviting the user to the room. +impl KnockRequestActions { + /// Accepts the knock request by inviting the user to the room. pub async fn accept(&self) -> Result<(), ClientError> { self.inner.accept().await.map_err(Into::into) } - /// Declines the request to join by kicking the user from the room with an + /// Declines the knock request by kicking the user from the room with an /// optional reason. pub async fn decline(&self, reason: Option) -> Result<(), ClientError> { self.inner.decline(reason.as_deref()).await.map_err(Into::into) } - /// Declines the request to join by banning the user from the room with an + /// Declines the knock request by banning the user from the room with an /// optional reason. pub async fn decline_and_ban(&self, reason: Option) -> Result<(), ClientError> { self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into) } - /// Marks the request as 'seen'. + /// Marks the knock request as 'seen'. /// /// **IMPORTANT**: this won't update the current reference to this request, /// a new one with the updated value should be emitted instead. pub async fn mark_as_seen(&self) -> Result<(), ClientError> { - self.inner.clone().mark_as_seen().await.map_err(Into::into) + self.inner.mark_as_seen().await.map_err(Into::into) } } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 55d604591ba..a7a5d4ea983 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1177,6 +1177,88 @@ impl Room { pub fn pinned_event_ids(&self) -> Option> { self.inner.read().pinned_event_ids() } + + /// Mark a list of requests to join the room as seen, given their state + /// event ids. + pub async fn mark_knock_requests_as_seen(&self, user_ids: &[OwnedUserId]) -> StoreResult<()> { + let raw_user_ids: Vec<&str> = user_ids.iter().map(|id| id.as_str()).collect(); + let member_raw_events = self + .store + .get_state_events_for_keys(self.room_id(), StateEventType::RoomMember, &raw_user_ids) + .await?; + let mut event_to_user_ids = Vec::with_capacity(member_raw_events.len()); + + // Map the list of events ids to their user ids, if they are event ids for knock + // membership events. Log an error and continue otherwise. + for raw_event in member_raw_events { + let event = raw_event.cast::().deserialize()?; + match event { + SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => { + if event.content.membership == MembershipState::Knock { + event_to_user_ids.push((event.event_id, event.state_key)) + } else { + warn!("Could not mark knock event as seen: event {} for user {} is not in Knock membership state.", event.event_id, event.state_key); + } + } + _ => warn!( + "Could not mark knock event as seen: event for user {} is not valid.", + event.state_key() + ), + } + } + + let mut current_seen_events_guard = self.seen_knock_request_ids_map.write().await; + // We're not calling `get_seen_join_request_ids` here because we need to keep + // the Mutex's guard until we've updated the data + let mut current_seen_events = if current_seen_events_guard.is_none() { + self.load_cached_knock_request_ids().await? + } else { + current_seen_events_guard.clone().unwrap() + }; + + current_seen_events.extend(event_to_user_ids); + + ObservableWriteGuard::set( + &mut current_seen_events_guard, + Some(current_seen_events.clone()), + ); + + self.store + .set_kv_data( + StateStoreDataKey::SeenKnockRequests(self.room_id()), + StateStoreDataValue::SeenKnockRequests(current_seen_events), + ) + .await?; + + Ok(()) + } + + /// Get the list of seen knock request event ids in this room. + pub async fn get_seen_knock_request_ids( + &self, + ) -> Result, StoreError> { + let mut guard = self.seen_knock_request_ids_map.write().await; + if guard.is_none() { + ObservableWriteGuard::set( + &mut guard, + Some(self.load_cached_knock_request_ids().await?), + ); + } + Ok(guard.clone().unwrap_or_default()) + } + + /// This loads the current list of seen knock request ids from the state + /// store. + async fn load_cached_knock_request_ids( + &self, + ) -> StoreResult> { + Ok(self + .store + .get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id())) + .await? + .and_then(|v| v.into_seen_knock_requests()) + .unwrap_or_default()) + } } // See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 4f98c42d4e8..9148c9b34da 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -232,7 +232,7 @@ impl StateStore for MemoryStore { inner.seen_knock_requests.insert( room_id.to_owned(), value - .into_seen_join_requests() + .into_seen_knock_requests() .expect("Session data is not a set of seen join request ids"), ); } diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 8e2447d4866..5f651483f5b 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -1093,7 +1093,7 @@ impl StateStoreDataValue { } /// Get this value if it is the data for the ignored join requests. - pub fn into_seen_join_requests(self) -> Option> { + pub fn into_seen_knock_requests(self) -> Option> { as_variant!(self, Self::SeenKnockRequests) } } @@ -1126,7 +1126,7 @@ pub enum StateStoreDataKey<'a> { /// [`ComposerDraft`]: Self::ComposerDraft ComposerDraft(&'a RoomId), - /// A list of requests to join in a room marked as seen. + /// A list of knock request ids marked as seen in a room. SeenKnockRequests(&'a RoomId), } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 8de8d22efd0..01d386f354f 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -583,8 +583,8 @@ impl_state_store!({ ), StateStoreDataKey::SeenKnockRequests(_) => self.serialize_value( &value - .into_seen_join_requests() - .expect("Session data is not a set of seen join request ids"), + .into_seen_knock_requests() + .expect("Session data is not a set of seen knock request ids"), ), }; diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index a8a2e792961..adfd9d5b5a3 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -1037,8 +1037,8 @@ impl StateStore for SqliteStateStore { )?, StateStoreDataKey::SeenKnockRequests(_) => self.serialize_value( &value - .into_seen_join_requests() - .expect("Session data is not a set of seen join request ids"), + .into_seen_knock_requests() + .expect("Session data is not a set of seen knock request ids"), )?, }; diff --git a/crates/matrix-sdk/src/room/request_to_join.rs b/crates/matrix-sdk/src/room/knock_requests.rs similarity index 65% rename from crates/matrix-sdk/src/room/request_to_join.rs rename to crates/matrix-sdk/src/room/knock_requests.rs index ff1b8dfe5af..de1409f5086 100644 --- a/crates/matrix-sdk/src/room/request_to_join.rs +++ b/crates/matrix-sdk/src/room/knock_requests.rs @@ -19,24 +19,24 @@ use crate::{room::RoomMember, Error, Room}; /// A request to join a room with `knock` join rule. #[derive(Debug, Clone)] -pub struct JoinRequest { +pub struct KnockRequest { room: Room, /// The event id of the event containing knock membership change. pub event_id: OwnedEventId, /// The timestamp when this request was created. pub timestamp: Option, /// Some general room member info to display. - pub member_info: RequestToJoinMemberInfo, + pub member_info: KnockRequestMemberInfo, /// Whether it's been marked as 'seen' by the client. pub is_seen: bool, } -impl JoinRequest { +impl KnockRequest { pub(crate) fn new( room: &Room, event_id: &EventId, timestamp: Option, - member: RequestToJoinMemberInfo, + member: KnockRequestMemberInfo, is_seen: bool, ) -> Self { Self { @@ -48,30 +48,30 @@ impl JoinRequest { } } - /// The room id for the `Room` form whose access is requested. + /// The room id for the `Room` from whose access is requested. pub fn room_id(&self) -> &RoomId { self.room.room_id() } - /// Marks the request to join as 'seen' so the client can ignore it in the + /// Marks the knock request as 'seen' so the client can ignore it in the /// future. pub async fn mark_as_seen(&self) -> Result<(), Error> { - self.room.mark_join_requests_as_seen(&[self.event_id.to_owned()]).await?; + self.room.mark_knock_requests_as_seen(&[self.member_info.user_id.to_owned()]).await?; Ok(()) } - /// Accepts the request to join by inviting the user to the room. + /// Accepts the knock request by inviting the user to the room. pub async fn accept(&self) -> Result<(), Error> { self.room.invite_user_by_id(&self.member_info.user_id).await } - /// Declines the request to join by kicking the user from the room, with an + /// Declines the knock request by kicking the user from the room, with an /// optional reason. pub async fn decline(&self, reason: Option<&str>) -> Result<(), Error> { self.room.kick_user(&self.member_info.user_id, reason).await } - /// Declines the request to join by banning the user from the room, with an + /// Declines the knock request by banning the user from the room, with an /// optional reason. pub async fn decline_and_ban(&self, reason: Option<&str>) -> Result<(), Error> { self.room.ban_user(&self.member_info.user_id, reason).await @@ -80,7 +80,7 @@ impl JoinRequest { /// General room member info to display along with the join request. #[derive(Debug, Clone)] -pub struct RequestToJoinMemberInfo { +pub struct KnockRequestMemberInfo { /// The user id for the room member requesting access. pub user_id: OwnedUserId, /// The optional display name of the room member requesting access. @@ -91,8 +91,8 @@ pub struct RequestToJoinMemberInfo { pub reason: Option, } -impl From for RequestToJoinMemberInfo { - fn from(member: RoomMember) -> Self { +impl KnockRequestMemberInfo { + pub(crate) fn from_member(member: &RoomMember) -> Self { Self { user_id: member.user_id().to_owned(), display_name: member.display_name().map(ToOwned::to_owned), @@ -102,13 +102,18 @@ impl From for RequestToJoinMemberInfo { } } +// The http mocking library is not supported for wasm32 #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use matrix_sdk_test::async_test; - use ruma::{event_id, owned_user_id, room_id, EventId}; + use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder}; + use ruma::{ + event_id, + events::room::member::{MembershipState, RoomMemberEventContent}, + owned_user_id, room_id, user_id, EventId, + }; use crate::{ - room::request_to_join::{JoinRequest, RequestToJoinMemberInfo}, + room::knock_requests::{KnockRequest, KnockRequestMemberInfo}, test_utils::mocks::MatrixMockServer, Room, }; @@ -119,19 +124,31 @@ mod tests { let client = server.client_builder().build().await; let room_id = room_id!("!a:b.c"); let event_id = event_id!("$a:b.c"); + let user_id = user_id!("@alice:b.c"); - let room = server.sync_joined_room(&client, room_id).await; + let f = EventFactory::new().room(room_id); + let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_bulk(vec![f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(event_id) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast()]); + let room = server.sync_room(&client, joined_room_builder).await; - let join_request = mock_join_request(&room, Some(event_id)); + let knock_request = make_knock_request(&room, Some(event_id)); - // When we mark the join request as seen - join_request.mark_as_seen().await.expect("Failed to mark as seen"); + // When we mark the knock request as seen + knock_request.mark_as_seen().await.expect("Failed to mark as seen"); // Then we can check it was successfully marked as seen from the room let seen_ids = - room.get_seen_join_request_ids().await.expect("Failed to get seen join request ids"); + room.get_seen_knock_request_ids().await.expect("Failed to get seen join request ids"); assert_eq!(seen_ids.len(), 1); - assert_eq!(seen_ids.into_iter().next().expect("Couldn't load next item"), event_id); + assert_eq!( + seen_ids.into_iter().next().expect("Couldn't load next item"), + (event_id.to_owned(), user_id.to_owned()) + ); } #[async_test] @@ -142,13 +159,13 @@ mod tests { let room = server.sync_joined_room(&client, room_id).await; - let join_request = mock_join_request(&room, None); + let knock_request = make_knock_request(&room, None); // The /invite endpoint must be called once server.mock_invite_user_by_id().ok().mock_once().mount().await; - // When we accept the join request - join_request.accept().await.expect("Failed to accept the request"); + // When we accept the knock request + knock_request.accept().await.expect("Failed to accept the request"); } #[async_test] @@ -159,13 +176,13 @@ mod tests { let room = server.sync_joined_room(&client, room_id).await; - let join_request = mock_join_request(&room, None); + let knock_request = make_knock_request(&room, None); // The /kick endpoint must be called once server.mock_kick_user().ok().mock_once().mount().await; - // When we decline the join request - join_request.decline(None).await.expect("Failed to decline the request"); + // When we decline the knock request + knock_request.decline(None).await.expect("Failed to decline the request"); } #[async_test] @@ -176,24 +193,24 @@ mod tests { let room = server.sync_joined_room(&client, room_id).await; - let join_request = mock_join_request(&room, None); + let knock_request = make_knock_request(&room, None); // The /ban endpoint must be called once server.mock_ban_user().ok().mock_once().mount().await; - // When we decline the join request and ban the user from the room - join_request + // When we decline the knock request and ban the user from the room + knock_request .decline_and_ban(None) .await .expect("Failed to decline the request and ban the user"); } - fn mock_join_request(room: &Room, event_id: Option<&EventId>) -> JoinRequest { - JoinRequest::new( + fn make_knock_request(room: &Room, event_id: Option<&EventId>) -> KnockRequest { + KnockRequest::new( room, event_id.unwrap_or(event_id!("$a:b.c")), None, - RequestToJoinMemberInfo { + KnockRequestMemberInfo { user_id: owned_user_id!("@alice:b.c"), display_name: None, avatar_url: None, diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 48830fd33cb..30a3eed4de5 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -16,7 +16,7 @@ use std::{ borrow::Borrow, - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashMap}, ops::Deref, sync::Arc, time::Duration, @@ -139,8 +139,8 @@ use crate::{ media::{MediaFormat, MediaRequestParameters}, notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode}, room::{ + knock_requests::{KnockRequest, KnockRequestMemberInfo}, power_levels::{RoomPowerLevelChanges, RoomPowerLevelsExt}, - request_to_join::JoinRequest, }, sync::RoomUpdate, utils::{IntoRawMessageLikeEventContent, IntoRawStateEventContent}, @@ -152,11 +152,11 @@ use crate::{crypto::types::events::CryptoContextInfo, encryption::backups::Backu pub mod edit; pub mod futures; pub mod identity_status_changes; +/// Contains code related to requests to join a room. +pub mod knock_requests; mod member; mod messages; pub mod power_levels; -/// Contains code related to requests to join a room. -pub mod request_to_join; /// A struct containing methods that are common for Joined, Invited and Left /// Rooms @@ -3214,103 +3214,103 @@ impl Room { ObservableLiveLocation::new(&self.client, self.room_id()) } - /// Helper to requests to join this `Room`. + /// Subscribe to knock requests in this `Room`. /// /// The current requests to join the room will be emitted immediately - /// when subscribing. When a new membership event is received, a request is - /// marked as seen or there is a limited sync, a new set of requests - /// will be emitted. - pub async fn subscribe_to_join_requests(&self) -> Result>> { + /// when subscribing. + /// + /// A new set of knock requests will be emitted whenever: + /// - A new member event is received. + /// - A knock request is marked as seen. + /// - A sync is gappy (limited), so room membership information may be + /// outdated. + pub async fn subscribe_to_knock_requests( + &self, + ) -> Result>> { let this = Arc::new(self.clone()); - let requests_observable = - this.client.observe_room_events::(this.room_id()); + let room_member_events_observer = + self.client.observe_room_events::(this.room_id()); - let (current_seen_ids, mut seen_request_ids_stream) = - this.subscribe_to_seen_join_request_ids().await?; + let current_seen_ids = self.get_seen_knock_request_ids().await?; + let mut seen_request_ids_stream = self + .seen_knock_request_ids_map + .subscribe() + .await + .map(|values| values.unwrap_or_default()); - let mut room_info_stream = this.subscribe_info(); + let mut room_info_stream = self.subscribe_info(); let combined_stream = stream! { // Emit current requests to join - match this.clone().get_current_join_requests(¤t_seen_ids).await { + match this.get_current_join_requests(¤t_seen_ids).await { Ok(initial_requests) => yield initial_requests, - Err(e) => warn!("Failed to get initial requests to join: {e:?}") + Err(err) => warn!("Failed to get initial requests to join: {err}") } - let mut requests_stream = requests_observable.subscribe(); - - let mut new_event: Option = None; + let mut requests_stream = room_member_events_observer.subscribe(); let mut seen_ids = current_seen_ids.clone(); - let mut prev_seen_ids = current_seen_ids; - let mut prev_missing_room_members: bool = false; - let mut missing_room_members: bool = false; loop { // This is equivalent to a combine stream operation, triggering a new emission // when any of the branches changes tokio::select! { - Some((next, _)) = requests_stream.next() => { new_event = Some(next); } - Some(next) = seen_request_ids_stream.next() => { seen_ids = next; } - Some(next) = room_info_stream.next() => { - missing_room_members = !next.are_members_synced() - } - else => break, - } - - // We need to emit new items when we may have missing room members: - // this usually happens after a gappy (limited) sync - let has_missing_room_members = prev_missing_room_members != missing_room_members; - if has_missing_room_members { - prev_missing_room_members = missing_room_members; - } - - // We need to emit new items if the seen join request ids have changed - let has_new_seen_ids = prev_seen_ids != seen_ids; - if has_new_seen_ids { - prev_seen_ids = seen_ids.clone(); - } - - if let Some(SyncStateEvent::Original(event)) = new_event.clone() { - // Reset the new event value so we can check this again in the next loop - new_event = None; - - // If we can calculate the membership change, try to emit only when needed - if event.prev_content().is_some() { - match event.membership_change() { - MembershipChange::Banned | - MembershipChange::Knocked | - MembershipChange::KnockAccepted | - MembershipChange::KnockDenied | - MembershipChange::KnockRetracted => { - match this.clone().get_current_join_requests(&seen_ids).await { + Some((event, _)) = requests_stream.next() => { + if let Some(event) = event.as_original() { + // If we can calculate the membership change, try to emit only when needed + let emit = if event.prev_content().is_some() { + matches!(event.membership_change(), + MembershipChange::Banned | + MembershipChange::Knocked | + MembershipChange::KnockAccepted | + MembershipChange::KnockDenied | + MembershipChange::KnockRetracted + ) + } else { + // If we can't calculate the membership change, assume we need to + // emit updated values + true + }; + + if emit { + match this.get_current_join_requests(&seen_ids).await { Ok(requests) => yield requests, - Err(e) => { - warn!("Failed to get updated requests to join on membership change: {e:?}") + Err(err) => { + warn!("Failed to get updated knock requests on new member event: {err}") } } } - _ => (), } - } else { - // If we can't calculate the membership change, assume we need to - // emit updated values - match this.clone().get_current_join_requests(&seen_ids).await { + } + + Some(new_seen_ids) = seen_request_ids_stream.next() => { + // Update the current seen ids + seen_ids = new_seen_ids; + + // If seen requests have changed we need to recalculate + // all the knock requests + match this.get_current_join_requests(&seen_ids).await { Ok(requests) => yield requests, - Err(e) => { - warn!("Failed to get updated requests to join on new member event: {e:?}") + Err(err) => { + warn!("Failed to get updated knock requests on seen ids changed: {err}") } } } - } else if has_new_seen_ids || has_missing_room_members { - // If seen requests have changed or we have missing room members, - // we need to recalculate all the requests to join - match this.clone().get_current_join_requests(&seen_ids).await { - Ok(requests) => yield requests, - Err(e) => { - warn!("Failed to get updated requests to join on seen ids changed: {e:?}") + + Some(room_info) = room_info_stream.next() => { + // We need to emit new items when we may have missing room members: + // this usually happens after a gappy (limited) sync + if !room_info.are_members_synced() { + match this.get_current_join_requests(&seen_ids).await { + Ok(requests) => yield requests, + Err(err) => { + warn!("Failed to get updated knock requests on gappy (limited) sync: {err}") + } + } } } + // If the streams in all branches are closed, stop the loop + else => break, } } }; @@ -3320,81 +3320,24 @@ impl Room { async fn get_current_join_requests( &self, - seen_request_ids: &HashSet, - ) -> Result> { + seen_request_ids: &BTreeMap, + ) -> Result> { Ok(self .members(RoomMemberships::KNOCK) .await? .into_iter() .filter_map(|member| { - if let Some(event_id) = member.event().event_id() { - let event_id = event_id.to_owned(); - Some(JoinRequest::new( - self, - &event_id, - member.event().timestamp(), - member.into(), - seen_request_ids.contains(&event_id), - )) - } else { - None - } + let event_id = member.event().event_id()?; + Some(KnockRequest::new( + self, + event_id, + member.event().timestamp(), + KnockRequestMemberInfo::from_member(&member), + seen_request_ids.contains_key(event_id), + )) }) .collect()) } - - /// Mark a list of requests to join the room as seen, given their state - /// event ids. - pub async fn mark_join_requests_as_seen(&self, event_ids: &[OwnedEventId]) -> Result<()> { - let mut current_seen_events = self.get_seen_join_request_ids().await?; - - for event_id in event_ids { - current_seen_events.insert(event_id.to_owned()); - } - - self.seen_join_request_ids.set(Some(current_seen_events.clone())); - - self.client - .store() - .set_kv_data( - StateStoreDataKey::SeenJoinRequests(self.room_id()), - StateStoreDataValue::SeenJoinRequests(current_seen_events), - ) - .await - .map_err(Into::into) - } - - /// Get the list of seen requests to join event ids in this room. - pub async fn get_seen_join_request_ids(&self) -> Result> { - let current_join_request_ids = self.seen_join_request_ids.get(); - let current_join_request_ids: HashSet = - if let Some(requests) = current_join_request_ids.as_ref() { - requests.clone() - } else { - let requests = self - .client - .store() - .get_kv_data(StateStoreDataKey::SeenJoinRequests(self.room_id())) - .await? - .and_then(|v| v.into_seen_join_requests()) - .unwrap_or_default(); - - self.seen_join_request_ids.set(Some(requests.clone())); - requests - }; - Ok(current_join_request_ids) - } - - /// Subscribes to the set of requests to join that have been marked as - /// 'seen'. - pub async fn subscribe_to_seen_join_request_ids( - &self, - ) -> Result<(HashSet, impl Stream>)> { - let current = self.get_seen_join_request_ids().await?; - let subscriber = - self.seen_join_request_ids.subscribe().map(|values| values.unwrap_or_default()); - Ok((current, subscriber)) - } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] @@ -3684,10 +3627,14 @@ pub struct TryFromReportedContentScoreError(()); mod tests { use matrix_sdk_base::{store::ComposerDraftType, ComposerDraft, SessionMeta}; use matrix_sdk_test::{ - async_test, test_json, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, - DEFAULT_TEST_ROOM_ID, + async_test, event_factory::EventFactory, test_json, JoinedRoomBuilder, StateTestEvent, + SyncResponseBuilder, + }; + use ruma::{ + device_id, event_id, + events::room::member::{MembershipState, RoomMemberEventContent}, + int, room_id, user_id, }; - use ruma::{device_id, event_id, int, user_id}; use wiremock::{ matchers::{header, method, path_regex}, Mock, MockServer, ResponseTemplate, @@ -3878,23 +3825,36 @@ mod tests { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; let event_id = event_id!("$a:b.c"); - - let room = server.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; + let room_id = room_id!("!a:b.c"); + let user_id = user_id!("@alice:b.c"); + + let f = EventFactory::new().room(room_id); + let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_bulk(vec![f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(event_id) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast()]); + let room = server.sync_room(&client, joined_room_builder).await; // When loading the initial seen ids, there are none let seen_ids = - room.get_seen_join_request_ids().await.expect("Couldn't load seen join request ids"); + room.get_seen_knock_request_ids().await.expect("Couldn't load seen join request ids"); assert!(seen_ids.is_empty()); // We mark a random event id as seen - room.mark_join_requests_as_seen(&[event_id.to_owned()]) + room.mark_knock_requests_as_seen(&[user_id.to_owned()]) .await .expect("Couldn't mark join request as seen"); // Then we can check it was successfully marked as seen let seen_ids = - room.get_seen_join_request_ids().await.expect("Couldn't load seen join request ids"); + room.get_seen_knock_request_ids().await.expect("Couldn't load seen join request ids"); assert_eq!(seen_ids.len(), 1); - assert_eq!(seen_ids.into_iter().next().expect("No next value"), event_id) + assert_eq!( + seen_ids.into_iter().next().expect("No next value"), + (event_id.to_owned(), user_id.to_owned()) + ) } } diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index a6b5f38949e..4e6ca00de93 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -34,6 +34,7 @@ use ruma::{ int, mxc_uri, owned_event_id, room_id, thirdparty, user_id, OwnedUserId, TransactionId, }; use serde_json::{from_value, json, Value}; +use stream_assert::assert_pending; use wiremock::{ matchers::{body_json, body_partial_json, header, method, path_regex}, Mock, ResponseTemplate, @@ -848,55 +849,56 @@ async fn test_subscribe_to_requests_to_join() { let room_id = room_id!("!a:b.c"); let f = EventFactory::new().room(room_id); - let alice_user_id = user_id!("@alice:b.c"); - let alice_knock_event_id = event_id!("$alice-knock:b.c"); - let alice_knock_event = f + let user_id = user_id!("@alice:b.c"); + let knock_event_id = event_id!("$alice-knock:b.c"); + let knock_event = f .event(RoomMemberEventContent::new(MembershipState::Knock)) - .event_id(alice_knock_event_id) - .sender(alice_user_id) - .state_key(alice_user_id) + .event_id(knock_event_id) + .sender(user_id) + .state_key(user_id) .into_raw_timeline() .cast(); - server.mock_get_members().ok(vec![alice_knock_event]).mock_once().mount().await; + server.mock_get_members().ok(vec![knock_event]).mock_once().mount().await; let room = server.sync_joined_room(&client, room_id).await; - let stream = room.subscribe_to_join_requests().await.unwrap(); + let stream = room.subscribe_to_knock_requests().await.unwrap(); pin_mut!(stream); - // We receive an initial request to join from Alice + // We receive an initial knock request from Alice let initial = assert_next_with_timeout!(stream, 100); - assert!(!initial.is_empty()); + assert_eq!(initial.len(), 1); - let alices_request_to_join = &initial[0]; - assert_eq!(alices_request_to_join.event_id, alice_knock_event_id); - assert!(!alices_request_to_join.is_seen); + let knock_request = &initial[0]; + assert_eq!(knock_request.event_id, knock_event_id); + assert!(!knock_request.is_seen); - // We then mark the request to join as seen - room.mark_join_requests_as_seen(&[alice_knock_event_id.to_owned()]).await.unwrap(); + // We then mark the knock request as seen + room.mark_knock_requests_as_seen(&[user_id.to_owned()]).await.unwrap(); // Now it's received again as seen let seen = assert_next_with_timeout!(stream, 100); - assert!(!seen.is_empty()); - let alices_seen_request_to_join = &seen[0]; - assert_eq!(alices_seen_request_to_join.event_id, alice_knock_event_id); - assert!(alices_seen_request_to_join.is_seen); + assert_eq!(initial.len(), 1); + let seen_knock = &seen[0]; + assert_eq!(seen_knock.event_id, knock_event_id); + assert!(seen_knock.is_seen); // If we then receive a new member event for Alice that's not 'knock' - let alice_join_event_id = event_id!("$alice-join:b.c"); let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_bulk(vec![f .event(RoomMemberEventContent::new(MembershipState::Invite)) - .event_id(alice_join_event_id) - .sender(alice_user_id) - .state_key(alice_user_id) + .sender(user_id) + .state_key(user_id) .into_raw_timeline() .cast()]); server.sync_room(&client, joined_room_builder).await; - // The requests to join are now empty + // The knock requests are now empty let updated_requests = assert_next_with_timeout!(stream, 100); assert!(updated_requests.is_empty()); + + // There should be no other knock requests + assert_pending!(stream) } #[async_test] @@ -909,19 +911,17 @@ async fn test_subscribe_to_requests_to_join_reloads_members_on_limited_sync() { let room_id = room_id!("!a:b.c"); let f = EventFactory::new().room(room_id); - let alice_user_id = user_id!("@alice:b.c"); - let alice_knock_event_id = event_id!("$alice-knock:b.c"); - let alice_knock_event = f + let user_id = user_id!("@alice:b.c"); + let knock_event = f .event(RoomMemberEventContent::new(MembershipState::Knock)) - .event_id(alice_knock_event_id) - .sender(alice_user_id) - .state_key(alice_user_id) + .sender(user_id) + .state_key(user_id) .into_raw_timeline() .cast(); server .mock_get_members() - .ok(vec![alice_knock_event]) + .ok(vec![knock_event]) // The endpoint will be called twice: // 1. For the initial loading of room members. // 2. When a gappy (limited) sync is received. @@ -930,18 +930,21 @@ async fn test_subscribe_to_requests_to_join_reloads_members_on_limited_sync() { .await; let room = server.sync_joined_room(&client, room_id).await; - let stream = room.subscribe_to_join_requests().await.unwrap(); + let stream = room.subscribe_to_knock_requests().await.unwrap(); pin_mut!(stream); - // We receive an initial request to join from Alice + // We receive an initial knock request from Alice let initial = assert_next_with_timeout!(stream, 500); assert!(!initial.is_empty()); - // This limited sync should trigger a new emission of join requests, with a + // This limited sync should trigger a new emission of knock requests, with a // reloading of the room members server.sync_room(&client, JoinedRoomBuilder::new(room_id).set_timeline_limited()).await; - // We should receive a new list of join requests + // We should receive a new list of knock requests assert_next_with_timeout!(stream, 500); + + // There should be no other knock requests + assert_pending!(stream) } From 47044b1a23461df65614b0dce5d199142e19943b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:57:42 +0000 Subject: [PATCH 792/979] chore(deps): bump crate-ci/typos from 1.28.2 to 1.28.3 Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.2 to 1.28.3. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/v1.28.2...v1.28.3) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74a33a3de7d..881b5010dfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,7 +304,7 @@ jobs: uses: actions/checkout@v4 - name: Check the spelling of the files in our repo - uses: crate-ci/typos@v1.28.2 + uses: crate-ci/typos@v1.28.3 clippy: name: Run clippy From f6cb8186c6e6e09bfd0debe34efa5edd3be6f898 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 12 Dec 2024 16:49:39 +0100 Subject: [PATCH 793/979] task(event cache): limit the display of event ids to 8 chars in the raw chunk debug string --- crates/matrix-sdk/src/event_cache/room/mod.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 21e29594b8f..51072771d81 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -816,9 +816,11 @@ mod private { let items = vec .into_iter() .map(|event| { - event - .event_id() - .map_or_else(|| "".to_owned(), |id| id.to_string()) + // Limit event ids to 8 chars *after* the $. + event.event_id().map_or_else( + || "".to_owned(), + |id| id.as_str().chars().take(1 + 8).collect(), + ) }) .collect::>() .join(", "); @@ -857,7 +859,9 @@ mod private { raws.push(RawChunk { content: ChunkContent::Items(vec![ - f.text_msg("hey").event_id(event_id!("$1")).into_sync(), + f.text_msg("hey") + .event_id(event_id!("$123456789101112131415617181920")) + .into_sync(), f.text_msg("you").event_id(event_id!("$2")).into_sync(), ]), identifier: CId::new(1), @@ -875,7 +879,7 @@ mod private { let output = raw_chunks_debug_string(raws); assert_eq!(output.len(), 2); assert_eq!(&output[0], "chunk #0 (prev=, next=1): gap['prev-token']"); - assert_eq!(&output[1], "chunk #1 (prev=0, next=): events[$1, $2]"); + assert_eq!(&output[1], "chunk #1 (prev=0, next=): events[$12345678, $2]"); } } } From 34d15a4d3792cb665f3d9d2b1809eea5ee32d14f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 12 Dec 2024 16:59:46 +0100 Subject: [PATCH 794/979] feat(event cache): propose a debug representation for the linked chunk in `RoomEvents` too --- .../matrix-sdk/src/event_cache/room/events.rs | 18 ++++++- crates/matrix-sdk/src/event_cache/room/mod.rs | 54 +++++++++++-------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 24c1d9ba3b7..83fc2218972 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -26,7 +26,10 @@ use matrix_sdk_common::linked_chunk::{ use ruma::OwnedEventId; use tracing::{debug, error, warn}; -use super::super::deduplicator::{Decoration, Deduplicator}; +use super::{ + super::deduplicator::{Decoration, Deduplicator}, + chunk_debug_string, +}; /// This type represents all events of a single room. #[derive(Debug)] @@ -177,7 +180,6 @@ impl RoomEvents { /// Iterate over the chunks, forward. /// /// The oldest chunk comes first. - #[cfg(test)] pub fn chunks( &self, ) -> matrix_sdk_common::linked_chunk::Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> { @@ -262,6 +264,18 @@ impl RoomEvents { (deduplicated_events, duplicated_event_ids) } + + /// Return a nice debug string (a vector of lines) for the linked chunk of + /// events for this room. + pub fn debug_string(&self) -> Vec { + let mut result = Vec::new(); + for c in self.chunks() { + let content = chunk_debug_string(c.content()); + let line = format!("chunk #{}: {content}", c.identifier().index()); + result.push(line); + } + result + } } // Private implementations, implementation specific. diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 51072771d81..ddbb5ceab56 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -19,6 +19,7 @@ use std::{collections::BTreeMap, fmt, sync::Arc}; use events::Gap; use matrix_sdk_base::{ deserialized_responses::{AmbiguityChange, SyncTimelineEvent}, + linked_chunk::ChunkContent, sync::{JoinedRoomUpdate, LeftRoomUpdate, Timeline}, }; use ruma::{ @@ -215,6 +216,12 @@ impl RoomEventCache { } } } + + /// Return a nice debug string (a vector of lines) for the linked chunk of + /// events for this room. + pub async fn debug_string(&self) -> Vec { + self.inner.state.read().await.events().debug_string() + } } /// The (non-cloneable) details of the `RoomEventCache`. @@ -572,6 +579,29 @@ impl RoomEventCacheInner { } } +/// Create a debug string for a [`ChunkContent`] for an event/gap pair. +fn chunk_debug_string(content: &ChunkContent) -> String { + match content { + ChunkContent::Gap(Gap { prev_token }) => { + format!("gap['{prev_token}']") + } + ChunkContent::Items(vec) => { + let items = vec + .iter() + .map(|event| { + // Limit event ids to 8 chars *after* the $. + event.event_id().map_or_else( + || "".to_owned(), + |id| id.as_str().chars().take(1 + 8).collect(), + ) + }) + .collect::>() + .join(", "); + format!("events[{items}]") + } + } +} + // Use a private module to hide `events` to this parent module. mod private { use std::sync::Arc; @@ -585,13 +615,13 @@ mod private { }, Event, Gap, }, - linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, RawChunk, Update}, + linked_chunk::{LinkedChunk, LinkedChunkBuilder, RawChunk, Update}, }; use once_cell::sync::OnceCell; use ruma::{serde::Raw, OwnedRoomId, RoomId}; use tracing::{error, trace}; - use super::events::RoomEvents; + use super::{chunk_debug_string, events::RoomEvents}; use crate::event_cache::EventCacheError; /// State for a single room's event cache. @@ -808,25 +838,7 @@ mod private { raw_chunks.sort_by_key(|c| c.identifier.index()); for c in raw_chunks { - let content = match c.content { - ChunkContent::Gap(Gap { prev_token }) => { - format!("gap['{prev_token}']") - } - ChunkContent::Items(vec) => { - let items = vec - .into_iter() - .map(|event| { - // Limit event ids to 8 chars *after* the $. - event.event_id().map_or_else( - || "".to_owned(), - |id| id.as_str().chars().take(1 + 8).collect(), - ) - }) - .collect::>() - .join(", "); - format!("events[{items}]") - } - }; + let content = chunk_debug_string(&c.content); let prev = c.previous.map_or_else(|| "".to_owned(), |prev| prev.index().to_string()); From cae7e43b91d6e4d0c58d27b82f8f6f8de79fa710 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 12 Dec 2024 17:12:32 +0100 Subject: [PATCH 795/979] feat(multiverse): add linked chunk debug screen in multiverse --- labs/multiverse/src/main.rs | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index e61cc17348b..50f4c3e7ac9 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -117,6 +117,7 @@ enum DetailsMode { #[default] TimelineItems, Events, + LinkedChunk, } struct Timeline { @@ -517,13 +518,17 @@ impl App { }; } - Char('l') => self.toggle_reaction_to_latest_msg().await, + Char('L') => self.toggle_reaction_to_latest_msg().await, Char('r') => self.details_mode = DetailsMode::ReadReceipts, Char('t') => self.details_mode = DetailsMode::TimelineItems, Char('e') => self.details_mode = DetailsMode::Events, + Char('l') => self.details_mode = DetailsMode::LinkedChunk, - Char('b') if self.details_mode == DetailsMode::TimelineItems => { + Char('b') + if self.details_mode == DetailsMode::TimelineItems + || self.details_mode == DetailsMode::LinkedChunk => + { self.back_paginate(); } @@ -751,6 +756,29 @@ impl App { } } + DetailsMode::LinkedChunk => { + // In linked chunk mode, show a rough representation of the chunks. + match self.ui_rooms.lock().unwrap().get(&room_id).cloned() { + Some(room) => { + let lines = tokio::task::block_in_place(|| { + Handle::current().block_on(async { + let (cache, _drop_guards) = room + .event_cache() + .await + .expect("no event cache for that room"); + cache.debug_string().await + }) + }); + render_paragraph(buf, lines.join("\n")); + } + + None => render_paragraph( + buf, + "(room disappeared in the room list service)".to_owned(), + ), + } + } + DetailsMode::Events => match self.ui_rooms.lock().unwrap().get(&room_id).cloned() { Some(room) => { let events = tokio::task::block_in_place(|| { @@ -883,11 +911,14 @@ impl App { "\nUse j/k to move, s/S to start/stop the sync service, m to mark as read, t to show the timeline, e to show events.".to_owned() } DetailsMode::TimelineItems => { - "\nUse j/k to move, s/S to start/stop the sync service, r to show read receipts, e to show events, Q to enable/disable the send queue, M to send a message.".to_owned() + "\nUse j/k to move, s/S to start/stop the sync service, r to show read receipts, e to show events, Q to enable/disable the send queue, M to send a message, L to like the last message.".to_owned() } DetailsMode::Events => { "\nUse j/k to move, s/S to start/stop the sync service, r to show read receipts, t to show the timeline".to_owned() } + DetailsMode::LinkedChunk => { + "\nUse j/k to move, s/S to start/stop the sync service, r to show read receipts, t to show the timeline, e to show events".to_owned() + } } }; Paragraph::new(content).centered().render(area, buf); From 34ea42aec02739ec4978e7a10500ca2d27066e8f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Dec 2024 16:00:17 +0100 Subject: [PATCH 796/979] feat(ffi): expose the linked chunk debug string function at the FFI layer --- bindings/matrix-sdk-ffi/src/room.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index c1233b51151..124fc74be26 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -933,6 +933,13 @@ impl Room { Ok(handle) } + + /// Return a debug representation for the internal room events data + /// structure, one line per entry in the resulting vector. + pub async fn room_events_debug_string(&self) -> Result, ClientError> { + let (cache, _drop_guards) = self.inner.event_cache().await?; + Ok(cache.debug_string().await) + } } impl From for KnockRequest { From 866b5fea40bea68be870f3cf02d59401c2d9933f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 13 Dec 2024 13:08:29 +0100 Subject: [PATCH 797/979] feat(room): Separate `RoomState::Banned` from `RoomState::Left`. This is needed to tell apart rooms in left and banned state in places like `RoomInfo` or `RoomPreview`. The banned rooms will still count as left rooms in the sync processes. --- bindings/matrix-sdk-ffi/src/room.rs | 2 + crates/matrix-sdk-base/src/rooms/normal.rs | 100 ++++++++++++++++-- .../matrix-sdk-base/src/sliding_sync/mod.rs | 19 +++- 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 124fc74be26..1402d5d9867 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -50,6 +50,7 @@ pub enum Membership { Joined, Left, Knocked, + Banned, } impl From for Membership { @@ -59,6 +60,7 @@ impl From for Membership { RoomState::Joined => Membership::Joined, RoomState::Left => Membership::Left, RoomState::Knocked => Membership::Knocked, + RoomState::Banned => Membership::Banned, } } } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index a7a5d4ea983..0370f13b70e 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -225,14 +225,14 @@ pub enum RoomState { Invited, /// The room is in a knocked state. Knocked, + /// The room is in a banned state. + Banned, } impl From<&MembershipState> for RoomState { fn from(membership_state: &MembershipState) -> Self { - // We consider Ban, Knock and Leave to be Left, because they all mean we are not - // in the room. match membership_state { - MembershipState::Ban => Self::Left, + MembershipState::Ban => Self::Banned, MembershipState::Invite => Self::Invited, MembershipState::Join => Self::Joined, MembershipState::Knock => Self::Knocked, @@ -465,7 +465,7 @@ impl Room { #[instrument(skip_all, fields(room_id = ?self.room_id))] pub async fn is_direct(&self) -> StoreResult { match self.state() { - RoomState::Joined | RoomState::Left => { + RoomState::Joined | RoomState::Left | RoomState::Banned => { Ok(!self.inner.read().base_info.dm_targets.is_empty()) } @@ -1420,6 +1420,11 @@ impl RoomInfo { self.set_state(RoomState::Knocked); } + /// Mark this Room as banned. + pub fn mark_as_banned(&mut self) { + self.set_state(RoomState::Banned); + } + /// Set the membership RoomState of this Room pub fn set_state(&mut self, room_state: RoomState) { if room_state != self.room_state { @@ -1981,6 +1986,8 @@ bitflags! { const LEFT = 0b00000100; /// The room is in a knocked state. const KNOCKED = 0b00001000; + /// The room is in a banned state. + const BANNED = 0b00010000; } } @@ -1996,6 +2003,7 @@ impl RoomStateFilter { RoomState::Left => Self::LEFT, RoomState::Invited => Self::INVITED, RoomState::Knocked => Self::KNOCKED, + RoomState::Banned => Self::BANNED, }; self.contains(bit_state) @@ -2014,6 +2022,12 @@ impl RoomStateFilter { if self.contains(Self::INVITED) { states.push(RoomState::Invited); } + if self.contains(Self::KNOCKED) { + states.push(RoomState::Knocked); + } + if self.contains(Self::BANNED) { + states.push(RoomState::Banned); + } states } @@ -2093,7 +2107,7 @@ mod tests { }, AnySyncStateEvent, EmptyStateKey, StateEventType, StateUnsigned, SyncStateEvent, }, - owned_event_id, owned_user_id, room_alias_id, room_id, + owned_event_id, owned_room_id, owned_user_id, room_alias_id, room_id, serde::Raw, time::SystemTime, user_id, DeviceId, EventEncryptionAlgorithm, EventId, MilliSecondsSinceUnixEpoch, @@ -2109,8 +2123,9 @@ mod tests { use crate::{ rooms::RoomNotableTags, store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig}, + test_utils::logged_in_base_client, BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName, - RoomInfoNotableUpdateReasons, SessionMeta, + RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta, }; #[test] @@ -3629,5 +3644,78 @@ mod tests { room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP); assert_eq!(room.prev_state(), Some(RoomState::Joined)); assert_eq!(room.state(), RoomState::Left); + + // Left -> Banned + let mut room_info = room.clone_info(); + room_info.mark_as_banned(); + room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP); + assert_eq!(room.prev_state(), Some(RoomState::Left)); + assert_eq!(room.state(), RoomState::Banned); + } + + #[async_test] + async fn test_room_state_filters() { + let client = logged_in_base_client(None).await; + + let joined_room_id = owned_room_id!("!joined:example.org"); + client.get_or_create_room(&joined_room_id, RoomState::Joined); + + let invited_room_id = owned_room_id!("!invited:example.org"); + client.get_or_create_room(&invited_room_id, RoomState::Invited); + + let left_room_id = owned_room_id!("!left:example.org"); + client.get_or_create_room(&left_room_id, RoomState::Left); + + let knocked_room_id = owned_room_id!("!knocked:example.org"); + client.get_or_create_room(&knocked_room_id, RoomState::Knocked); + + let banned_room_id = owned_room_id!("!banned:example.org"); + client.get_or_create_room(&banned_room_id, RoomState::Banned); + + let joined_rooms = client.rooms_filtered(RoomStateFilter::JOINED); + assert_eq!(joined_rooms.len(), 1); + assert_eq!(joined_rooms[0].state(), RoomState::Joined); + assert_eq!(joined_rooms[0].room_id, joined_room_id); + + let invited_rooms = client.rooms_filtered(RoomStateFilter::INVITED); + assert_eq!(invited_rooms.len(), 1); + assert_eq!(invited_rooms[0].state(), RoomState::Invited); + assert_eq!(invited_rooms[0].room_id, invited_room_id); + + let left_rooms = client.rooms_filtered(RoomStateFilter::LEFT); + assert_eq!(left_rooms.len(), 1); + assert_eq!(left_rooms[0].state(), RoomState::Left); + assert_eq!(left_rooms[0].room_id, left_room_id); + + let knocked_rooms = client.rooms_filtered(RoomStateFilter::KNOCKED); + assert_eq!(knocked_rooms.len(), 1); + assert_eq!(knocked_rooms[0].state(), RoomState::Knocked); + assert_eq!(knocked_rooms[0].room_id, knocked_room_id); + + let banned_rooms = client.rooms_filtered(RoomStateFilter::BANNED); + assert_eq!(banned_rooms.len(), 1); + assert_eq!(banned_rooms[0].state(), RoomState::Banned); + assert_eq!(banned_rooms[0].room_id, banned_room_id); + } + + #[test] + fn test_room_state_filters_as_vec() { + assert_eq!(RoomStateFilter::JOINED.as_vec(), vec![RoomState::Joined]); + assert_eq!(RoomStateFilter::LEFT.as_vec(), vec![RoomState::Left]); + assert_eq!(RoomStateFilter::INVITED.as_vec(), vec![RoomState::Invited]); + assert_eq!(RoomStateFilter::KNOCKED.as_vec(), vec![RoomState::Knocked]); + assert_eq!(RoomStateFilter::BANNED.as_vec(), vec![RoomState::Banned]); + + // Check all filters are taken into account + assert_eq!( + RoomStateFilter::all().as_vec(), + vec![ + RoomState::Joined, + RoomState::Left, + RoomState::Invited, + RoomState::Knocked, + RoomState::Banned + ] + ); } } diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 3bf2669030e..d8a96aa34a6 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -269,7 +269,7 @@ impl BaseClient { .or_insert_with(JoinedRoomUpdate::default) .account_data .append(&mut raw.to_vec()), - RoomState::Left => new_rooms + RoomState::Left | RoomState::Banned => new_rooms .leave .entry(room_id.to_owned()) .or_insert_with(LeftRoomUpdate::default) @@ -546,7 +546,7 @@ impl BaseClient { )) } - RoomState::Left => Ok(( + RoomState::Left | RoomState::Banned => Ok(( room_info, None, Some(LeftRoomUpdate::new( @@ -1247,7 +1247,7 @@ mod tests { room.required_state.push(make_state_event( user_b_id, user_a_id.as_str(), - RoomMemberEventContent::new(membership), + RoomMemberEventContent::new(membership.clone()), None, )); let response = response_with_room(room_id, room); @@ -1256,8 +1256,17 @@ mod tests { .await .expect("Failed to process sync"); - // The room is left. - assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); + match membership { + MembershipState::Leave => { + // The room is left. + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); + } + MembershipState::Ban => { + // The room is banned. + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Banned); + } + _ => panic!("Unexpected membership state found: {membership}"), + } // And it is added to the list of left rooms only. assert!(!sync_resp.rooms.join.contains_key(room_id)); From 95582a6c3caa5a6ef0f911450c374d2aef58c7d0 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 17 Dec 2024 09:51:28 +0100 Subject: [PATCH 798/979] feat(crypto-bindings): Save/Load dehydrated pickle key review: better tests --- .../src/dehydrated_devices.rs | 94 +++++++++++++++---- bindings/matrix-sdk-crypto-ffi/src/error.rs | 7 +- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 40 +++++++- .../src/dehydrated_devices.rs | 4 + crates/matrix-sdk-crypto/src/store/mod.rs | 60 +++++++++++- 5 files changed, 181 insertions(+), 24 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs index ae05014c943..d774b13f5db 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs @@ -5,12 +5,13 @@ use matrix_sdk_crypto::{ DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices, RehydratedDevice as InnerRehydratedDevice, }, - store::DehydratedDeviceKey, + store::DehydratedDeviceKey as InnerDehydratedDeviceKey, }; use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId}; use serde_json::json; use tokio::runtime::Handle; -use zeroize::Zeroize; + +use crate::{CryptoStoreError, DehydratedDeviceKey}; #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] @@ -25,6 +26,8 @@ pub enum DehydrationError { Store(#[from] matrix_sdk_crypto::CryptoStoreError), #[error("The pickle key has an invalid length, expected 32 bytes, got {0}")] PickleKeyLength(usize), + #[error(transparent)] + Rand(#[from] rand::Error), } impl From for DehydrationError { @@ -36,6 +39,9 @@ impl From for Dehydrati Self::MissingSigningKey(e) } matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e), + matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => { + Self::PickleKeyLength(l) + } } } } @@ -69,14 +75,14 @@ impl DehydratedDevices { pub fn rehydrate( &self, - pickle_key: Vec, + pickle_key: &DehydratedDeviceKey, device_id: String, device_data: String, ) -> Result, DehydrationError> { let device_data: Raw<_> = serde_json::from_str(&device_data)?; let device_id: OwnedDeviceId = device_id.into(); - let mut key = get_pickle_key(&pickle_key)?; + let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?; let ret = RehydratedDevice { runtime: self.runtime.to_owned(), @@ -88,10 +94,41 @@ impl DehydratedDevices { } .into(); - key.zeroize(); - Ok(ret) } + + /// Get the cached dehydrated device pickle key if any. + /// + /// None if the key was not previously cached (via + /// [`Self::save_dehydrated_device_pickle_key`]). + /// + /// Should be used to periodically rotate the dehydrated device to avoid + /// OTK exhaustion and accumulation of to_device messages. + pub fn get_dehydrated_device_key( + &self, + ) -> Result, CryptoStoreError> { + Ok(self + .runtime + .block_on(self.inner.get_dehydrated_device_pickle_key())? + .map(crate::DehydratedDeviceKey::from)) + } + + /// Store the dehydrated device pickle key in the crypto store. + /// + /// This is useful if the client wants to periodically rotate dehydrated + /// devices to avoid OTK exhaustion and accumulated to_device problems. + pub fn save_dehydrated_device_key( + &self, + pickle_key: &crate::DehydratedDeviceKey, + ) -> Result<(), CryptoStoreError> { + let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?; + Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?) + } + + /// Deletes the previously stored dehydrated device pickle key. + pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> { + Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?) + } } #[derive(uniffi::Object)] @@ -141,15 +178,13 @@ impl DehydratedDevice { pub fn keys_for_upload( &self, device_display_name: String, - pickle_key: Vec, + pickle_key: &DehydratedDeviceKey, ) -> Result { - let mut key = get_pickle_key(&pickle_key)?; + let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?; let request = self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?; - key.zeroize(); - Ok(request.into()) } } @@ -180,15 +215,36 @@ impl From } } -fn get_pickle_key(pickle_key: &[u8]) -> Result { - let pickle_key_length = pickle_key.len(); +#[cfg(test)] +mod tests { + use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey}; + + #[test] + fn test_creating_dehydrated_key() { + let result = DehydratedDeviceKey::new(); + assert!(result.is_ok()); + let dehydrated_device_key = result.unwrap(); + let base_64 = dehydrated_device_key.to_base64(); + let inner_bytes = dehydrated_device_key.inner; + + let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap(); + + assert_eq!(base_64, copy.to_base64()); + } - if pickle_key_length == 32 { - let mut raw_bytes = [0u8; 32]; - raw_bytes.copy_from_slice(pickle_key); - let key = DehydratedDeviceKey::from_bytes(&raw_bytes); - Ok(key) - } else { - Err(DehydrationError::PickleKeyLength(pickle_key_length)) + #[test] + fn test_creating_dehydrated_key_failure() { + let bytes = [0u8; 24]; + + let pickle_key = DehydratedDeviceKey::from_slice(&bytes); + + assert!(pickle_key.is_err()); + + match pickle_key { + Err(DehydrationError::PickleKeyLength(pickle_key_length)) => { + assert_eq!(bytes.len(), pickle_key_length); + } + _ => panic!("Should have failed!"), + } } } diff --git a/bindings/matrix-sdk-crypto-ffi/src/error.rs b/bindings/matrix-sdk-crypto-ffi/src/error.rs index 116244b68cf..e84685fe4a6 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/error.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/error.rs @@ -1,8 +1,9 @@ #![allow(missing_docs)] use matrix_sdk_crypto::{ - store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError, - SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError, + store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError}, + KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError, + SignatureError as InnerSignatureError, }; use matrix_sdk_sqlite::OpenStoreError; use ruma::{IdParseError, OwnedUserId}; @@ -57,6 +58,8 @@ pub enum CryptoStoreError { InvalidUserId(String, IdParseError), #[error(transparent)] Identifier(#[from] IdParseError), + #[error(transparent)] + DehydrationError(#[from] InnerDehydrationError), } #[derive(Debug, thiserror::Error, uniffi::Error)] diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 125abd99ed3..e6c09013edd 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -36,7 +36,10 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification}; use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode}; use matrix_sdk_crypto::{ olm::{IdentityKeys, InboundGroupSession, SenderData, Session}, - store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings}, + store::{ + Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges, + RoomSettings as RustRoomSettings, + }, types::{ DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey, }, @@ -62,6 +65,8 @@ pub use verification::{ }; use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; +use crate::dehydrated_devices::DehydrationError; + /// Struct collecting data that is important to migrate to the rust-sdk #[derive(Deserialize, Serialize, uniffi::Record)] pub struct MigrationData { @@ -822,6 +827,39 @@ impl TryFrom for BackupKeys { } } +/// Dehydrated device key +#[derive(uniffi::Record, Clone)] +pub struct DehydratedDeviceKey { + pub(crate) inner: Vec, +} + +impl DehydratedDeviceKey { + /// Generates a new random pickle key. + pub fn new() -> Result { + let inner = InnerDehydratedDeviceKey::new()?; + Ok(inner.into()) + } + + /// Creates a new dehydration pickle key from the given slice. + /// + /// Fail if the slice length is not 32. + pub fn from_slice(slice: &[u8]) -> Result { + let inner = InnerDehydratedDeviceKey::from_slice(slice)?; + Ok(inner.into()) + } + + /// Export the [`DehydratedDeviceKey`] as a base64 encoded string. + pub fn to_base64(&self) -> String { + let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap(); + inner.to_base64() + } +} +impl From for DehydratedDeviceKey { + fn from(pickle_key: InnerDehydratedDeviceKey) -> Self { + DehydratedDeviceKey { inner: pickle_key.into() } + } +} + impl From for RoomKeyCounts { fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self { Self { total: count.total as i64, backed_up: count.backed_up as i64 } diff --git a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs index 5231e1f4418..2b81bfe3477 100644 --- a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs +++ b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs @@ -69,6 +69,10 @@ pub enum DehydrationError { #[error(transparent)] Pickle(#[from] LibolmPickleError), + /// The pickle key has an invalid length + #[error("The pickle key has an invalid length, expected 32 bytes, got {0}")] + PickleKeyLength(usize), + /// The dehydrated device could not be signed by our user identity, /// we're missing the self-signing key. #[error("The self-signing key is missing, can't create a dehydrated device")] diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 0fa133824c8..631e3a33355 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -96,7 +96,10 @@ use matrix_sdk_common::{store_locks::CrossProcessStoreLock, timeout::timeout}; pub use memorystore::MemoryStore; pub use traits::{CryptoStore, DynCryptoStore, IntoCryptoStore}; -pub use crate::gossiping::{GossipRequest, SecretInfo}; +pub use crate::{ + dehydrated_devices::DehydrationError, + gossiping::{GossipRequest, SecretInfo}, +}; /// A wrapper for our CryptoStore trait object. /// @@ -775,6 +778,19 @@ impl DehydratedDeviceKey { Ok(Self { inner: key }) } + /// Creates a new dehydration pickle key from the given slice. + /// + /// Fail if the slice length is not 32. + pub fn from_slice(slice: &[u8]) -> Result { + if slice.len() == 32 { + let mut key = Box::new([0u8; 32]); + key.copy_from_slice(slice); + Ok(DehydratedDeviceKey { inner: key }) + } else { + Err(DehydrationError::PickleKeyLength(slice.len())) + } + } + /// Creates a dehydration pickle key from the given bytes. pub fn from_bytes(raw_key: &[u8; 32]) -> Self { let mut inner = Box::new([0u8; Self::KEY_SIZE]); @@ -789,6 +805,18 @@ impl DehydratedDeviceKey { } } +impl From<&[u8; 32]> for DehydratedDeviceKey { + fn from(value: &[u8; 32]) -> Self { + DehydratedDeviceKey { inner: Box::new(*value) } + } +} + +impl From for Vec { + fn from(key: DehydratedDeviceKey) -> Self { + key.inner.to_vec() + } +} + #[cfg(not(tarpaulin_include))] impl Debug for DehydratedDeviceKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -1992,7 +2020,10 @@ mod tests { use matrix_sdk_test::async_test; use ruma::{room_id, user_id}; - use crate::{machine::test_helpers::get_machine_pair, types::EventEncryptionAlgorithm}; + use crate::{ + machine::test_helpers::get_machine_pair, store::DehydratedDeviceKey, + types::EventEncryptionAlgorithm, + }; #[async_test] async fn test_import_room_keys_notifies_stream() { @@ -2129,4 +2160,29 @@ mod tests { assert!(status.is_complete(), "We should have imported all the cross-signing keys"); } + + #[async_test] + async fn test_create_dehydrated_device_key() { + let pickle_key = DehydratedDeviceKey::new() + .expect("Should be able to create a random dehydrated device key"); + + let to_vec = pickle_key.inner.to_vec(); + let pickle_key_from_slice = DehydratedDeviceKey::from_slice(to_vec.as_slice()) + .expect("Should be able to create a dehydrated device key from slice"); + + assert_eq!(pickle_key_from_slice.to_base64(), pickle_key.to_base64()); + } + + #[async_test] + async fn test_create_dehydrated_errors() { + let too_small = [0u8; 22]; + let pickle_key = DehydratedDeviceKey::from_slice(&too_small); + + assert!(pickle_key.is_err()); + + let too_big = [0u8; 40]; + let pickle_key = DehydratedDeviceKey::from_slice(&too_big); + + assert!(pickle_key.is_err()); + } } From 177ec1216f907d55d7a170b6cbaa78ad48abe3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 28 Nov 2024 12:45:56 +0100 Subject: [PATCH 799/979] feat(crypto)!: Don't ignore the error if the room_keys_received stream lags --- .../src/machine/tests/megolm_sender_data.rs | 34 ++++++++++--------- .../src/machine/tests/mod.rs | 3 +- .../src/store/crypto_store_wrapper.rs | 16 +++++---- crates/matrix-sdk-crypto/src/store/mod.rs | 12 ++++--- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs index 0c91b97471a..95d246b3301 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs @@ -1,18 +1,16 @@ -/* -Copyright 2024 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use std::{fmt::Debug, iter, pin::Pin}; @@ -23,6 +21,7 @@ use matrix_sdk_test::async_test; use ruma::{room_id, user_id, RoomId, TransactionId, UserId}; use serde::Serialize; use serde_json::json; +use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use crate::{ machine::{ @@ -305,13 +304,16 @@ where /// Given the `room_keys_received_stream`, check that there is a pending update, /// and pop it. fn get_room_key_received_update( - room_keys_received_stream: &mut Pin>>>, + room_keys_received_stream: &mut Pin< + Box, BroadcastStreamRecvError>>>, + >, ) -> RoomKeyInfo { room_keys_received_stream .next() .now_or_never() .flatten() .expect("We should have received an update of room key infos") + .unwrap() .pop() .expect("Received an empty room key info update") } diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 86c2526e4b9..09ea5107254 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -530,7 +530,8 @@ async fn test_megolm_encryption() { .next() .now_or_never() .flatten() - .expect("We should have received an update of room key infos"); + .expect("We should have received an update of room key infos") + .unwrap(); assert_eq!(room_keys.len(), 1); assert_eq!(room_keys[0].session_id, group_session.session_id()); diff --git a/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs b/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs index 609c5fe8262..e7f40800095 100644 --- a/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs +++ b/crates/matrix-sdk-crypto/src/store/crypto_store_wrapper.rs @@ -4,7 +4,10 @@ use futures_core::Stream; use futures_util::StreamExt; use matrix_sdk_common::store_locks::CrossProcessStoreLock; use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId}; -use tokio::sync::{broadcast, Mutex}; +use tokio::sync::{ + broadcast::{self}, + Mutex, +}; use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}; use tracing::{debug, trace, warn}; @@ -292,11 +295,12 @@ impl CryptoStoreWrapper { /// the stream. Updates that happen at the same time are batched into a /// [`Vec`]. /// - /// If the reader of the stream lags too far behind, a warning will be - /// logged and items will be dropped. - pub fn room_keys_received_stream(&self) -> impl Stream> { - let stream = BroadcastStream::new(self.room_keys_received_sender.subscribe()); - Self::filter_errors_out_of_stream(stream, "room_keys_received_stream") + /// If the reader of the stream lags too far behind an error will be sent to + /// the reader. + pub fn room_keys_received_stream( + &self, + ) -> impl Stream, BroadcastStreamRecvError>> { + BroadcastStream::new(self.room_keys_received_sender.subscribe()) } /// Receive notifications of received `m.room_key.withheld` messages. diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 631e3a33355..662829f8c35 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -57,6 +57,7 @@ use ruma::{ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use thiserror::Error; use tokio::sync::{Mutex, MutexGuard, Notify, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock}; +use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tracing::{info, warn}; use vodozemac::{base64_encode, megolm::SessionOrdering, Curve25519PublicKey}; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -1593,12 +1594,14 @@ impl Store { /// the stream. Updates that happen at the same time are batched into a /// [`Vec`]. /// - /// If the reader of the stream lags too far behind, a warning will be - /// logged and items will be dropped. + /// If the reader of the stream lags too far behind an error will be sent to + /// the reader. /// /// The stream will terminate once all references to the underlying /// `CryptoStoreWrapper` are dropped. - pub fn room_keys_received_stream(&self) -> impl Stream> { + pub fn room_keys_received_stream( + &self, + ) -> impl Stream, BroadcastStreamRecvError>> { self.inner.store.room_keys_received_stream() } @@ -2043,7 +2046,8 @@ mod tests { .next() .now_or_never() .flatten() - .expect("We should have received an update of room key infos"); + .expect("We should have received an update of room key infos") + .unwrap(); assert_eq!(room_keys.len(), 1); assert_eq!(room_keys[0].room_id, "!room1:localhost"); } From f17f4e2bf6963f309b6a705f008e07256c7878e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 28 Nov 2024 12:47:15 +0100 Subject: [PATCH 800/979] feat: Add a stream to listen to room keys being inserted to the store --- crates/matrix-sdk/src/encryption/mod.rs | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 78376cf99f8..2344fbc7779 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -31,6 +31,7 @@ use futures_util::{ stream::{self, StreamExt}, }; use matrix_sdk_base::crypto::{ + store::RoomKeyInfo, types::requests::{ OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, }, @@ -58,6 +59,7 @@ use ruma::{ }; use serde::Deserialize; use tokio::sync::{Mutex, RwLockReadGuard}; +use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tracing::{debug, error, instrument, trace, warn}; use url::Url; use vodozemac::Curve25519PublicKey; @@ -1444,6 +1446,45 @@ impl Encryption { Ok(ret) } + /// Receive notifications of room keys being received as a [`Stream`]. + /// + /// Each time a room key is updated in any way, an update will be sent to + /// the stream. Updates that happen at the same time are batched into a + /// [`Vec`]. + /// + /// If the reader of the stream lags too far behind, an error is broadcast + /// containing the number of skipped items. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// use futures_util::StreamExt; + /// + /// let Some(mut room_keys_stream) = + /// client.encryption().room_keys_received_stream().await + /// else { + /// return Ok(()); + /// }; + /// + /// while let Some(update) = room_keys_stream.next().await { + /// println!("Received room keys {update:?}"); + /// } + /// # anyhow::Ok(()) }; + /// ``` + pub async fn room_keys_received_stream( + &self, + ) -> Option, BroadcastStreamRecvError>>> { + let olm = self.client.olm_machine().await; + let olm = olm.as_ref()?; + + Some(olm.store().room_keys_received_stream()) + } + /// Get the secret storage manager of the client. pub fn secret_storage(&self) -> SecretStorage { SecretStorage { client: self.client.to_owned() } From bd15f4ecbe136c864fc6ce5898689f64b09ae11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 28 Nov 2024 13:03:22 +0100 Subject: [PATCH 801/979] feat(timeline): Listen to the room keys stream to retry decryptions --- crates/matrix-sdk-ui/src/timeline/builder.rs | 43 ++++++++++++++++++++ crates/matrix-sdk-ui/src/timeline/mod.rs | 2 + 2 files changed, 45 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index b3e7ec8cd01..44bba18431f 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -23,6 +23,7 @@ use matrix_sdk::{ }; use ruma::{events::AnySyncTimelineEvent, RoomVersionId}; use tokio::sync::broadcast::error::RecvError; +use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tracing::{info, info_span, trace, warn, Instrument, Span}; use super::{ @@ -433,6 +434,47 @@ impl TimelineBuilder { }) }; + // TODO: Technically, this should be the only stream we need to listen to get + // notified when we should retry to decrypt an event. We sadly can't do that, + // since the cross-process support kills the `OlmMachine` which then in + // turn kills this stream. Once this is solved remove all the other ways we + // listen for room keys. + let room_keys_received_join_handle = { + let inner = controller.clone(); + let stream = client.encryption().room_keys_received_stream().await.expect( + "We should be logged in by now, so we should have access to an OlmMachine \ + to be able to listen to this stream", + ); + + spawn(async move { + pin_mut!(stream); + + while let Some(room_keys) = stream.next().await { + let session_ids = match room_keys { + Ok(room_keys) => { + let session_ids: BTreeSet = room_keys + .into_iter() + .filter(|info| info.room_id == inner.room().room_id()) + .map(|info| info.session_id) + .collect(); + + Some(session_ids) + } + Err(BroadcastStreamRecvError::Lagged(missed_updates)) => { + // We lagged, let's retry to decrypt anything we have, maybe something + // was received. + warn!(missed_updates, "The room keys stream has lagged, retrying to decrypt the whole timeline"); + + None + } + }; + + let room = inner.room(); + inner.retry_event_decryption(room, session_ids).await; + } + }) + }; + let timeline = Timeline { controller, event_cache: room_event_cache, @@ -443,6 +485,7 @@ impl TimelineBuilder { pinned_events_join_handle, room_key_from_backups_join_handle, room_key_backup_enabled_join_handle, + room_keys_received_join_handle, local_echo_listener_handle, _event_cache_drop_handle: event_cache_drop, encryption_changes_handle, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 9a9623e3f02..927180093cc 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -832,6 +832,7 @@ struct TimelineDropHandle { room_update_join_handle: JoinHandle<()>, pinned_events_join_handle: Option>, room_key_from_backups_join_handle: JoinHandle<()>, + room_keys_received_join_handle: JoinHandle<()>, room_key_backup_enabled_join_handle: JoinHandle<()>, local_echo_listener_handle: JoinHandle<()>, _event_cache_drop_handle: Arc, @@ -852,6 +853,7 @@ impl Drop for TimelineDropHandle { self.room_update_join_handle.abort(); self.room_key_from_backups_join_handle.abort(); self.room_key_backup_enabled_join_handle.abort(); + self.room_keys_received_join_handle.abort(); self.encryption_changes_handle.abort(); } } From daeffc07b3627441ec7345f14ed463e5374bba2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 17 Dec 2024 14:00:23 +0100 Subject: [PATCH 802/979] feat: Derive PartialEq and Eq for RoomListLoadingState --- crates/matrix-sdk-ui/src/room_list_service/room_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index 8056c14dac8..99d8fa10bb0 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -254,7 +254,7 @@ fn merge_stream_and_receiver( /// When a [`RoomList`] is displayed to the user, it can be in various states. /// This enum tries to represent those states with a correct level of /// abstraction. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum RoomListLoadingState { /// The [`RoomList`] has not been loaded yet, i.e. a sync might run /// or not run at all, there is nothing to show in this `RoomList` yet. From 0ca35d6c4a1a3512e60a48cb75afeaf17afaf20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 17 Dec 2024 13:59:26 +0100 Subject: [PATCH 803/979] test: Test that room keys received by notification clients trigger redecryptions --- .../src/tests/timeline.rs | 174 +++++++++++++++++- 1 file changed, 170 insertions(+), 4 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 531ca7a1d21..0b63cfb1c26 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -25,16 +25,20 @@ use matrix_sdk::{ encryption::{backups::BackupState, EncryptionSettings}, room::edit::EditedContent, ruma::{ - api::client::room::create_room::v3::Request as CreateRoomRequest, + api::client::room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, events::{ room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, InitialStateEvent, }, MilliSecondsSinceUnixEpoch, }, + RoomState, }; -use matrix_sdk_ui::timeline::{ - EventSendState, ReactionStatus, RoomExt, TimelineItem, TimelineItemContent, +use matrix_sdk_ui::{ + notification_client::NotificationClient, + room_list_service::RoomListLoadingState, + sync_service::SyncService, + timeline::{EventSendState, ReactionStatus, RoomExt, TimelineItem, TimelineItemContent}, }; use similar_asserts::assert_eq; use tokio::{ @@ -357,7 +361,7 @@ async fn test_enabling_backups_retries_decryption() { .create_room(assign!(CreateRoomRequest::new(), { is_direct: true, initial_state, - preset: Some(matrix_sdk::ruma::api::client::room::create_room::v3::RoomPreset::PrivateChat) + preset: Some(RoomPreset::PrivateChat) })) .await .unwrap(); @@ -462,3 +466,165 @@ async fn test_enabling_backups_retries_decryption() { bob_sync.abort(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_room_keys_received_on_notification_client_trigger_redecryption() { + let alice = TestClientBuilder::new("alice").use_sqlite().build().await.unwrap(); + alice.encryption().wait_for_e2ee_initialization_tasks().await; + + // Set up sync for user Alice, and create a room. + let alice_sync = spawn({ + let alice = alice.clone(); + async move { + alice.sync(Default::default()).await.expect("sync failed!"); + } + }); + + debug!("Creating the room…"); + + // The room needs to be encrypted. + let initial_state = + vec![InitialStateEvent::new(RoomEncryptionEventContent::with_recommended_defaults()) + .to_raw_any()]; + + let alice_room = alice + .create_room(assign!(CreateRoomRequest::new(), { + is_direct: true, + initial_state, + preset: Some(RoomPreset::PrivateChat) + })) + .await + .unwrap(); + + assert!(alice_room + .is_encrypted() + .await + .expect("We should be able to check that the room is encrypted")); + + // Now here comes bob. + let bob = TestClientBuilder::new("bob").use_sqlite().build().await.unwrap(); + bob.encryption().wait_for_e2ee_initialization_tasks().await; + + debug!("Inviting Bob."); + + alice_room + .invite_user_by_id(bob.user_id().expect("We should have access to bob's user ID")) + .await + .expect("We should be able to invite Bob to the room"); + + // Sync once to get access to the room. + let sync_service = SyncService::builder(bob.clone()).build().await.expect("Wat"); + sync_service.start().await; + + let bob_rooms = sync_service + .room_list_service() + .all_rooms() + .await + .expect("We should be able to get the room"); + + debug!("Waiting for the room list to load"); + let wait_for_room_list_load = async { + while let Some(state) = bob_rooms.loading_state().next().await { + if let RoomListLoadingState::Loaded { .. } = state { + break; + } + } + }; + + timeout(Duration::from_secs(5), wait_for_room_list_load) + .await + .expect("We should be able to load the room list"); + + // Bob joins the room. + let bob_room = + bob.get_room(alice_room.room_id()).expect("We should have access to the room now"); + bob_room.join().await.expect("Bob should be able to join the room"); + + debug!("Bob joined the room"); + assert_eq!(bob_room.state(), RoomState::Joined); + assert!(bob_room.is_encrypted().await.unwrap()); + + // Let's stop the sync so we don't receive the room key using the usual channel. + sync_service.stop().await.expect("We should be able to stop the sync service"); + + debug!("Alice sends the message"); + let event_id = alice_room + .send(RoomMessageEventContent::text_plain("It's a secret to everybody!")) + .await + .expect("We should be able to send a message to our new room") + .event_id; + + // We don't need Alice anymore. + alice_sync.abort(); + + // Let's get the timeline and backpaginate to load the event. + let mut timeline = + bob_room.timeline().await.expect("We should be able to get a timeline for our room"); + + let mut item = None; + + for _ in 0..10 { + timeline + .paginate_backwards(50) + .await + .expect("We should be able to paginate the timeline to fetch the history"); + + if let Some(timeline_item) = timeline.item_by_event_id(&event_id).await { + item = Some(timeline_item); + break; + } else { + timeline = bob_room.timeline().await.expect("We should be able to reset our timeline"); + sleep(Duration::from_millis(100)).await + } + } + + let item = item.expect("The event should be in the timeline by now"); + + // The event is not decrypted yet. + assert_matches!(item.content(), TimelineItemContent::UnableToDecrypt(_)); + + // Let's subscribe to our timeline so we don't miss the transition from UTD to + // decrypted event. + let (_, mut stream) = timeline + .subscribe_filter_map(|item| { + item.as_event().cloned().filter(|item| item.event_id() == Some(&event_id)) + }) + .await; + + // Now we create a notification client. + let notification_client = bob + .notification_client("BOB_NOTIFICATION_CLIENT".to_owned()) + .await + .expect("We should be able to build a notification client"); + + // This syncs and fetches the room key. + debug!("The notification client syncs"); + let notification_client = NotificationClient::new( + notification_client, + matrix_sdk_ui::notification_client::NotificationProcessSetup::SingleProcess { + sync_service: sync_service.into(), + }, + ) + .await + .expect("We should be able to build a notification client"); + + let _ = notification_client + .get_notification(bob_room.room_id(), &event_id) + .await + .expect("We should be able toe get a notification item for the given event"); + + // Alright, we should now receive an update that the event had been decrypted. + let _vector_diff = timeout(Duration::from_secs(5), stream.next()).await.unwrap().unwrap(); + + // Let's fetch the event again. + let item = + timeline.item_by_event_id(&event_id).await.expect("The event should be in the timeline"); + + // Yup it's decrypted great. + assert_let!( + TimelineItemContent::Message(message) = item.content(), + "The event should have been decrypted now" + ); + + assert_eq!(message.body(), "It's a secret to everybody!"); +} From 5d8ad3a4a920c5f0a85674f3c7dbac4154c5b16b Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 17 Dec 2024 17:22:57 +0100 Subject: [PATCH 804/979] fix(linked chunk): in LinkedChunk::ritems_from, skip as long as we're on the right chunk The previous code would skip based on the position's index, but not the position's chunk. It could be that the position's chunk is different from the first items chunk, as shown in the example, where the linked chunk ends with a gap; in this case, the position's index would be 0, while the first chunk found while iterating backwards had 3 items. As a result, items 'd' and 'e' would be skipped incorrectly. The fix is to take into account the chunk id when skipping over items. --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 6a67383b014..f42409d68f7 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -816,8 +816,9 @@ impl LinkedChunk { .skip_while({ let expected_index = position.index(); - move |(Position(_chunk_identifier, item_index), _item)| { - *item_index != expected_index + move |(Position(chunk_identifier, item_index), _item)| { + *chunk_identifier == position.chunk_identifier() + && *item_index != expected_index } })) } @@ -1813,6 +1814,26 @@ mod tests { assert_matches!(iterator.next(), None); } + #[test] + fn test_ritems_with_final_gap() -> Result<(), Error> { + let mut linked_chunk = LinkedChunk::<3, char, ()>::new(); + linked_chunk.push_items_back(['a', 'b']); + linked_chunk.push_gap_back(()); + linked_chunk.push_items_back(['c', 'd', 'e']); + linked_chunk.push_gap_back(()); + + let mut iterator = linked_chunk.ritems(); + + assert_matches!(iterator.next(), Some((Position(ChunkIdentifier(2), 2), 'e'))); + assert_matches!(iterator.next(), Some((Position(ChunkIdentifier(2), 1), 'd'))); + assert_matches!(iterator.next(), Some((Position(ChunkIdentifier(2), 0), 'c'))); + assert_matches!(iterator.next(), Some((Position(ChunkIdentifier(0), 1), 'b'))); + assert_matches!(iterator.next(), Some((Position(ChunkIdentifier(0), 0), 'a'))); + assert_matches!(iterator.next(), None); + + Ok(()) + } + #[test] fn test_ritems_empty() { let linked_chunk = LinkedChunk::<2, char, ()>::new(); From 373709fb38c28aa182bd6d94c588bf9c89ad7e1d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Dec 2024 18:36:50 +0100 Subject: [PATCH 805/979] feat(event cache): don't replace a gap chunk by an empty items chunks --- .../matrix-sdk-common/src/linked_chunk/mod.rs | 139 +++++++++++++++--- .../matrix-sdk/src/event_cache/pagination.rs | 9 +- .../matrix-sdk/src/event_cache/room/events.rs | 60 ++++++-- .../tests/integration/event_cache.rs | 73 ++++++++- 4 files changed, 246 insertions(+), 35 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index f42409d68f7..3294f58845f 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -634,6 +634,47 @@ impl LinkedChunk { Ok(()) } + /// Remove a gap with the given identifier. + /// + /// This returns the next insert position, viz. the start of the next + /// chunk, if any, or none if there was no next chunk. + pub fn remove_gap_at( + &mut self, + chunk_identifier: ChunkIdentifier, + ) -> Result, Error> { + let chunk = self + .links + .chunk_mut(chunk_identifier) + .ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?; + + if chunk.is_items() { + return Err(Error::ChunkIsItems { identifier: chunk_identifier }); + }; + + let next = chunk.next; + + chunk.unlink(&mut self.updates); + + let chunk_ptr = chunk.as_ptr(); + + // If this ever changes, we may need to update self.links.first too. + debug_assert!(chunk.is_first_chunk().not(), "A gap cannot be the first chunk"); + + if chunk.is_last_chunk() { + self.links.last = chunk.previous; + } + + // SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't + // use it anymore, it's a leak. It is time to re-`Box` it and drop it. + let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) }; + + // Return the first position of the next chunk, if any. + Ok(next.map(|next| { + let chunk = unsafe { next.as_ref() }; + chunk.first_position() + })) + } + /// Replace the gap identified by `chunk_identifier`, by items. /// /// Because the `chunk_identifier` can represent non-gap chunk, this method @@ -661,27 +702,25 @@ impl LinkedChunk { .chunk_mut(chunk_identifier) .ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?; - debug_assert!(chunk.is_first_chunk().not(), "A gap cannot be the first chunk"); + if chunk.is_items() { + return Err(Error::ChunkIsItems { identifier: chunk_identifier }); + }; - let maybe_last_chunk_ptr = match &mut chunk.content { - ChunkContent::Gap(..) => { - let items = items.into_iter(); + debug_assert!(chunk.is_first_chunk().not(), "A gap cannot be the first chunk"); - let last_inserted_chunk = chunk - // Insert a new items chunk… - .insert_next( - Chunk::new_items_leaked(self.chunk_identifier_generator.next()), - &mut self.updates, - ) - // … and insert the items. - .push_items(items, &self.chunk_identifier_generator, &mut self.updates); + let maybe_last_chunk_ptr = { + let items = items.into_iter(); - last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr()) - } + let last_inserted_chunk = chunk + // Insert a new items chunk… + .insert_next( + Chunk::new_items_leaked(self.chunk_identifier_generator.next()), + &mut self.updates, + ) + // … and insert the items. + .push_items(items, &self.chunk_identifier_generator, &mut self.updates); - ChunkContent::Items(..) => { - return Err(Error::ChunkIsItems { identifier: chunk_identifier }) - } + last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr()) }; new_chunk_ptr = chunk @@ -2691,6 +2730,72 @@ mod tests { Ok(()) } + #[test] + fn test_remove_gap() -> Result<(), Error> { + use super::Update::*; + + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + + // Ignore initial update. + let _ = linked_chunk.updates().unwrap().take(); + + linked_chunk.push_items_back(['a', 'b']); + linked_chunk.push_gap_back(()); + linked_chunk.push_items_back(['l', 'm']); + linked_chunk.push_gap_back(()); + assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['l', 'm'] [-]); + assert_eq!( + linked_chunk.updates().unwrap().take(), + &[ + PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a', 'b'] }, + NewGapChunk { + previous: Some(ChunkIdentifier(0)), + new: ChunkIdentifier(1), + next: None, + gap: (), + }, + NewItemsChunk { + previous: Some(ChunkIdentifier(1)), + new: ChunkIdentifier(2), + next: None, + }, + PushItems { at: Position(ChunkIdentifier(2), 0), items: vec!['l', 'm'] }, + NewGapChunk { + previous: Some(ChunkIdentifier(2)), + new: ChunkIdentifier(3), + next: None, + gap: (), + }, + ] + ); + + // Try to remove a gap that's not a gap. + let err = linked_chunk.remove_gap_at(ChunkIdentifier(0)).unwrap_err(); + assert_matches!(err, Error::ChunkIsItems { .. }); + + // Try to remove an unknown gap chunk. + let err = linked_chunk.remove_gap_at(ChunkIdentifier(42)).unwrap_err(); + assert_matches!(err, Error::InvalidChunkIdentifier { .. }); + + // Remove the gap in the middle. + let maybe_next = linked_chunk.remove_gap_at(ChunkIdentifier(1)).unwrap(); + let next = maybe_next.unwrap(); + // The next insert position at the start of the next chunk. + assert_eq!(next.chunk_identifier(), ChunkIdentifier(2)); + assert_eq!(next.index(), 0); + assert_items_eq!(linked_chunk, ['a', 'b'] ['l', 'm'] [-]); + assert_eq!(linked_chunk.updates().unwrap().take(), &[RemoveChunk(ChunkIdentifier(1))]); + + // Remove the gap at the end. + let next = linked_chunk.remove_gap_at(ChunkIdentifier(3)).unwrap(); + // It was the last chunk, so there's no next insert position. + assert!(next.is_none()); + assert_items_eq!(linked_chunk, ['a', 'b'] ['l', 'm']); + assert_eq!(linked_chunk.updates().unwrap().take(), &[RemoveChunk(ChunkIdentifier(3))]); + + Ok(()) + } + #[test] fn test_chunk_item_positions() { let mut linked_chunk = LinkedChunk::<3, char, ()>::new(); diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 064acd5ae00..832398ec57c 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -178,12 +178,9 @@ impl RoomPagination { let insert_new_gap_pos = if let Some(gap_id) = prev_gap_id { // There is a prior gap, let's replace it by new events! trace!("replaced gap with new events from backpagination"); - Some( - room_events - .replace_gap_at(sync_events, gap_id) - .expect("gap_identifier is a valid chunk id we read previously") - .first_position(), - ) + room_events + .replace_gap_at(sync_events, gap_id) + .expect("gap_identifier is a valid chunk id we read previously") } else if let Some(pos) = first_event_pos { // No prior gap, but we had some events: assume we need to prepend events // before those. diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 83fc2218972..67a65635153 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -145,13 +145,13 @@ impl RoomEvents { /// Because the `gap_identifier` can represent non-gap chunk, this method /// returns a `Result`. /// - /// This method returns a reference to the (first if many) newly created - /// `Chunk` that contains the `items`. + /// This method returns either the position of the first chunk that's been + /// created, or the next insert position if the chunk has been removed. pub fn replace_gap_at( &mut self, events: I, gap_identifier: ChunkIdentifier, - ) -> Result<&Chunk, Error> + ) -> Result, Error> where I: IntoIterator, { @@ -165,8 +165,14 @@ impl RoomEvents { // because of the removals. self.remove_events(duplicated_event_ids); - // Replace the gap by new events. - self.chunks.replace_gap_at(unique_events, gap_identifier) + if unique_events.is_empty() { + // There are no new events, so there's no need to create a new empty items + // chunk; instead, remove the gap. + self.chunks.remove_gap_at(gap_identifier) + } else { + // Replace the gap by new events. + Ok(Some(self.chunks.replace_gap_at(unique_events, gap_identifier)?.first_position())) + } } /// Search for a chunk, and return its identifier. @@ -711,9 +717,8 @@ mod tests { let chunk_identifier_of_gap = room_events .chunks() - .find_map(|chunk| chunk.is_gap().then_some(chunk.first_position())) - .unwrap() - .chunk_identifier(); + .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier())) + .unwrap(); room_events.replace_gap_at([event_1, event_2], chunk_identifier_of_gap).unwrap(); @@ -752,9 +757,8 @@ mod tests { let chunk_identifier_of_gap = room_events .chunks() - .find_map(|chunk| chunk.is_gap().then_some(chunk.first_position())) - .unwrap() - .chunk_identifier(); + .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier())) + .unwrap(); assert_events_eq!( room_events.events(), @@ -790,6 +794,40 @@ mod tests { } } + #[test] + fn test_replace_gap_at_with_no_new_events() { + let (_, event_0) = new_event("$ev0"); + let (_, event_1) = new_event("$ev1"); + let (_, event_2) = new_event("$ev2"); + + let mut room_events = RoomEvents::new(); + + room_events.push_events([event_0, event_1]); + room_events.push_gap(Gap { prev_token: "middle".to_owned() }); + room_events.push_events([event_2]); + room_events.push_gap(Gap { prev_token: "end".to_owned() }); + + // Remove the first gap. + let first_gap_id = room_events + .chunks() + .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier())) + .unwrap(); + + // The next insert position is the next chunk's start. + let pos = room_events.replace_gap_at([], first_gap_id).unwrap(); + assert_eq!(pos, Some(Position::new(ChunkIdentifier::new(2), 0))); + + // Remove the second gap. + let second_gap_id = room_events + .chunks() + .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier())) + .unwrap(); + + // No next insert position. + let pos = room_events.replace_gap_at([], second_gap_id).unwrap(); + assert!(pos.is_none()); + } + #[test] fn test_remove_events() { let (event_id_0, event_0) = new_event("$ev0"); diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 0832fea4b5d..20541241840 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -14,7 +14,7 @@ use matrix_sdk::{ use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, }; -use ruma::{event_id, room_id, user_id}; +use ruma::{event_id, events::AnyTimelineEvent, room_id, serde::Raw, user_id}; use serde_json::json; use tokio::{spawn, sync::broadcast}; use wiremock::ResponseTemplate; @@ -916,3 +916,74 @@ async fn test_backpaginate_with_no_initial_events() { assert_event_matches_msg(&events[2], "world"); assert_eq!(events.len(), 3); } + +#[async_test] +async fn test_backpaginate_replace_empty_gap() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + + let room_id = room_id!("!omelette:fromage.fr"); + + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + + // Start with a room with an event, limited timeline and prev-batch token. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("world").event_id(event_id!("$2"))) + .set_timeline_limited() + .set_timeline_prev_batch("prev-batch".to_owned()), + ) + .await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); + wait_for_initial_events(events, &mut stream).await; + + // The first back-pagination will return a previous-batch token, but no events. + server + .mock_room_messages() + .ok( + "start-token-unused1".to_owned(), + Some("prev_batch".to_owned()), + Vec::>::new(), + Vec::new(), + ) + .mock_once() + .mount() + .await; + + // The second round of back-pagination will return this one. + server + .mock_room_messages() + .from("prev_batch") + .ok( + "start-token-unused2".to_owned(), + None, + vec![f.text_msg("hello").event_id(event_id!("$1"))], + Vec::new(), + ) + .mock_once() + .mount() + .await; + + let pagination = room_event_cache.pagination(); + + // Run pagination twice. + pagination.run_backwards(20, once).await.unwrap(); + pagination.run_backwards(20, once).await.unwrap(); + + // The linked chunk should contain the events in the correct order. + let (events, _stream) = room_event_cache.subscribe().await.unwrap(); + + assert_event_matches_msg(&events[0], "hello"); + assert_event_matches_msg(&events[1], "world"); + assert_eq!(events.len(), 2); +} From 5f3b56a987b7799ca42fcf4dedb2b80b8a98660e Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 17 Dec 2024 15:49:55 +0000 Subject: [PATCH 806/979] task(crypto): Accept old PreviouslyVerified value of SenderData when deserializing --- .../src/olm/group_sessions/sender_data.rs | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index df54390e580..0be07403e00 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -215,6 +215,7 @@ enum SenderDataReader { legacy_session: bool, }, + #[serde(alias = "SenderUnverifiedButPreviouslyVerified")] VerificationViolation(KnownSenderData), SenderUnverified(KnownSenderData), @@ -286,7 +287,10 @@ mod tests { use vodozemac::Ed25519PublicKey; use super::SenderData; - use crate::types::{DeviceKeys, Signatures}; + use crate::{ + olm::KnownSenderData, + types::{DeviceKeys, Signatures}, + }; #[test] fn serializing_unknown_device_correctly_preserves_owner_check_failed_if_true() { @@ -360,6 +364,47 @@ mod tests { assert_let!(SenderData::SenderVerified { .. } = end); } + #[test] + fn deserializing_sender_unverified_but_previously_verified_migrates_to_verification_violation() + { + let json = r#" + { + "SenderUnverifiedButPreviouslyVerified":{ + "user_id":"@u:s.co", + "master_key":[ + 150,140,249,139,141,29,63,230,179,14,213,175,176,61,11,255, + 26,103,10,51,100,154,183,47,181,117,87,204,33,215,241,92 + ], + "master_key_verified":true + } + } + "#; + + let end: SenderData = serde_json::from_str(json).expect("Failed to parse!"); + assert_let!(SenderData::VerificationViolation(KnownSenderData { user_id, .. }) = end); + assert_eq!(user_id, owned_user_id!("@u:s.co")); + } + + #[test] + fn deserializing_verification_violation() { + let json = r#" + { + "VerificationViolation":{ + "user_id":"@u:s.co", + "master_key":[ + 150,140,249,139,141,29,63,230,179,14,213,175,176,61,11,255, + 26,103,10,51,100,154,183,47,181,117,87,204,33,215,241,92 + ], + "master_key_verified":true + } + } + "#; + + let end: SenderData = serde_json::from_str(json).expect("Failed to parse!"); + assert_let!(SenderData::VerificationViolation(KnownSenderData { user_id, .. }) = end); + assert_eq!(user_id, owned_user_id!("@u:s.co")); + } + #[test] fn equal_sessions_have_same_trust_level() { let unknown = SenderData::unknown(); From db39c6bea6ee3007fae469d5aa5e66389e6127b9 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 18 Dec 2024 09:42:49 +0000 Subject: [PATCH 807/979] task(crypto): Accept old PreviouslyVerified value of VerificationLevel when deserializing --- .../src/deserialized_responses.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index c63a0e42968..3ec6401a16d 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -176,6 +176,7 @@ pub enum VerificationLevel { /// The message was sent by a user identity we have not verified, but the /// user was previously verified. + #[serde(alias = "PreviouslyVerified")] VerificationViolation, /// The message was sent by a device not linked to (signed by) any user @@ -883,6 +884,40 @@ mod tests { ); } + #[test] + fn test_verification_level_deserializes() { + // Given a JSON VerificationLevel + #[derive(Deserialize)] + struct Container { + verification_level: VerificationLevel, + } + let container = json!({ "verification_level": "VerificationViolation" }); + + // When we deserialize it + let deserialized: Container = serde_json::from_value(container) + .expect("We can deserialize the old PreviouslyVerified value"); + + // Then it is populated correctly + assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation); + } + + #[test] + fn test_verification_level_deserializes_from_old_previously_verified_value() { + // Given a JSON VerificationLevel with the old value PreviouslyVerified + #[derive(Deserialize)] + struct Container { + verification_level: VerificationLevel, + } + let container = json!({ "verification_level": "PreviouslyVerified" }); + + // When we deserialize it + let deserialized: Container = serde_json::from_value(container) + .expect("We can deserialize the old PreviouslyVerified value"); + + // Then it is migrated to the new value + assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation); + } + #[test] fn sync_timeline_event_serialisation() { let room_event = SyncTimelineEvent { From 612ba6fa29280dfd2a5b70f14699af9728eeb022 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 18 Dec 2024 09:49:17 +0000 Subject: [PATCH 808/979] task(crypto): Accept old PreviouslyVerified value of ShieldStateCode when deserializing --- .../src/deserialized_responses.rs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 3ec6401a16d..ea41fd2f3b2 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -260,6 +260,7 @@ pub enum ShieldStateCode { /// An unencrypted event in an encrypted room. SentInClear, /// The sender was previously verified but changed their identity. + #[serde(alias = "PreviouslyVerified")] VerificationViolation, } @@ -809,7 +810,7 @@ mod tests { TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationState, }; - use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; + use crate::deserialized_responses::{DeviceLinkProblem, ShieldStateCode, VerificationLevel}; fn example_event() -> serde_json::Value { json!({ @@ -918,6 +919,40 @@ mod tests { assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation); } + #[test] + fn test_shield_state_code_deserializes() { + // Given a JSON ShieldStateCode with value VerificationViolation + #[derive(Deserialize)] + struct Container { + shield_state_code: ShieldStateCode, + } + let container = json!({ "shield_state_code": "VerificationViolation" }); + + // When we deserialize it + let deserialized: Container = serde_json::from_value(container) + .expect("We can deserialize the old PreviouslyVerified value"); + + // Then it is populated correctly + assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation); + } + + #[test] + fn test_shield_state_code_deserializes_from_old_previously_verified_value() { + // Given a JSON ShieldStateCode with the old value PreviouslyVerified + #[derive(Deserialize)] + struct Container { + shield_state_code: ShieldStateCode, + } + let container = json!({ "shield_state_code": "PreviouslyVerified" }); + + // When we deserialize it + let deserialized: Container = serde_json::from_value(container) + .expect("We can deserialize the old PreviouslyVerified value"); + + // Then it is migrated to the new value + assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation); + } + #[test] fn sync_timeline_event_serialisation() { let room_event = SyncTimelineEvent { From b18e7d71edc13901598281aca04477874f1254e7 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 18 Dec 2024 10:16:38 +0000 Subject: [PATCH 809/979] fix(crypto): Fix error when reading VerifiedStateOrBool with old PreviouslyVerifiedButNoLonger value --- .../matrix-sdk-crypto/src/identities/user.rs | 77 ++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index bbfdf54c4ee..be1f54e4011 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -911,6 +911,7 @@ enum OwnUserIdentityVerifiedState { NeverVerified, /// We previously verified this identity, but it has changed. + #[serde(alias = "PreviouslyVerifiedButNoLonger")] VerificationViolation, /// We have verified the current identity. @@ -1530,26 +1531,10 @@ pub(crate) mod tests { /// that we can deserialize boolean values. #[test] fn test_deserialize_own_user_identity_bool_verified() { - let mut json = json!({ - "user_id": "@example:localhost", - "master_key": { - "user_id":"@example:localhost", - "usage":["master"], - "keys":{"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0":"rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"}, - }, - "self_signing_key": { - "user_id":"@example:localhost", - "usage":["self_signing"], - "keys":{"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210":"0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"} - }, - "user_signing_key": { - "user_id":"@example:localhost", - "usage":["user_signing"], - "keys":{"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo":"DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"} - }, - "verified": false - }); + let mut json = own_user_identity_data(); + // Set `"verified": false` + *json.get_mut("verified").unwrap() = false.into(); let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap(); assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::NeverVerified); @@ -1559,6 +1544,38 @@ pub(crate) mod tests { assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::Verified); } + #[test] + fn test_own_user_identity_verified_state_verification_violation_deserializes() { + // Given data containing verified: VerificationViolation + let mut json = own_user_identity_data(); + *json.get_mut("verified").unwrap() = "VerificationViolation".into(); + + // When we deserialize + let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap(); + + // Then the value is correctly populated + assert_eq!( + *id.verified.read().unwrap(), + OwnUserIdentityVerifiedState::VerificationViolation + ); + } + + #[test] + fn test_own_user_identity_verified_state_previously_verified_deserializes() { + // Given data containing verified: PreviouslyVerifiedButNoLonger + let mut json = own_user_identity_data(); + *json.get_mut("verified").unwrap() = "PreviouslyVerifiedButNoLonger".into(); + + // When we deserialize + let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap(); + + // Then the old value is re-interpreted as VerificationViolation + assert_eq!( + *id.verified.read().unwrap(), + OwnUserIdentityVerifiedState::VerificationViolation + ); + } + #[test] fn own_identity_check_signatures() { let response = own_key_query(); @@ -1895,4 +1912,26 @@ pub(crate) mod tests { assert!(!own_identity.was_previously_verified()); assert!(!own_identity.has_verification_violation()); } + + fn own_user_identity_data() -> Value { + json!({ + "user_id": "@example:localhost", + "master_key": { + "user_id":"@example:localhost", + "usage":["master"], + "keys":{"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0":"rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"}, + }, + "self_signing_key": { + "user_id":"@example:localhost", + "usage":["self_signing"], + "keys":{"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210":"0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"} + }, + "user_signing_key": { + "user_id":"@example:localhost", + "usage":["user_signing"], + "keys":{"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo":"DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"} + }, + "verified": false + }) + } } From bb70229dd884c1315f5e20f4b9e9685be7d30af7 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 18 Dec 2024 13:09:33 +0100 Subject: [PATCH 810/979] chore: Make Clippy happy. --- crates/matrix-sdk-crypto/src/verification/requests.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 4c25a969807..50960185a8f 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -658,7 +658,7 @@ impl VerificationRequest { let recip_devices: Vec = self .recipient_devices .iter() - .filter(|&d| filter_device.map_or(true, |device| **d != *device)) + .filter(|&d| filter_device.is_none_or(|device| **d != *device)) .cloned() .collect(); diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 30a3eed4de5..6ee2995822d 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1813,7 +1813,7 @@ impl Room { // A member has no unknown devices iff it was tracked *and* the tracking is // not considered dirty. let members_with_unknown_devices = - members.iter().filter(|member| tracked.get(*member).map_or(true, |dirty| *dirty)); + members.iter().filter(|member| tracked.get(*member).is_none_or(|dirty| *dirty)); let (req_id, request) = olm.query_keys_for_users(members_with_unknown_devices.map(|owned| owned.borrow())); From ff7077b742d845dfd35f0730e7c2c75adf06b961 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 18 Dec 2024 13:27:07 +0000 Subject: [PATCH 811/979] doc(crypto): Add changelog entry for #4424 --- crates/matrix-sdk-crypto/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index af8c51f7e33..7177a251fb8 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -93,6 +93,12 @@ All notable changes to this project will be documented in this file. ### Refactor +- Fix [#4424](https://github.com/matrix-org/matrix-rust-sdk/issues/4424) Failed + storage upgrade for "PreviouslyVerifiedButNoLonger". This bug caused errors to + occur when loading crypto information from storage, which typically prevented + apps from starting correctly. + ([#4430](https://github.com/matrix-org/matrix-rust-sdk/pull/4430)) + - Add new method `OlmMachine::try_decrypt_room_event`. ([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116)) From bb573117e102261b46da3f2d6761c4b3f51850a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 18 Dec 2024 12:52:52 +0100 Subject: [PATCH 812/979] chore: Release matrix-sdk version 0.9.0 --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 18 +++++++++--------- crates/matrix-sdk-base/CHANGELOG.md | 2 ++ crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-common/CHANGELOG.md | 2 ++ crates/matrix-sdk-common/Cargo.toml | 2 +- crates/matrix-sdk-crypto/CHANGELOG.md | 2 ++ crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-indexeddb/CHANGELOG.md | 4 ++++ crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- crates/matrix-sdk-qrcode/CHANGELOG.md | 4 ++++ crates/matrix-sdk-qrcode/Cargo.toml | 2 +- crates/matrix-sdk-sqlite/CHANGELOG.md | 2 ++ crates/matrix-sdk-sqlite/Cargo.toml | 2 +- .../matrix-sdk-store-encryption/CHANGELOG.md | 4 ++++ crates/matrix-sdk-store-encryption/Cargo.toml | 2 +- crates/matrix-sdk-ui/CHANGELOG.md | 2 ++ crates/matrix-sdk-ui/Cargo.toml | 2 +- crates/matrix-sdk/CHANGELOG.md | 2 ++ crates/matrix-sdk/Cargo.toml | 2 +- 20 files changed, 51 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 36ee61dd332..f4f968755e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3048,7 +3048,7 @@ dependencies = [ [[package]] name = "matrix-sdk" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anyhow", "anymap2", @@ -3124,7 +3124,7 @@ dependencies = [ [[package]] name = "matrix-sdk-base" -version = "0.8.0" +version = "0.9.0" dependencies = [ "as_variant", "assert_matches", @@ -3160,7 +3160,7 @@ dependencies = [ [[package]] name = "matrix-sdk-common" -version = "0.8.0" +version = "0.9.0" dependencies = [ "assert_matches", "async-trait", @@ -3189,7 +3189,7 @@ dependencies = [ [[package]] name = "matrix-sdk-crypto" -version = "0.8.0" +version = "0.9.0" dependencies = [ "aes", "anyhow", @@ -3313,7 +3313,7 @@ dependencies = [ [[package]] name = "matrix-sdk-indexeddb" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anyhow", "assert_matches", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "matrix-sdk-qrcode" -version = "0.8.0" +version = "0.9.0" dependencies = [ "byteorder", "image", @@ -3393,7 +3393,7 @@ dependencies = [ [[package]] name = "matrix-sdk-sqlite" -version = "0.8.0" +version = "0.9.0" dependencies = [ "assert_matches", "async-trait", @@ -3421,7 +3421,7 @@ dependencies = [ [[package]] name = "matrix-sdk-store-encryption" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -3469,7 +3469,7 @@ dependencies = [ [[package]] name = "matrix-sdk-ui" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anyhow", "as_variant", diff --git a/Cargo.toml b/Cargo.toml index d9f068ae18b..5882bd8bd74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,17 +97,17 @@ web-sys = "0.3.69" wiremock = "0.6.2" zeroize = "1.8.1" -matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false } -matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" } -matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" } -matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.0" } +matrix-sdk = { path = "crates/matrix-sdk", version = "0.9.0", default-features = false } +matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.9.0" } +matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.9.0" } +matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.9.0" } matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" } -matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.8.0", default-features = false } -matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" } -matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false } -matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" } +matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.9.0", default-features = false } +matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.9.0" } +matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.9.0", default-features = false } +matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.9.0" } matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" } -matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false } +matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.9.0", default-features = false } # Default release profile, select with `--release` [profile.release] diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 862d8a3fa0b..988064b396a 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + ### Features - Introduced support for diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 58a9c50e64c..825c524062a 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk-base" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.8.0" +version = "0.9.0" [package.metadata.docs.rs] all-features = true diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md index 683eed16852..7eedc0ab0b7 100644 --- a/crates/matrix-sdk-common/CHANGELOG.md +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + ### Bug Fixes - Change the behavior of `LinkedChunk::new_with_update_history()` to emit an diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 78837937e11..2aef55b7efd 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk-common" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.8.0" +version = "0.9.0" [package.metadata.docs.rs] default-target = "x86_64-unknown-linux-gnu" diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 7177a251fb8..5ddf3a801ad 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + - Expose new API `DehydratedDevices::get_dehydrated_device_pickle_key`, `DehydratedDevices::save_dehydrated_device_pickle_key` and `DehydratedDevices::delete_dehydrated_device_pickle_key` to store/load the dehydrated device pickle key. This allows client to automatically rotate the dehydrated device to avoid one-time-keys exhaustion and to_device accumulation. diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 060692780fe..b3e3ccd1155 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk-crypto" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.8.0" +version = "0.9.0" [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/matrix-sdk-indexeddb/CHANGELOG.md b/crates/matrix-sdk-indexeddb/CHANGELOG.md index 4372794f5a1..fa58f5dda75 100644 --- a/crates/matrix-sdk-indexeddb/CHANGELOG.md +++ b/crates/matrix-sdk-indexeddb/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + +No notable changes in this release. + ## [0.8.0] - 2024-11-19 ### Features diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 46de1f42acf..e06e315b060 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "matrix-sdk-indexeddb" -version = "0.8.0" +version = "0.9.0" repository = "https://github.com/matrix-org/matrix-rust-sdk" description = "Web's IndexedDB Storage backend for matrix-sdk" license = "Apache-2.0" diff --git a/crates/matrix-sdk-qrcode/CHANGELOG.md b/crates/matrix-sdk-qrcode/CHANGELOG.md index 9235a4b9ff2..d3fae753d53 100644 --- a/crates/matrix-sdk-qrcode/CHANGELOG.md +++ b/crates/matrix-sdk-qrcode/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + +No notable changes in this release. + ## [0.8.0] - 2024-11-19 No notable changes in this release. diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index 42cf72489ca..ecc95ac78cf 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "matrix-sdk-qrcode" description = "Library to encode and decode QR codes for interactive verifications in Matrix land" -version = "0.8.0" +version = "0.9.0" authors = ["Damir Jelić "] edition = "2021" homepage = "https://github.com/matrix-org/matrix-rust-sdk" diff --git a/crates/matrix-sdk-sqlite/CHANGELOG.md b/crates/matrix-sdk-sqlite/CHANGELOG.md index dcf6ac6d8ad..b37a3a9a0cc 100644 --- a/crates/matrix-sdk-sqlite/CHANGELOG.md +++ b/crates/matrix-sdk-sqlite/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + ### Features - Add support for persisting LinkedChunks in the SQLite store. This is a step diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index c94b0f764f6..4596f4fc222 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "matrix-sdk-sqlite" -version = "0.8.0" +version = "0.9.0" edition = "2021" repository = "https://github.com/matrix-org/matrix-rust-sdk" description = "Sqlite storage backend for matrix-sdk" diff --git a/crates/matrix-sdk-store-encryption/CHANGELOG.md b/crates/matrix-sdk-store-encryption/CHANGELOG.md index 9235a4b9ff2..d3fae753d53 100644 --- a/crates/matrix-sdk-store-encryption/CHANGELOG.md +++ b/crates/matrix-sdk-store-encryption/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + +No notable changes in this release. + ## [0.8.0] - 2024-11-19 No notable changes in this release. diff --git a/crates/matrix-sdk-store-encryption/Cargo.toml b/crates/matrix-sdk-store-encryption/Cargo.toml index eda60adb6a3..e76c1f5a4f1 100644 --- a/crates/matrix-sdk-store-encryption/Cargo.toml +++ b/crates/matrix-sdk-store-encryption/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "matrix-sdk-store-encryption" -version = "0.8.0" +version = "0.9.0" edition = "2021" description = "Helpers for encrypted storage keys for the Matrix SDK" repository = "https://github.com/matrix-org/matrix-rust-sdk" diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 0b0d578ba9a..908df65b4e1 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + ### Bug Fixes - Add the `m.room.create` and the `m.room.history_visibility` state events to diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index 106220a3420..2ba3efa8db5 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "matrix-sdk-ui" description = "GUI-centric utilities on top of matrix-rust-sdk (experimental)." -version = "0.8.0" +version = "0.9.0" edition = "2021" repository = "https://github.com/matrix-org/matrix-rust-sdk" license = "Apache-2.0" diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 87862d8672b..2906a2568b6 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +## [0.9.0] - 2024-12-18 + ### Bug Fixes - Use the inviter's server name and the server name from the room alias as diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 7a432a49ed1..450f008491a 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -9,7 +9,7 @@ name = "matrix-sdk" readme = "README.md" repository = "https://github.com/matrix-org/matrix-rust-sdk" rust-version = { workspace = true } -version = "0.8.0" +version = "0.9.0" [package.metadata.docs.rs] features = ["docsrs"] From bc582ae1017164d6faabf593b7898f6d4320b8f4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 18 Dec 2024 15:26:39 +0100 Subject: [PATCH 813/979] doc(common): Update documentation of `AsVector`. This patch updates the documentation of `AsVector`. --- .../src/linked_chunk/as_vector.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs index b607aca056e..38cb1a2a07a 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs @@ -142,17 +142,19 @@ impl UpdateToVectorDiff { /// [`VectorDiff`] is emitted, /// * [`Update::StartReattachItems`] and [`Update::EndReattachItems`] are /// respectively muting or unmuting the emission of [`VectorDiff`] by - /// [`Update::PushItems`]. + /// [`Update::PushItems`], + /// * [`Update::Clear`] reinitialises the state. /// - /// The only `VectorDiff` that are emitted are [`VectorDiff::Insert`] or - /// [`VectorDiff::Append`] because a [`LinkedChunk`] is append-only. + /// The only `VectorDiff` that are emitted are [`VectorDiff::Insert`], + /// [`VectorDiff::Append`], [`VectorDiff::Remove`] and + /// [`VectorDiff::Clear`]. /// /// `VectorDiff::Append` is an optimisation when numerous /// `VectorDiff::Insert`s have to be emitted at the last position. /// - /// `VectorDiff::Insert` need an index. To compute this index, the algorithm - /// will iterate over all pairs to accumulate each chunk length until it - /// finds the appropriate pair (given by + /// `VectorDiff::Insert` needs an index. To compute this index, the + /// algorithm will iterate over all pairs to accumulate each chunk length + /// until it finds the appropriate pair (given by /// [`Update::PushItems::at`]). This is _the offset_. To this offset, the /// algorithm adds the position's index of the new items (still given by /// [`Update::PushItems::at`]). This is _the index_. This logic works From de568837fb8b7ea0814c6a0e4e3fc19769f70402 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 18 Dec 2024 17:41:12 +0100 Subject: [PATCH 814/979] fix(linked chunk): fix order handling of initial chunks in `UpdateToVectorDiff::new()` The code would use a chunk iterator that moves forward, but call `push_front()` repetitively on each chunk, semantically storing the lengths in *reverse* order. This could result in subsequent panics, when a new chunk was added, because the links would not match what's expected (e.g. the last chunk must have no successor, etc.). --- .../src/linked_chunk/as_vector.rs | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs index 38cb1a2a07a..e9bfe0c6008 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs @@ -99,7 +99,7 @@ impl UpdateToVectorDiff { let mut initial_chunk_lengths = VecDeque::new(); for chunk in chunk_iterator { - initial_chunk_lengths.push_front(( + initial_chunk_lengths.push_back(( chunk.identifier(), match chunk.content() { ChunkContent::Gap(_) => 0, @@ -773,7 +773,7 @@ mod tests { } #[test] - fn updates_are_drained_when_constructing_as_vector() { + fn test_updates_are_drained_when_constructing_as_vector() { let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history(); linked_chunk.push_items_back(['a']); @@ -793,6 +793,40 @@ mod tests { assert_eq!(diffs.len(), 1); } + #[test] + fn test_as_vector_with_initial_content() { + // Fill the linked chunk with some initial items. + let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history(); + linked_chunk.push_items_back(['a', 'b', 'c', 'd']); + + #[rustfmt::skip] + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']); + + // Empty updates first. + let _ = linked_chunk.updates().take(); + + // Start observing future updates. + let mut as_vector = linked_chunk.as_vector().unwrap(); + + assert!(as_vector.take().is_empty()); + + // It's important to cause a change that will create new chunks, like pushing + // enough items. + linked_chunk.push_items_back(['e', 'f', 'g']); + #[rustfmt::skip] + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g']); + + // And the vector diffs can be computed without crashing. + let diffs = as_vector.take(); + assert_eq!(diffs.len(), 2); + assert_matches!(&diffs[0], VectorDiff::Append { values } => { + assert_eq!(*values, ['e', 'f'].into()); + }); + assert_matches!(&diffs[1], VectorDiff::Append { values } => { + assert_eq!(*values, ['g'].into()); + }); + } + #[cfg(not(target_arch = "wasm32"))] mod proptests { use proptest::prelude::*; @@ -824,7 +858,7 @@ mod tests { proptest! { #[test] - fn as_vector_is_correct( + fn test_as_vector_is_correct( operations in prop::collection::vec(as_vector_operation_strategy(), 50..=200) ) { let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history(); From b18100228e0456973d56d4ac6a58caf30b17bd8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 18 Dec 2024 17:50:49 +0100 Subject: [PATCH 815/979] test(room): add test to verify how `Room::observe_events` will behave when several events are received in a short while --- crates/matrix-sdk/src/event_handler/mod.rs | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/crates/matrix-sdk/src/event_handler/mod.rs b/crates/matrix-sdk/src/event_handler/mod.rs index ec1676f3931..e324d70a9c3 100644 --- a/crates/matrix-sdk/src/event_handler/mod.rs +++ b/crates/matrix-sdk/src/event_handler/mod.rs @@ -1167,4 +1167,78 @@ mod tests { Ok(()) } + + #[async_test] + async fn test_observe_several_room_events() -> crate::Result<()> { + let client = logged_in_client(None).await; + + let room_id = room_id!("!r0.matrix.org"); + + let observable_for_room = + client.observe_room_events::(room_id); + + let mut subscriber_for_room = observable_for_room.subscribe(); + + assert_pending!(subscriber_for_room); + + let mut response_builder = SyncResponseBuilder::new(); + let response = response_builder + .add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_state_event(StateTestEvent::Custom(json!({ + "content": { + "name": "Name 0" + }, + "event_id": "$ev0", + "origin_server_ts": 1, + "sender": "@mnt_io:matrix.org", + "state_key": "", + "type": "m.room.name", + "unsigned": { + "age": 1, + } + }))) + .add_state_event(StateTestEvent::Custom(json!({ + "content": { + "name": "Name 1" + }, + "event_id": "$ev1", + "origin_server_ts": 2, + "sender": "@mnt_io:matrix.org", + "state_key": "", + "type": "m.room.name", + "unsigned": { + "age": 1, + } + }))) + .add_state_event(StateTestEvent::Custom(json!({ + "content": { + "name": "Name 2" + }, + "event_id": "$ev2", + "origin_server_ts": 3, + "sender": "@mnt_io:matrix.org", + "state_key": "", + "type": "m.room.name", + "unsigned": { + "age": 1, + } + }))), + ) + .build_sync_response(); + client.process_sync(response).await?; + + let (room_name, (room, _client)) = assert_ready!(subscriber_for_room); + + // Check we only get notified about the latest received event + assert_eq!(room_name.event_id.as_str(), "$ev2"); + assert_eq!(room.name().unwrap(), "Name 2"); + + assert_pending!(subscriber_for_room); + + drop(observable_for_room); + assert_closed!(subscriber_for_room); + + Ok(()) + } } From f18e0b18a1ea757921bcae07c29600a9839fdc97 Mon Sep 17 00:00:00 2001 From: Integral Date: Thu, 19 Dec 2024 20:09:32 +0800 Subject: [PATCH 816/979] Replace PathBuf/Utf8PathBuf with Path/Utf8Path when ownership not needed --- bindings/matrix-sdk-crypto-ffi/build.rs | 9 +++++++-- bindings/matrix-sdk-ffi/build.rs | 9 +++++++-- bindings/matrix-sdk-ffi/src/client_builder.rs | 14 +++++++------- crates/matrix-sdk-sqlite/src/crypto_store.rs | 4 ++-- labs/multiverse/src/main.rs | 10 +++++----- xtask/src/kotlin.rs | 4 ++-- xtask/src/swift.rs | 6 +++--- 7 files changed, 33 insertions(+), 23 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/build.rs b/bindings/matrix-sdk-crypto-ffi/build.rs index 14f2d11e10b..30f3df482a2 100644 --- a/bindings/matrix-sdk-crypto-ffi/build.rs +++ b/bindings/matrix-sdk-crypto-ffi/build.rs @@ -1,4 +1,9 @@ -use std::{env, error::Error, path::PathBuf, process::Command}; +use std::{ + env, + error::Error, + path::{Path, PathBuf}, + process::Command, +}; use vergen::EmitBuilder; @@ -39,7 +44,7 @@ fn setup_x86_64_android_workaround() { } /// Run the clang binary at `clang_path`, and return its major version number -fn get_clang_major_version(clang_path: &PathBuf) -> String { +fn get_clang_major_version(clang_path: &Path) -> String { let clang_output = Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang"); diff --git a/bindings/matrix-sdk-ffi/build.rs b/bindings/matrix-sdk-ffi/build.rs index 2605a8fe514..d58dc7a1690 100644 --- a/bindings/matrix-sdk-ffi/build.rs +++ b/bindings/matrix-sdk-ffi/build.rs @@ -1,4 +1,9 @@ -use std::{env, error::Error, path::PathBuf, process::Command}; +use std::{ + env, + error::Error, + path::{Path, PathBuf}, + process::Command, +}; use vergen::EmitBuilder; @@ -39,7 +44,7 @@ fn setup_x86_64_android_workaround() { } /// Run the clang binary at `clang_path`, and return its major version number -fn get_clang_major_version(clang_path: &PathBuf) -> String { +fn get_clang_major_version(clang_path: &Path) -> String { let clang_output = Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang"); diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index d080233399d..2509758eb9f 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -1,4 +1,4 @@ -use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration}; +use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration}; use futures_util::StreamExt; use matrix_sdk::{ @@ -509,8 +509,8 @@ impl ClientBuilder { } if let Some(session_paths) = &builder.session_paths { - let data_path = PathBuf::from(&session_paths.data_path); - let cache_path = PathBuf::from(&session_paths.cache_path); + let data_path = Path::new(&session_paths.data_path); + let cache_path = Path::new(&session_paths.cache_path); debug!( data_path = %data_path.to_string_lossy(), @@ -518,12 +518,12 @@ impl ClientBuilder { "Creating directories for data and cache stores.", ); - fs::create_dir_all(&data_path)?; - fs::create_dir_all(&cache_path)?; + fs::create_dir_all(data_path)?; + fs::create_dir_all(cache_path)?; inner_builder = inner_builder.sqlite_store_with_cache_path( - &data_path, - &cache_path, + data_path, + cache_path, builder.passphrase.as_deref(), ); } else { diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index aa4e3e9d211..236b6b7fcb4 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -1402,7 +1402,7 @@ impl CryptoStore for SqliteCryptoStore { #[cfg(test)] mod tests { - use std::path::PathBuf; + use std::path::Path; use matrix_sdk_crypto::{ cryptostore_integration_tests, cryptostore_integration_tests_time, store::CryptoStore, @@ -1428,7 +1428,7 @@ mod tests { async fn get_test_db() -> TestDb { let db_name = "matrix-sdk-crypto.sqlite3"; - let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); let database_path = manifest_path.join("testing/data/storage").join(db_name); let tmpdir = tempdir().unwrap(); diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 50f4c3e7ac9..9da33ecf578 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, env, io::{self, stdout, Write}, - path::PathBuf, + path::Path, process::exit, sync::{Arc, Mutex}, time::Duration, @@ -62,7 +62,7 @@ async fn main() -> anyhow::Result<()> { }; let config_path = env::args().nth(2).unwrap_or("/tmp/".to_owned()); - let client = configure_client(server_name, config_path).await?; + let client = configure_client(&server_name, &config_path).await?; let ec = client.event_cache(); ec.subscribe().unwrap(); @@ -978,10 +978,10 @@ impl StatefulList { /// Configure the client so it's ready for sync'ing. /// /// Will log in or reuse a previous session. -async fn configure_client(server_name: String, config_path: String) -> anyhow::Result { - let server_name = ServerName::parse(&server_name)?; +async fn configure_client(server_name: &str, config_path: &str) -> anyhow::Result { + let server_name = ServerName::parse(server_name)?; - let config_path = PathBuf::from(config_path); + let config_path = Path::new(config_path); let mut client_builder = Client::builder() .store_config( StoreConfig::new("multiverse".to_owned()) diff --git a/xtask/src/kotlin.rs b/xtask/src/kotlin.rs index 4e1836211de..6ee768bce17 100644 --- a/xtask/src/kotlin.rs +++ b/xtask/src/kotlin.rs @@ -71,7 +71,7 @@ impl KotlinArgs { package, } => { let profile = profile.as_deref().unwrap_or(if release { "release" } else { "dev" }); - build_android_library(profile, only_target, src_dir, package) + build_android_library(profile, only_target, &src_dir, package) } } } @@ -80,7 +80,7 @@ impl KotlinArgs { fn build_android_library( profile: &str, only_target: Option, - src_dir: Utf8PathBuf, + src_dir: &Utf8Path, package: Package, ) -> Result<()> { let package_values = package.values(); diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs index 31a38287a84..cf41e2600a6 100644 --- a/xtask/src/swift.rs +++ b/xtask/src/swift.rs @@ -318,7 +318,7 @@ fn build_path_for_target(target: &Target, profile: &str) -> Result /// Lipo's together the libraries for each platform into a single library. fn lipo_platform_libraries( platform_build_paths: &HashMap>, - generated_dir: &Utf8PathBuf, + generated_dir: &Utf8Path, ) -> Result> { let mut libs = Vec::new(); let sh = sh(); @@ -350,7 +350,7 @@ fn lipo_platform_libraries( /// Moves all files of the specified file extension from one directory into /// another. -fn move_files(extension: &str, source: &Utf8PathBuf, destination: &Utf8PathBuf) -> Result<()> { +fn move_files(extension: &str, source: &Utf8Path, destination: &Utf8Path) -> Result<()> { for entry in source.read_dir_utf8()? { let entry = entry?; @@ -368,7 +368,7 @@ fn move_files(extension: &str, source: &Utf8PathBuf, destination: &Utf8PathBuf) /// Consolidates the contents of each modulemap file found in the source /// directory into a single `module.modulemap` file in the destination /// directory. -fn consolidate_modulemap_files(source: &Utf8PathBuf, destination: &Utf8PathBuf) -> Result<()> { +fn consolidate_modulemap_files(source: &Utf8Path, destination: &Utf8Path) -> Result<()> { let mut modulemap = String::new(); for entry in source.read_dir_utf8()? { let entry = entry?; From 9975365a1eb4d851fd4ec1325592b70605f04aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 18 Dec 2024 17:14:32 +0100 Subject: [PATCH 817/979] feat(room): add `Room::room_member_updates_sender` This sender will notify receivers when new room members are received: this can happen either when reloading the full room member list from the HS or when new member events arrive during a sync. The sender will emit a `RoomMembersUpdate`, which can be either a full reload or a partial one, including the user ids of the members that changed. --- crates/matrix-sdk-base/src/client.rs | 16 ++++++++- crates/matrix-sdk-base/src/lib.rs | 1 + crates/matrix-sdk-base/src/rooms/mod.rs | 4 +-- crates/matrix-sdk-base/src/rooms/normal.rs | 40 +++++++++++++++++++++- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 05de8660e3c..790a6c00431 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -73,7 +73,7 @@ use crate::{ event_cache::store::EventCacheStoreLock, response_processors::AccountDataProcessor, rooms::{ - normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons}, + normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate}, Room, RoomInfo, RoomState, }, store::{ @@ -983,6 +983,9 @@ impl BaseClient { let mut new_rooms = RoomUpdates::default(); let mut notifications = Default::default(); + let mut updated_members_in_room: BTreeMap> = + BTreeMap::new(); + for (room_id, new_info) in response.rooms.join { let room = self.store.get_or_create_room( &room_id, @@ -1011,6 +1014,8 @@ impl BaseClient { ) .await?; + updated_members_in_room.insert(room_id.to_owned(), user_ids.clone()); + for raw in &new_info.ephemeral.events { match raw.deserialize() { Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => { @@ -1252,6 +1257,13 @@ impl BaseClient { // above. Oh well. new_rooms.update_in_memory_caches(&self.store).await; + for (room_id, member_ids) in updated_members_in_room { + if let Some(room) = self.get_room(&room_id) { + let _ = + room.room_member_updates_sender.send(RoomMembersUpdate::Partial(member_ids)); + } + } + info!("Processed a sync response in {:?}", now.elapsed()); let response = SyncResponse { @@ -1401,6 +1413,8 @@ impl BaseClient { self.store.save_changes(&changes).await?; self.apply_changes(&changes, Default::default()); + let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload); + Ok(()) } diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 0c4f394fd7e..1c37a915f52 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -37,6 +37,7 @@ mod rooms; pub mod read_receipts; pub use read_receipts::PreviousEventsProvider; +pub use rooms::RoomMembersUpdate; #[cfg(feature = "experimental-sliding-sync")] pub mod sliding_sync; diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index a426ca7133e..d738199ed95 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -12,8 +12,8 @@ use std::{ use bitflags::bitflags; pub use members::RoomMember; pub use normal::{ - Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState, - RoomStateFilter, + Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, + RoomMembersUpdate, RoomState, RoomStateFilter, }; use regex::Regex; use ruma::{ diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 0370f13b70e..a8bca610a13 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -71,7 +71,7 @@ use super::{ use crate::latest_event::LatestEvent; use crate::{ deserialized_responses::{ - DisplayName, MemberEvent, RawSyncOrStrippedState, SyncOrStrippedState, + DisplayName, MemberEvent, RawMemberEvent, RawSyncOrStrippedState, SyncOrStrippedState, }, notification_settings::RoomNotificationMode, read_receipts::RoomReadReceipts, @@ -174,6 +174,9 @@ pub struct Room { /// user has marked as seen so they can be ignored. pub seen_knock_request_ids_map: SharedObservable>, AsyncLock>, + + /// A sender that will notify receivers when room member updates happen. + pub room_member_updates_sender: broadcast::Sender, } /// The room summary containing member counts and members that should be used to @@ -262,6 +265,15 @@ fn heroes_filter<'a>( move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id) } +/// The kind of room member updates that just happened. +#[derive(Debug, Clone)] +pub enum RoomMembersUpdate { + /// The whole list room members was reloaded. + FullReload, + /// A few members were updated, their user ids are included. + Partial(BTreeSet), +} + impl Room { /// The size of the latest_encrypted_events RingBuffer // SAFETY: `new_unchecked` is safe because 10 is not zero. @@ -286,6 +298,7 @@ impl Room { room_info: RoomInfo, room_info_notable_update_sender: broadcast::Sender, ) -> Self { + let (room_member_updates_sender, _) = broadcast::channel(10); Self { own_user_id: own_user_id.into(), room_id: room_info.room_id.clone(), @@ -297,6 +310,7 @@ impl Room { ))), room_info_notable_update_sender, seen_knock_request_ids_map: SharedObservable::new_async(None), + room_member_updates_sender, } } @@ -2075,6 +2089,7 @@ mod tests { use std::{ collections::BTreeSet, ops::{Not, Sub}, + pin::pin, str::FromStr, sync::Arc, time::Duration, @@ -3718,4 +3733,27 @@ mod tests { ] ); } + + #[async_test] + async fn test_room_member_updates_sender_and_receiver() { + use assert_matches::assert_matches; + + let client = logged_in_base_client(None).await; + let room = client.get_or_create_room(room_id!("!a:b.c"), RoomState::Joined); + + let mut receiver = room.room_member_updates_sender.subscribe(); + + assert!(receiver.is_empty()); + + room.room_member_updates_sender + .send(RoomMembersUpdate::FullReload) + .expect("broadcasting a room members update failed"); + + let recv = pin!(receiver.recv()); + let next = matrix_sdk_common::timeout::timeout(recv, Duration::from_secs(1)) + .await + .expect("receiving a room members update timed out") + .expect("failed receiving a room members update"); + assert_matches!(next, RoomMembersUpdate::FullReload); + } } From 5d0fed5e535a1b1c7cf3b9bd3259f36c48f2c62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 18 Dec 2024 17:18:20 +0100 Subject: [PATCH 818/979] feat(room): add helper methods to `BaseRoom` to get and write the current seen knock request ids while keeping them thread-safe with a lock around them --- crates/matrix-sdk-base/src/rooms/normal.rs | 70 ++++++++++++---------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index a8bca610a13..6b7aa75c855 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1221,21 +1221,14 @@ impl Room { } } - let mut current_seen_events_guard = self.seen_knock_request_ids_map.write().await; + let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?; // We're not calling `get_seen_join_request_ids` here because we need to keep // the Mutex's guard until we've updated the data - let mut current_seen_events = if current_seen_events_guard.is_none() { - self.load_cached_knock_request_ids().await? - } else { - current_seen_events_guard.clone().unwrap() - }; + let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default(); current_seen_events.extend(event_to_user_ids); - ObservableWriteGuard::set( - &mut current_seen_events_guard, - Some(current_seen_events.clone()), - ); + self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?; self.store .set_kv_data( @@ -1251,27 +1244,46 @@ impl Room { pub async fn get_seen_knock_request_ids( &self, ) -> Result, StoreError> { + Ok(self.get_write_guarded_current_knock_request_ids().await?.clone().unwrap_or_default()) + } + + async fn get_write_guarded_current_knock_request_ids( + &self, + ) -> StoreResult>, AsyncLock>> + { let mut guard = self.seen_knock_request_ids_map.write().await; + // If there are no loaded request ids yet if guard.is_none() { - ObservableWriteGuard::set( - &mut guard, - Some(self.load_cached_knock_request_ids().await?), - ); + // Load the values from the store and update the shared observable contents + let updated_seen_ids = self + .store + .get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id())) + .await? + .and_then(|v| v.into_seen_knock_requests()) + .unwrap_or_default(); + + ObservableWriteGuard::set(&mut guard, Some(updated_seen_ids)); } - Ok(guard.clone().unwrap_or_default()) + Ok(guard) } - /// This loads the current list of seen knock request ids from the state - /// store. - async fn load_cached_knock_request_ids( + async fn update_seen_knock_request_ids( &self, - ) -> StoreResult> { - Ok(self - .store - .get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id())) - .await? - .and_then(|v| v.into_seen_knock_requests()) - .unwrap_or_default()) + mut guard: ObservableWriteGuard<'_, Option>, AsyncLock>, + new_value: BTreeMap, + ) -> StoreResult<()> { + // Save the new values to the shared observable + ObservableWriteGuard::set(&mut guard, Some(new_value.clone())); + + // Save them into the store too + self.store + .set_kv_data( + StateStoreDataKey::SeenKnockRequests(self.room_id()), + StateStoreDataValue::SeenKnockRequests(new_value), + ) + .await?; + + Ok(()) } } @@ -2135,13 +2147,7 @@ mod tests { use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo}; #[cfg(any(feature = "experimental-sliding-sync", feature = "e2e-encryption"))] use crate::latest_event::LatestEvent; - use crate::{ - rooms::RoomNotableTags, - store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig}, - test_utils::logged_in_base_client, - BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName, - RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta, - }; + use crate::{rooms::RoomNotableTags, store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig}, test_utils::logged_in_base_client, BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomStateFilter, SessionMeta}; #[test] #[cfg(feature = "experimental-sliding-sync")] From 4a88e7cfeee02eea8597459e422430ee2fb8323a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 18 Dec 2024 17:46:48 +0100 Subject: [PATCH 819/979] feat(room): add `BaseRoom::remove_outdated_seen_knock_requests_ids` fn This will check the current seen knock request ids against the room members related to them and will remove those seen ids for members which are no longer in knock state or come from an outdated knock member event. --- crates/matrix-sdk-base/src/rooms/normal.rs | 90 +++++++++------ .../tests/integration/room/joined.rs | 105 ++++++++++++++++++ 2 files changed, 161 insertions(+), 34 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 6b7aa75c855..bd6da19832d 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -15,7 +15,7 @@ #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] use std::sync::RwLock as SyncRwLock; use std::{ - collections::{BTreeMap, HashSet}, + collections::{BTreeMap, BTreeSet, HashSet}, mem, sync::{atomic::AtomicBool, Arc}, }; @@ -1222,20 +1222,60 @@ impl Room { } let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?; - // We're not calling `get_seen_join_request_ids` here because we need to keep - // the Mutex's guard until we've updated the data let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default(); current_seen_events.extend(event_to_user_ids); self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?; - self.store - .set_kv_data( - StateStoreDataKey::SeenKnockRequests(self.room_id()), - StateStoreDataValue::SeenKnockRequests(current_seen_events), - ) - .await?; + Ok(()) + } + + /// Removes the seen knock request ids that are no longer valid given the + /// current room members. + pub async fn remove_outdated_seen_knock_requests_ids(&self) -> StoreResult<()> { + let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?; + let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default(); + + // Get and deserialize the member events for the seen knock requests + let keys: Vec = current_seen_events.values().map(|id| id.to_owned()).collect(); + let raw_member_events: Vec = + self.store.get_state_events_for_keys_static(self.room_id(), &keys).await?; + let member_events = raw_member_events + .into_iter() + .map(|raw| raw.deserialize()) + .collect::, _>>()?; + + let mut ids_to_remove = Vec::new(); + + for (event_id, user_id) in current_seen_events.iter() { + // Check the seen knock request ids against the current room member events for + // the room members associated to them + let matching_member = member_events.iter().find(|event| event.user_id() == user_id); + + if let Some(member) = matching_member { + let member_event_id = member.event_id(); + // If the member event is not a knock or it's different knock, it's outdated + if *member.membership() != MembershipState::Knock + || member_event_id.is_some_and(|id| id != event_id) + { + ids_to_remove.push(event_id.to_owned()); + } + } else { + ids_to_remove.push(event_id.to_owned()); + } + } + + // If there are no ids to remove, do nothing + if ids_to_remove.is_empty() { + return Ok(()); + } + + for event_id in ids_to_remove { + current_seen_events.remove(&event_id); + } + + self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?; Ok(()) } @@ -2101,7 +2141,6 @@ mod tests { use std::{ collections::BTreeSet, ops::{Not, Sub}, - pin::pin, str::FromStr, sync::Arc, time::Duration, @@ -2147,7 +2186,13 @@ mod tests { use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo}; #[cfg(any(feature = "experimental-sliding-sync", feature = "e2e-encryption"))] use crate::latest_event::LatestEvent; - use crate::{rooms::RoomNotableTags, store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig}, test_utils::logged_in_base_client, BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomStateFilter, SessionMeta}; + use crate::{ + rooms::RoomNotableTags, + store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig}, + test_utils::logged_in_base_client, + BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName, + RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta, + }; #[test] #[cfg(feature = "experimental-sliding-sync")] @@ -3739,27 +3784,4 @@ mod tests { ] ); } - - #[async_test] - async fn test_room_member_updates_sender_and_receiver() { - use assert_matches::assert_matches; - - let client = logged_in_base_client(None).await; - let room = client.get_or_create_room(room_id!("!a:b.c"), RoomState::Joined); - - let mut receiver = room.room_member_updates_sender.subscribe(); - - assert!(receiver.is_empty()); - - room.room_member_updates_sender - .send(RoomMembersUpdate::FullReload) - .expect("broadcasting a room members update failed"); - - let recv = pin!(receiver.recv()); - let next = matrix_sdk_common::timeout::timeout(recv, Duration::from_secs(1)) - .await - .expect("receiving a room members update timed out") - .expect("failed receiving a room members update"); - assert_matches!(next, RoomMembersUpdate::FullReload); - } } diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 4e6ca00de93..76f9b38a036 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -947,4 +947,109 @@ async fn test_subscribe_to_requests_to_join_reloads_members_on_limited_sync() { // There should be no other knock requests assert_pending!(stream) + assert_pending!(stream); + + handle.abort(); +} + +#[async_test] +async fn test_remove_outdated_seen_knock_requests_ids_when_membership_changed() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a:b.c"); + let f = EventFactory::new().room(room_id); + + let user_id = user_id!("@alice:b.c"); + let knock_event_id = event_id!("$alice-knock:b.c"); + let knock_event = f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(knock_event_id) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast(); + + // When syncing the room, we'll have a knock request coming from alice + let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id).add_state_bulk(vec![knock_event])).await; + + // We then mark the knock request as seen + room.mark_knock_requests_as_seen(&[user_id.to_owned()]).await.unwrap(); + + // Now it's received again as seen + let seen = room.get_seen_knock_request_ids().await.unwrap(); + assert_eq!(seen.len(), 1); + + // If we then load the members again and the previously knocking member is in + // another state now + let joined_event = f + .event(RoomMemberEventContent::new(MembershipState::Join)) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast(); + + server.mock_get_members().ok(vec![joined_event]).mock_once().mount().await; + + room.mark_members_missing(); + room.sync_members().await.expect("could not reload room members"); + + // Calling remove outdated seen knock request ids will remove the seen id + room.remove_outdated_seen_knock_requests_ids().await.expect("could not remove outdated seen knock request ids"); + + let seen = room.get_seen_knock_request_ids().await.unwrap(); + assert!(seen.is_empty()); +} + +#[async_test] +async fn test_remove_outdated_seen_knock_requests_ids_when_we_have_an_outdated_knock() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a:b.c"); + let f = EventFactory::new().room(room_id); + + let user_id = user_id!("@alice:b.c"); + let knock_event_id = event_id!("$alice-knock:b.c"); + let knock_event = f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(knock_event_id) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast(); + + // When syncing the room, we'll have a knock request coming from alice + let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id).add_state_bulk(vec![knock_event])).await; + + // We then mark the knock request as seen + room.mark_knock_requests_as_seen(&[user_id.to_owned()]).await.unwrap(); + + // Now it's received again as seen + let seen = room.get_seen_knock_request_ids().await.unwrap(); + assert_eq!(seen.len(), 1); + + // If we then load the members again and the previously knocking member has a different event id + let knock_event = f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(event_id!("$knock-2:b.c")) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast(); + + server.mock_get_members().ok(vec![knock_event]).mock_once().mount().await; + + room.mark_members_missing(); + room.sync_members().await.expect("could not reload room members"); + + // Calling remove outdated seen knock request ids will remove the seen id + room.remove_outdated_seen_knock_requests_ids().await.expect("could not remove outdated seen knock request ids"); + + let seen = room.get_seen_knock_request_ids().await.unwrap(); + assert!(seen.is_empty()); } From 616c193a30e9bf67af37f612fce8ced3cce3cb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 18 Dec 2024 17:49:18 +0100 Subject: [PATCH 820/979] feat(room): create a cleanup task in `Room::subscribe_to_knock_requests` This cleanup task will run while the knock request subscription runs and will use the `Room::room_member_updates_sender` notification to call `Room::remove_outdated_seen_knock_requests_ids` and remove outdated seen knock request ids automatically. --- bindings/matrix-sdk-ffi/src/room.rs | 4 +- crates/matrix-sdk/src/room/mod.rs | 28 +++- .../tests/integration/room/joined.rs | 127 ++++++++++++++++-- 3 files changed, 143 insertions(+), 16 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 1402d5d9867..3cef3ca8e67 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -924,13 +924,15 @@ impl Room { self: Arc, listener: Box, ) -> Result, ClientError> { - let stream = self.inner.subscribe_to_knock_requests().await?; + let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?; let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move { pin_mut!(stream); while let Some(requests) = stream.next().await { listener.call(requests.into_iter().map(Into::into).collect()); } + // Cancel the seen ids cleanup task + seen_ids_cleanup_handle.abort(); }))); Ok(handle) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 6ee2995822d..a4a089652a9 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -47,7 +47,11 @@ use matrix_sdk_base::{ ComposerDraft, RoomInfoNotableUpdateReasons, RoomMemberships, StateChanges, StateStoreDataKey, StateStoreDataValue, }; -use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timeout::timeout}; +use matrix_sdk_common::{ + deserialized_responses::SyncTimelineEvent, + executor::{spawn, JoinHandle}, + timeout::timeout, +}; use mime::Mime; #[cfg(feature = "e2e-encryption")] use ruma::events::{ @@ -3224,9 +3228,12 @@ impl Room { /// - A knock request is marked as seen. /// - A sync is gappy (limited), so room membership information may be /// outdated. + /// + /// Returns both a stream of knock requests and a handle for a task that + /// will clean up the seen knock request ids when possible. pub async fn subscribe_to_knock_requests( &self, - ) -> Result>> { + ) -> Result<(impl Stream>, JoinHandle<()>)> { let this = Arc::new(self.clone()); let room_member_events_observer = @@ -3241,6 +3248,21 @@ impl Room { let mut room_info_stream = self.subscribe_info(); + // Spawn a task that will clean up the seen knock request ids when updated room + // members are received + let clear_seen_ids_handle = spawn({ + let this = self.clone(); + async move { + let mut member_updates_stream = this.room_member_updates_sender.subscribe(); + while member_updates_stream.recv().await.is_ok() { + // If room members were updated, try to remove outdated seen knock request ids + if let Err(err) = this.remove_outdated_seen_knock_requests_ids().await { + warn!("Failed to remove seen knock requests: {err}") + } + } + } + }); + let combined_stream = stream! { // Emit current requests to join match this.get_current_join_requests(¤t_seen_ids).await { @@ -3315,7 +3337,7 @@ impl Room { } }; - Ok(combined_stream) + Ok((combined_stream, clear_seen_ids_handle)) } async fn get_current_join_requests( diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 76f9b38a036..e024c3567d1 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -35,6 +35,7 @@ use ruma::{ }; use serde_json::{from_value, json, Value}; use stream_assert::assert_pending; +use tokio::time::sleep; use wiremock::{ matchers::{body_json, body_partial_json, header, method, path_regex}, Mock, ResponseTemplate, @@ -840,7 +841,7 @@ async fn test_enable_encryption_doesnt_stay_unencrypted() { } #[async_test] -async fn test_subscribe_to_requests_to_join() { +async fn test_subscribe_to_knock_requests() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -862,7 +863,7 @@ async fn test_subscribe_to_requests_to_join() { server.mock_get_members().ok(vec![knock_event]).mock_once().mount().await; let room = server.sync_joined_room(&client, room_id).await; - let stream = room.subscribe_to_knock_requests().await.unwrap(); + let (stream, handle) = room.subscribe_to_knock_requests().await.unwrap(); pin_mut!(stream); @@ -893,16 +894,30 @@ async fn test_subscribe_to_requests_to_join() { .cast()]); server.sync_room(&client, joined_room_builder).await; - // The knock requests are now empty + // The knock requests are now empty because we have new member events + let updated_requests = assert_next_with_timeout!(stream, 100); + assert!(updated_requests.is_empty()); + + // And it's emitted again because the seen id value has changed let updated_requests = assert_next_with_timeout!(stream, 100); assert!(updated_requests.is_empty()); // There should be no other knock requests - assert_pending!(stream) + assert_pending!(stream); + + // The seen knock request id is no longer there because the associated knock + // request doesn't exist anymore + let seen_knock_request_ids = room + .get_seen_knock_request_ids() + .await + .expect("could not get current seen knock request ids"); + assert!(seen_knock_request_ids.is_empty()); + + handle.abort(); } #[async_test] -async fn test_subscribe_to_requests_to_join_reloads_members_on_limited_sync() { +async fn test_subscribe_to_knock_requests_reloads_members_on_limited_sync() { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -930,7 +945,7 @@ async fn test_subscribe_to_requests_to_join_reloads_members_on_limited_sync() { .await; let room = server.sync_joined_room(&client, room_id).await; - let stream = room.subscribe_to_knock_requests().await.unwrap(); + let (stream, handle) = room.subscribe_to_knock_requests().await.unwrap(); pin_mut!(stream); @@ -946,7 +961,6 @@ async fn test_subscribe_to_requests_to_join_reloads_members_on_limited_sync() { assert_next_with_timeout!(stream, 500); // There should be no other knock requests - assert_pending!(stream) assert_pending!(stream); handle.abort(); @@ -973,7 +987,9 @@ async fn test_remove_outdated_seen_knock_requests_ids_when_membership_changed() .cast(); // When syncing the room, we'll have a knock request coming from alice - let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id).add_state_bulk(vec![knock_event])).await; + let room = server + .sync_room(&client, JoinedRoomBuilder::new(room_id).add_state_bulk(vec![knock_event])) + .await; // We then mark the knock request as seen room.mark_knock_requests_as_seen(&[user_id.to_owned()]).await.unwrap(); @@ -997,7 +1013,9 @@ async fn test_remove_outdated_seen_knock_requests_ids_when_membership_changed() room.sync_members().await.expect("could not reload room members"); // Calling remove outdated seen knock request ids will remove the seen id - room.remove_outdated_seen_knock_requests_ids().await.expect("could not remove outdated seen knock request ids"); + room.remove_outdated_seen_knock_requests_ids() + .await + .expect("could not remove outdated seen knock request ids"); let seen = room.get_seen_knock_request_ids().await.unwrap(); assert!(seen.is_empty()); @@ -1024,7 +1042,9 @@ async fn test_remove_outdated_seen_knock_requests_ids_when_we_have_an_outdated_k .cast(); // When syncing the room, we'll have a knock request coming from alice - let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id).add_state_bulk(vec![knock_event])).await; + let room = server + .sync_room(&client, JoinedRoomBuilder::new(room_id).add_state_bulk(vec![knock_event])) + .await; // We then mark the knock request as seen room.mark_knock_requests_as_seen(&[user_id.to_owned()]).await.unwrap(); @@ -1033,7 +1053,8 @@ async fn test_remove_outdated_seen_knock_requests_ids_when_we_have_an_outdated_k let seen = room.get_seen_knock_request_ids().await.unwrap(); assert_eq!(seen.len(), 1); - // If we then load the members again and the previously knocking member has a different event id + // If we then load the members again and the previously knocking member has a + // different event id let knock_event = f .event(RoomMemberEventContent::new(MembershipState::Knock)) .event_id(event_id!("$knock-2:b.c")) @@ -1048,8 +1069,90 @@ async fn test_remove_outdated_seen_knock_requests_ids_when_we_have_an_outdated_k room.sync_members().await.expect("could not reload room members"); // Calling remove outdated seen knock request ids will remove the seen id - room.remove_outdated_seen_knock_requests_ids().await.expect("could not remove outdated seen knock request ids"); + room.remove_outdated_seen_knock_requests_ids() + .await + .expect("could not remove outdated seen knock request ids"); let seen = room.get_seen_knock_request_ids().await.unwrap(); assert!(seen.is_empty()); } + +#[async_test] +async fn test_subscribe_to_knock_requests_clears_seen_ids_on_member_reload() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a:b.c"); + let f = EventFactory::new().room(room_id); + + let user_id = user_id!("@alice:b.c"); + let knock_event_id = event_id!("$alice-knock:b.c"); + let knock_event = f + .event(RoomMemberEventContent::new(MembershipState::Knock)) + .event_id(knock_event_id) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast(); + + server.mock_get_members().ok(vec![knock_event]).mock_once().mount().await; + + let room = server.sync_joined_room(&client, room_id).await; + let (stream, handle) = room.subscribe_to_knock_requests().await.unwrap(); + + pin_mut!(stream); + + // We receive an initial knock request from Alice + let initial = assert_next_with_timeout!(stream, 100); + assert_eq!(initial.len(), 1); + + let knock_request = &initial[0]; + assert_eq!(knock_request.event_id, knock_event_id); + assert!(!knock_request.is_seen); + + // We then mark the knock request as seen + room.mark_knock_requests_as_seen(&[user_id.to_owned()]).await.unwrap(); + + // Now it's received again as seen + let seen = assert_next_with_timeout!(stream, 100); + assert_eq!(seen.len(), 1); + let seen_knock = &seen[0]; + assert_eq!(seen_knock.event_id, knock_event_id); + assert!(seen_knock.is_seen); + + // If we then load the members again and the previously knocking member is in + // another state now + let joined_event = f + .event(RoomMemberEventContent::new(MembershipState::Join)) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast(); + + server.mock_get_members().ok(vec![joined_event]).mock_once().mount().await; + + room.mark_members_missing(); + room.sync_members().await.expect("could not reload room members"); + + // The knock requests are now empty because we have new member events + let updated_requests = assert_next_with_timeout!(stream, 100); + assert!(updated_requests.is_empty()); + + // There should be no other knock requests + assert_pending!(stream); + + // Give some time for the seen ids purging to be done + sleep(Duration::from_millis(100)).await; + + // The seen knock request id is no longer there because the associated knock + // request doesn't exist anymore + let seen_knock_request_ids = room + .get_seen_knock_request_ids() + .await + .expect("could not get current seen knock request ids"); + assert!(seen_knock_request_ids.is_empty()); + + handle.abort(); +} From 38cc9fb7c86e2d3940ef9ea8c6c3f8a7df0fd939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 19 Dec 2024 13:37:48 +0100 Subject: [PATCH 821/979] test(room): Improve `Room::room_member_updates_sender` tests --- crates/matrix-sdk/src/test_utils/mod.rs | 12 ++++ .../tests/integration/room/joined.rs | 66 ++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/test_utils/mod.rs b/crates/matrix-sdk/src/test_utils/mod.rs index 649e0501639..80865ad2a8d 100644 --- a/crates/matrix-sdk/src/test_utils/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mod.rs @@ -117,6 +117,18 @@ macro_rules! assert_next_with_timeout { }}; } +/// Asserts the next item in a `Receiver` can be loaded in the given timeout in +/// milliseconds. +#[macro_export] +macro_rules! assert_recv_with_timeout { + ($receiver:expr, $timeout_ms:expr) => {{ + tokio::time::timeout(std::time::Duration::from_millis($timeout_ms), $receiver.recv()) + .await + .expect("Next event timed out") + .expect("No next event received") + }}; +} + /// Assert the next item in a `Stream` or `Subscriber` matches the provided /// pattern in the given timeout in milliseconds. /// diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index e024c3567d1..e2ab2a568da 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -1,16 +1,18 @@ use std::{ + collections::BTreeSet, sync::{Arc, Mutex}, time::Duration, }; +use assert_matches2::assert_let; use futures_util::{future::join_all, pin_mut}; use matrix_sdk::{ - assert_next_with_timeout, + assert_next_with_timeout, assert_recv_with_timeout, config::SyncSettings, room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, test_utils::mocks::MatrixMockServer, }; -use matrix_sdk_base::RoomState; +use matrix_sdk_base::{RoomMembersUpdate, RoomState}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, @@ -1156,3 +1158,63 @@ async fn test_subscribe_to_knock_requests_clears_seen_ids_on_member_reload() { handle.abort(); } + +#[async_test] +async fn test_room_member_updates_sender_on_full_member_reload() { + use assert_matches::assert_matches; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + let mut receiver = room.room_member_updates_sender.subscribe(); + assert!(receiver.is_empty()); + + // When loading the full room member list + let user_id = user_id!("@alice:b.c"); + let joined_event = EventFactory::new() + .room(room_id) + .event(RoomMemberEventContent::new(MembershipState::Join)) + .sender(user_id) + .state_key(user_id) + .into_raw_timeline() + .cast(); + server.mock_get_members().ok(vec![joined_event]).mock_once().mount().await; + room.sync_members().await.expect("could not reload room members"); + + // The member updates sender emits a full reload + let next = assert_recv_with_timeout!(receiver, 100); + assert_matches!(next, RoomMembersUpdate::FullReload); +} + +#[async_test] +async fn test_room_member_updates_sender_on_partial_members_update() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + let mut receiver = room.room_member_updates_sender.subscribe(); + assert!(receiver.is_empty()); + + // When loading a few room member updates + let user_id = user_id!("@alice:b.c"); + let joined_event = EventFactory::new() + .room(room_id) + .event(RoomMemberEventContent::new(MembershipState::Join)) + .sender(user_id) + .state_key(user_id) + .into_raw_sync() + .cast(); + server + .sync_room(&client, JoinedRoomBuilder::new(room_id).add_state_bulk(vec![joined_event])) + .await; + + // The member updates sender emits a partial update with the user ids of the + // members + let next = assert_recv_with_timeout!(receiver, 100); + assert_let!(RoomMembersUpdate::Partial(user_ids) = next); + assert_eq!(user_ids, BTreeSet::from_iter(vec![user_id!("@alice:b.c").to_owned()])); +} From 0d546dce5f4c2b156269cc056f5005435f5df816 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Dec 2024 16:24:01 +0100 Subject: [PATCH 822/979] refactor(event cache): move `PaginationToken` from the pagination to room/mod file --- .../matrix-sdk/src/event_cache/pagination.rs | 23 ++++++++++++++++++ .../matrix-sdk/src/event_cache/paginator.rs | 24 +------------------ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 832398ec57c..4e1fcc0d2bf 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -280,6 +280,29 @@ impl RoomPagination { } } +/// Pagination token data, indicating in which state is the current pagination. +#[derive(Clone, Debug)] +pub(super) enum PaginationToken { + /// We never had a pagination token, so we'll start back-paginating from the + /// end, or forward-paginating from the start. + None, + /// We paginated once before, and we received a prev/next batch token that + /// we may reuse for the next query. + HasMore(String), + /// We've hit one end of the timeline (either the start or the actual end), + /// so there's no need to continue paginating. + HitEnd, +} + +impl From> for PaginationToken { + fn from(token: Option) -> Self { + match token { + Some(val) => Self::HasMore(val), + None => Self::None, + } + } +} + /// A type representing whether the timeline has been reset. #[derive(Debug)] pub enum TimelineHasBeenResetWhilePaginating { diff --git a/crates/matrix-sdk/src/event_cache/paginator.rs b/crates/matrix-sdk/src/event_cache/paginator.rs index 280d033c50e..18f50e0a14e 100644 --- a/crates/matrix-sdk/src/event_cache/paginator.rs +++ b/crates/matrix-sdk/src/event_cache/paginator.rs @@ -23,6 +23,7 @@ use eyeball::{SharedObservable, Subscriber}; use matrix_sdk_base::{deserialized_responses::TimelineEvent, SendOutsideWasm, SyncOutsideWasm}; use ruma::{api::Direction, EventId, OwnedEventId, UInt}; +use super::pagination::PaginationToken; use crate::{ room::{EventWithContextResponse, Messages, MessagesOptions, WeakRoom}, Room, @@ -67,29 +68,6 @@ pub enum PaginatorError { SdkError(#[from] Box), } -/// Pagination token data, indicating in which state is the current pagination. -#[derive(Clone, Debug)] -enum PaginationToken { - /// We never had a pagination token, so we'll start back-paginating from the - /// end, or forward-paginating from the start. - None, - /// We paginated once before, and we received a prev/next batch token that - /// we may reuse for the next query. - HasMore(String), - /// We've hit one end of the timeline (either the start or the actual end), - /// so there's no need to continue paginating. - HitEnd, -} - -impl From> for PaginationToken { - fn from(token: Option) -> Self { - match token { - Some(val) => Self::HasMore(val), - None => Self::None, - } - } -} - /// Paginations tokens used for backward and forward pagination. #[derive(Debug)] struct PaginationTokens { From a20ad728b52e08a5349b280c641b65c34ad28402 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Dec 2024 16:44:36 +0100 Subject: [PATCH 823/979] feat(event cache): don't restart back-pagination from the end if we had no prev-batch token --- .../tests/integration/timeline/edit.rs | 21 +- crates/matrix-sdk/src/event_cache/mod.rs | 2 +- .../matrix-sdk/src/event_cache/pagination.rs | 181 ++++++++++++++---- .../tests/integration/event_cache.rs | 94 ++++++--- 4 files changed, 218 insertions(+), 80 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 9945e6e2c9b..b1f0291854a 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -1054,21 +1054,24 @@ async fn test_pending_edit_from_backpagination_doesnt_override_pending_edit_from let mut h = PendingEditHelper::new().await; let f = EventFactory::new(); - let (_, mut timeline_stream) = h.timeline.subscribe().await; - // When I receive an edit live from a sync for an event I don't know about… let original_event_id = event_id!("$original"); let edit_event_id = event_id!("$edit"); h.handle_sync( - JoinedRoomBuilder::new(&h.room_id).add_timeline_event( - f.text_msg("* hello") - .sender(&ALICE) - .event_id(edit_event_id) - .edit(original_event_id, RoomMessageEventContent::text_plain("[edit]").into()), - ), + JoinedRoomBuilder::new(&h.room_id) + .add_timeline_event( + f.text_msg("* hello") + .sender(&ALICE) + .event_id(edit_event_id) + .edit(original_event_id, RoomMessageEventContent::text_plain("[edit]").into()), + ) + .set_timeline_prev_batch("prev-batch-token".to_owned()) + .set_timeline_limited(), ) .await; + let (_, mut timeline_stream) = h.timeline.subscribe().await; + // And then I receive an edit from a back-pagination for the same event… let edit_event_id2 = event_id!("$edit2"); h.handle_backpagination( @@ -1084,7 +1087,7 @@ async fn test_pending_edit_from_backpagination_doesnt_override_pending_edit_from .await; // Nothing happens. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_matches!(timeline_stream.next().now_or_never(), None); // And then I receive the original event after a bit… h.handle_sync( diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 762e2a64950..37c34f50e91 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -62,7 +62,7 @@ mod pagination; mod room; pub mod paginator; -pub use pagination::{RoomPagination, TimelineHasBeenResetWhilePaginating}; +pub use pagination::{PaginationToken, RoomPagination, TimelineHasBeenResetWhilePaginating}; pub use room::RoomEventCache; /// An error observed in the [`EventCache`]. diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 4e1fcc0d2bf..20f1f14c442 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -35,6 +35,7 @@ use super::{ /// /// Can be created with [`super::RoomEventCache::pagination()`]. #[allow(missing_debug_implementations)] +#[derive(Clone)] pub struct RoomPagination { pub(super) inner: Arc, } @@ -123,6 +124,15 @@ impl RoomPagination { let prev_token = self.get_or_wait_for_token(Some(DEFAULT_WAIT_FOR_TOKEN_DURATION)).await; + let prev_token = match prev_token { + PaginationToken::HasMore(token) => Some(token), + PaginationToken::None => None, + PaginationToken::HitEnd => { + debug!("Not back-paginating since we've reached the start of the timeline."); + return Ok(Some(BackPaginationOutcome { reached_start: true, events: Vec::new() })); + } + }; + let paginator = &self.inner.paginator; paginator.set_idle_state(PaginatorState::Idle, prev_token.clone(), None)?; @@ -221,7 +231,7 @@ impl RoomPagination { /// It will only wait if we *never* saw an initial previous-batch token. /// Otherwise, it will immediately skip. #[doc(hidden)] - pub async fn get_or_wait_for_token(&self, wait_time: Option) -> Option { + pub async fn get_or_wait_for_token(&self, wait_time: Option) -> PaginationToken { fn get_latest(events: &RoomEvents) -> Option { events.rchunks().find_map(|chunk| match chunk.content() { ChunkContent::Gap(gap) => Some(gap.prev_token.clone()), @@ -232,19 +242,35 @@ impl RoomPagination { { // Scope for the lock guard. let state = self.inner.state.read().await; + + // Check if the linked chunk contains any events. If so, absence of a gap means + // we've hit the start of the timeline. If not, absence of a gap + // means we've never received a pagination token from sync, and we + // should wait for one. + let has_events = state.events().events().next().is_some(); + // Fast-path: we do have a previous-batch token already. if let Some(found) = get_latest(state.events()) { - return Some(found); + return PaginationToken::HasMore(found); } + + // If we had events, and there was no gap, then we've hit the end of the + // timeline. + if has_events { + return PaginationToken::HitEnd; + } + // If we've already waited for an initial previous-batch token before, // immediately abort. if state.waited_for_initial_prev_token { - return None; + return PaginationToken::None; } } // If the caller didn't set a wait time, return none early. - let wait_time = wait_time?; + let Some(wait_time) = wait_time else { + return PaginationToken::None; + }; // Otherwise, wait for a notification that we received a previous-batch token. // Note the state lock is released while doing so, allowing other tasks to write @@ -252,9 +278,17 @@ impl RoomPagination { let _ = timeout(wait_time, self.inner.pagination_batch_token_notifier.notified()).await; let mut state = self.inner.state.write().await; - let token = get_latest(state.events()); + state.waited_for_initial_prev_token = true; - token + + if let Some(token) = get_latest(state.events()) { + PaginationToken::HasMore(token) + } else if state.events().events().next().is_some() { + // See logic above, in the read lock guard scope. + PaginationToken::HitEnd + } else { + PaginationToken::None + } } /// Returns a subscriber to the pagination status used for the @@ -281,8 +315,8 @@ impl RoomPagination { } /// Pagination token data, indicating in which state is the current pagination. -#[derive(Clone, Debug)] -pub(super) enum PaginationToken { +#[derive(Clone, Debug, PartialEq)] +pub enum PaginationToken { /// We never had a pagination token, so we'll start back-paginating from the /// end, or forward-paginating from the start. None, @@ -320,6 +354,7 @@ mod tests { mod time_tests { use std::time::{Duration, Instant}; + use assert_matches::assert_matches; use matrix_sdk_base::RoomState; use matrix_sdk_test::{ async_test, event_factory::EventFactory, sync_timeline_event, ALICE, @@ -328,7 +363,8 @@ mod tests { use tokio::{spawn, time::sleep}; use crate::{ - deserialized_responses::SyncTimelineEvent, event_cache::room::events::Gap, + deserialized_responses::SyncTimelineEvent, + event_cache::{pagination::PaginationToken, room::events::Gap}, test_utils::logged_in_client, }; @@ -344,32 +380,15 @@ mod tests { let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap(); - // When I only have events in a room, - room_event_cache - .inner - .state - .write() - .await - .with_events_mut(|events| { - events.push_events([SyncTimelineEvent::new(sync_timeline_event!({ - "sender": "b@z.h", - "type": "m.room.message", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content": { "body":"yolo", "msgtype": "m.text" }, - }))]) - }) - .await - .unwrap(); - let pagination = room_event_cache.pagination(); - // If I don't wait for the backpagination token, + // If I have a room with no events, and try to get a pagination token without + // waiting, let found = pagination.get_or_wait_for_token(None).await; - // Then I don't find it. - assert!(found.is_none()); + // Then I don't get any pagination token. + assert_matches!(found, PaginationToken::None); - // Reset waited_for_initial_prev_token state. + // Reset waited_for_initial_prev_token and event state. pagination.inner.state.write().await.reset().await.unwrap(); // If I wait for a back-pagination token for 0 seconds, @@ -377,7 +396,7 @@ mod tests { let found = pagination.get_or_wait_for_token(Some(Duration::default())).await; let waited = before.elapsed(); // then I don't get any, - assert!(found.is_none()); + assert_matches!(found, PaginationToken::None); // and I haven't waited long. assert!(waited.as_secs() < 1); @@ -389,12 +408,96 @@ mod tests { let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await; let waited = before.elapsed(); // then I still don't get any. - assert!(found.is_none()); + assert_matches!(found, PaginationToken::None); // and I've waited a bit. assert!(waited.as_secs() < 2); assert!(waited.as_secs() >= 1); } + #[async_test] + async fn test_wait_hit_end_of_timeline() { + let client = logged_in_client(None).await; + let room_id = room_id!("!galette:saucisse.bzh"); + client.base_client().get_or_create_room(room_id, RoomState::Joined); + + let event_cache = client.event_cache(); + + event_cache.subscribe().unwrap(); + + let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap(); + + let f = EventFactory::new().room(room_id).sender(*ALICE); + let pagination = room_event_cache.pagination(); + + // Add a previous event. + room_event_cache + .inner + .state + .write() + .await + .with_events_mut(|events| { + events + .push_events([f.text_msg("this is the start of the timeline").into_sync()]); + }) + .await + .unwrap(); + + // If I have a room with events, and try to get a pagination token without + // waiting, + let found = pagination.get_or_wait_for_token(None).await; + // I've reached the start of the timeline. + assert_matches!(found, PaginationToken::HitEnd); + + // If I wait for a back-pagination token for 0 seconds, + let before = Instant::now(); + let found = pagination.get_or_wait_for_token(Some(Duration::default())).await; + let waited = before.elapsed(); + // Then I still have reached the start of the timeline. + assert_matches!(found, PaginationToken::HitEnd); + // and I've waited very little. + assert!(waited.as_secs() < 1); + + // If I wait for a back-pagination token for 1 second, + let before = Instant::now(); + let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await; + let waited = before.elapsed(); + // then I still don't get any. + assert_matches!(found, PaginationToken::HitEnd); + // and I've waited very little (there's no point in waiting in this case). + assert!(waited.as_secs() < 1); + + // Now, reset state. We'll add an event *after* we've started waiting, this + // time. + room_event_cache.clear().await.unwrap(); + + spawn(async move { + sleep(Duration::from_secs(1)).await; + + room_event_cache + .inner + .state + .write() + .await + .with_events_mut(|events| { + events.push_events([f + .text_msg("this is the start of the timeline") + .into_sync()]); + }) + .await + .unwrap(); + }); + + // If I wait for a pagination token, + let before = Instant::now(); + let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(2))).await; + let waited = before.elapsed(); + // since sync has returned all events, and no prior gap, I've hit the end. + assert_matches!(found, PaginationToken::HitEnd); + // and I've waited for the whole duration. + assert!(waited.as_secs() >= 2); + assert!(waited.as_secs() < 3); + } + #[async_test] async fn test_wait_for_pagination_token_already_present() { let client = logged_in_client(None).await; @@ -435,14 +538,14 @@ mod tests { // If I don't wait for a back-pagination token, let found = pagination.get_or_wait_for_token(None).await; // Then I get it. - assert_eq!(found.as_ref(), Some(&expected_token)); + assert_eq!(found, PaginationToken::HasMore(expected_token.clone())); // If I wait for a back-pagination token for 0 seconds, let before = Instant::now(); let found = pagination.get_or_wait_for_token(Some(Duration::default())).await; let waited = before.elapsed(); // then I do get one. - assert_eq!(found.as_ref(), Some(&expected_token)); + assert_eq!(found, PaginationToken::HasMore(expected_token.clone())); // and I haven't waited long. assert!(waited.as_millis() < 100); @@ -451,7 +554,7 @@ mod tests { let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await; let waited = before.elapsed(); // then I do get one. - assert_eq!(found, Some(expected_token)); + assert_eq!(found, PaginationToken::HasMore(expected_token)); // and I haven't waited long. assert!(waited.as_millis() < 100); } @@ -493,14 +596,14 @@ mod tests { // Then first I don't get it (if I'm not waiting,) let found = pagination.get_or_wait_for_token(None).await; - assert!(found.is_none()); + assert_matches!(found, PaginationToken::None); // And if I wait for the back-pagination token for 600ms, let found = pagination.get_or_wait_for_token(Some(Duration::from_millis(600))).await; let waited = before.elapsed(); // then I do get one eventually. - assert_eq!(found, Some(expected_token)); + assert_eq!(found, PaginationToken::HasMore(expected_token)); // and I have waited between ~400 and ~1000 milliseconds. assert!(waited.as_secs() < 1); assert!(waited.as_millis() >= 400); @@ -551,7 +654,7 @@ mod tests { // Retrieving the pagination token will return the most recent one, not the old // one. let found = pagination.get_or_wait_for_token(None).await; - assert_eq!(found, Some(new_token)); + assert_eq!(found, PaginationToken::HasMore(new_token)); } } } diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 20541241840..5f5bc44ba46 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1,13 +1,14 @@ use std::{future::ready, ops::ControlFlow, time::Duration}; use assert_matches::assert_matches; +use assert_matches2::assert_let; use futures_util::FutureExt as _; use matrix_sdk::{ assert_let_timeout, assert_next_matches_with_timeout, deserialized_responses::SyncTimelineEvent, event_cache::{ - paginator::PaginatorState, BackPaginationOutcome, EventCacheError, RoomEventCacheUpdate, - TimelineHasBeenResetWhilePaginating, + paginator::PaginatorState, BackPaginationOutcome, EventCacheError, PaginationToken, + RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, test_utils::{assert_event_matches_msg, mocks::MatrixMockServer}, }; @@ -16,7 +17,7 @@ use matrix_sdk_test::{ }; use ruma::{event_id, events::AnyTimelineEvent, room_id, serde::Raw, user_id}; use serde_json::json; -use tokio::{spawn, sync::broadcast}; +use tokio::{spawn, sync::broadcast, time::sleep}; use wiremock::ResponseTemplate; async fn once( @@ -259,7 +260,7 @@ async fn test_backpaginate_once() { // Then if I backpaginate, let pagination = room_event_cache.pagination(); - assert!(pagination.get_or_wait_for_token(None).await.is_some()); + assert_matches!(pagination.get_or_wait_for_token(None).await, PaginationToken::HasMore(_)); pagination.run_backwards(20, once).await.unwrap() }; @@ -351,7 +352,7 @@ async fn test_backpaginate_many_times_with_many_iterations() { // Then if I backpaginate in a loop, let pagination = room_event_cache.pagination(); - while pagination.get_or_wait_for_token(None).await.is_some() { + while matches!(pagination.get_or_wait_for_token(None).await, PaginationToken::HasMore(_)) { pagination .run_backwards(20, |outcome, timeline_has_been_reset| { num_paginations += 1; @@ -470,7 +471,7 @@ async fn test_backpaginate_many_times_with_one_iteration() { // Then if I backpaginate in a loop, let pagination = room_event_cache.pagination(); - while pagination.get_or_wait_for_token(None).await.is_some() { + while matches!(pagination.get_or_wait_for_token(None).await, PaginationToken::HasMore(_)) { pagination .run_backwards(20, |outcome, timeline_has_been_reset| { num_paginations += 1; @@ -606,8 +607,9 @@ async fn test_reset_while_backpaginating() { // Run the pagination! let pagination = room_event_cache.pagination(); - let first_token = pagination.get_or_wait_for_token(None).await; - assert!(first_token.is_some()); + assert_let!( + PaginationToken::HasMore(first_token) = pagination.get_or_wait_for_token(None).await + ); let backpagination = spawn({ let pagination = room_event_cache.pagination(); @@ -645,8 +647,10 @@ async fn test_reset_while_backpaginating() { assert!(!events.is_empty()); // Now if we retrieve the oldest token, it's set to something else. - let second_token = pagination.get_or_wait_for_token(None).await.unwrap(); - assert!(first_token.unwrap() != second_token); + assert_let!( + PaginationToken::HasMore(second_token) = pagination.get_or_wait_for_token(None).await + ); + assert!(first_token != second_token); assert_eq!(second_token, "third_backpagination"); } @@ -687,7 +691,7 @@ async fn test_backpaginating_without_token() { // We don't have a token. let pagination = room_event_cache.pagination(); - assert!(pagination.get_or_wait_for_token(None).await.is_none()); + assert_eq!(pagination.get_or_wait_for_token(None).await, PaginationToken::None); // If we try to back-paginate with a token, it will hit the end of the timeline // and give us the resulting event. @@ -850,19 +854,10 @@ async fn test_backpaginate_with_no_initial_events() { let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); // Start with a room with an event, but no prev-batch token. - let room = server - .sync_room( - &client, - JoinedRoomBuilder::new(room_id) - .add_timeline_event(f.text_msg("hello").event_id(event_id!("$3"))), - ) - .await; + let room = server.sync_joined_room(&client, room_id).await; let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); - let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); - wait_for_initial_events(events, &mut stream).await; - // The first back-pagination will return these two events. // // Note: it's important to return the same event that came from sync: since we @@ -870,16 +865,37 @@ async fn test_backpaginate_with_no_initial_events() { // from the end of the timeline, which must include the event we got from // sync. + // We need to trigger the following conditions: + // - a back-pagination starts, + // - but then we get events from sync, before the back-pagination is done. + // + // The following things will happen: + // - We don't have a prev-batch token to start with, so the first + // back-pagination doesn't start + // before DEFAULT_WAIT_FOR_TOKEN_DURATION seconds. + // - While the back-pagination is actually running, we need a sync adding events + // to happen + // (after DEFAULT_WAIT_FOR_TOKEN_DURATION + 500 milliseconds). + // - The back-pagination finishes after this sync (after + // DEFAULT_WAIT_FOR_TOKEN_DURATION + 1 + // second). + + let wait_time = Duration::from_millis(500); server .mock_room_messages() - .ok( - "start-token-unused1".to_owned(), - Some("prev_batch".to_owned()), - vec![ - f.text_msg("world").event_id(event_id!("$2")), - f.text_msg("hello").event_id(event_id!("$3")), - ], - Vec::new(), + .respond_with( + ResponseTemplate::new(200) + .set_body_json(json!({ + "chunk": vec![ + f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), + f.text_msg("hello").event_id(event_id!("$3")).into_raw_timeline(), + ], + "start": "start-token-unused1", + "end": "prev_batch" + })) + // This is why we don't use `server.mock_room_messages()`. + // This delay has to be greater than the one used to return the sync response. + .set_delay(2 * wait_time), ) .mock_once() .mount() @@ -904,17 +920,33 @@ async fn test_backpaginate_with_no_initial_events() { // Run pagination: since there's no token, we'll wait a bit for a sync to return // one, and since there's none, we'll end up starting from the end of the // timeline. - pagination.run_backwards(20, once).await.unwrap(); + let pagination_clone = pagination.clone(); + + let first_pagination = spawn(async move { pagination_clone.run_backwards(20, once).await }); + + // Make sure we've waited for the initial token long enough (3 seconds, as of + // 2024-12-16). + sleep(Duration::from_millis(3000) + wait_time).await; + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("hello").event_id(event_id!("$3"))), + ) + .await; + + first_pagination.await.expect("joining must work").expect("first backpagination must work"); + // Second pagination will be instant. pagination.run_backwards(20, once).await.unwrap(); // The linked chunk should contain the events in the correct order. let (events, _stream) = room_event_cache.subscribe().await.unwrap(); + assert_eq!(events.len(), 3, "{events:?}"); assert_event_matches_msg(&events[0], "oh well"); assert_event_matches_msg(&events[1], "hello"); assert_event_matches_msg(&events[2], "world"); - assert_eq!(events.len(), 3); } #[async_test] From d89194f07112d0a6a126b7972920e623f5447d5f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 16 Dec 2024 17:40:34 +0100 Subject: [PATCH 824/979] refactor: display source pagination error in `PaginatorError::SdkError` --- crates/matrix-sdk/src/event_cache/paginator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/event_cache/paginator.rs b/crates/matrix-sdk/src/event_cache/paginator.rs index 18f50e0a14e..4df79363e2a 100644 --- a/crates/matrix-sdk/src/event_cache/paginator.rs +++ b/crates/matrix-sdk/src/event_cache/paginator.rs @@ -64,7 +64,7 @@ pub enum PaginatorError { }, /// There was another SDK error while paginating. - #[error("an error happened while paginating")] + #[error("an error happened while paginating: {0}")] SdkError(#[from] Box), } From 3f0712010f686de2f2987d746f89647039e7dfe0 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 17 Dec 2024 16:15:11 +0100 Subject: [PATCH 825/979] refactor(event cache): add a way to know if we deduplicated all events (at least one) --- .../matrix-sdk/src/event_cache/pagination.rs | 11 ++-- .../matrix-sdk/src/event_cache/room/events.rs | 66 +++++++++++++++---- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 20f1f14c442..7ceb38fc4e9 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -185,7 +185,7 @@ impl RoomPagination { let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); // First, insert events. - let insert_new_gap_pos = if let Some(gap_id) = prev_gap_id { + let (add_event_report, insert_new_gap_pos) = if let Some(gap_id) = prev_gap_id { // There is a prior gap, let's replace it by new events! trace!("replaced gap with new events from backpagination"); room_events @@ -195,16 +195,17 @@ impl RoomPagination { // No prior gap, but we had some events: assume we need to prepend events // before those. trace!("inserted events before the first known event"); - room_events + let report = room_events .insert_events_at(sync_events, pos) .expect("pos is a valid position we just read above"); - Some(pos) + (report, Some(pos)) } else { // No prior gap, and no prior events: push the events. trace!("pushing events received from back-pagination"); - room_events.push_events(sync_events); + let report = room_events.push_events(sync_events); // A new gap may be inserted before the new events, if there are any. - room_events.events().next().map(|(item_pos, _)| item_pos) + let next_pos = room_events.events().next().map(|(item_pos, _)| item_pos); + (report, next_pos) }; // And insert the new gap if needs be. diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 67a65635153..81e75bf79bc 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -95,13 +95,18 @@ impl RoomEvents { /// Push events after all events or gaps. /// /// The last event in `events` is the most recent one. - pub fn push_events(&mut self, events: I) + pub fn push_events(&mut self, events: I) -> AddEventReport where I: IntoIterator, { let (unique_events, duplicated_event_ids) = self.filter_duplicated_events(events.into_iter()); + let report = AddEventReport { + num_new_unique: unique_events.len(), + num_duplicated: duplicated_event_ids.len(), + }; + // Remove the _old_ duplicated events! // // We don't have to worry the removals can change the position of the existing @@ -110,6 +115,8 @@ impl RoomEvents { // Push new `events`. self.chunks.push_items_back(unique_events); + + report } /// Push a gap after all events or gaps. @@ -118,13 +125,22 @@ impl RoomEvents { } /// Insert events at a specified position. - pub fn insert_events_at(&mut self, events: I, mut position: Position) -> Result<(), Error> + pub fn insert_events_at( + &mut self, + events: I, + mut position: Position, + ) -> Result where I: IntoIterator, { let (unique_events, duplicated_event_ids) = self.filter_duplicated_events(events.into_iter()); + let report = AddEventReport { + num_new_unique: unique_events.len(), + num_duplicated: duplicated_event_ids.len(), + }; + // Remove the _old_ duplicated events! // // We **have to worry* the removals can change the position of the @@ -132,7 +148,9 @@ impl RoomEvents { // argument value for each removal. self.remove_events_and_update_insert_position(duplicated_event_ids, &mut position); - self.chunks.insert_items_at(unique_events, position) + self.chunks.insert_items_at(unique_events, position)?; + + Ok(report) } /// Insert a gap at a specified position. @@ -145,19 +163,24 @@ impl RoomEvents { /// Because the `gap_identifier` can represent non-gap chunk, this method /// returns a `Result`. /// - /// This method returns either the position of the first chunk that's been - /// created, or the next insert position if the chunk has been removed. + /// This method returns a reference to the (first if many) newly created + /// `Chunk` that contains the `items`. pub fn replace_gap_at( &mut self, events: I, gap_identifier: ChunkIdentifier, - ) -> Result, Error> + ) -> Result<(AddEventReport, Option), Error> where I: IntoIterator, { let (unique_events, duplicated_event_ids) = self.filter_duplicated_events(events.into_iter()); + let report = AddEventReport { + num_new_unique: unique_events.len(), + num_duplicated: duplicated_event_ids.len(), + }; + // Remove the _old_ duplicated events! // // We don't have to worry the removals can change the position of the existing @@ -165,14 +188,15 @@ impl RoomEvents { // because of the removals. self.remove_events(duplicated_event_ids); - if unique_events.is_empty() { + let next_pos = if unique_events.is_empty() { // There are no new events, so there's no need to create a new empty items // chunk; instead, remove the gap. - self.chunks.remove_gap_at(gap_identifier) + self.chunks.remove_gap_at(gap_identifier)? } else { // Replace the gap by new events. - Ok(Some(self.chunks.replace_gap_at(unique_events, gap_identifier)?.first_position())) - } + Some(self.chunks.replace_gap_at(unique_events, gap_identifier)?.first_position()) + }; + Ok((report, next_pos)) } /// Search for a chunk, and return its identifier. @@ -398,6 +422,20 @@ impl RoomEvents { } } +pub(in crate::event_cache) struct AddEventReport { + /// Number of new unique events that have been added. + num_new_unique: usize, + /// Number of events which have been deduplicated. + num_duplicated: usize, +} + +impl AddEventReport { + /// Were all the events (at least one) we added already known? + pub fn deduplicated_all_new_events(&self) -> bool { + self.num_new_unique > 0 && self.num_new_unique == self.num_duplicated + } +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; @@ -814,8 +852,10 @@ mod tests { .unwrap(); // The next insert position is the next chunk's start. - let pos = room_events.replace_gap_at([], first_gap_id).unwrap(); + let (report, pos) = room_events.replace_gap_at([], first_gap_id).unwrap(); assert_eq!(pos, Some(Position::new(ChunkIdentifier::new(2), 0))); + assert_eq!(report.num_new_unique, 0); + assert_eq!(report.num_duplicated, 0); // Remove the second gap. let second_gap_id = room_events @@ -824,8 +864,10 @@ mod tests { .unwrap(); // No next insert position. - let pos = room_events.replace_gap_at([], second_gap_id).unwrap(); + let (report, pos) = room_events.replace_gap_at([], second_gap_id).unwrap(); assert!(pos.is_none()); + assert_eq!(report.num_new_unique, 0); + assert_eq!(report.num_duplicated, 0); } #[test] From bcb9a86a00b8b3f3e2fc3d99aa87a6fdd959771d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 17 Dec 2024 16:15:29 +0100 Subject: [PATCH 826/979] feat(event cache): don't add a previous gap if all events were deduplicated, after sync --- crates/matrix-sdk/src/event_cache/room/mod.rs | 23 +++++- .../tests/integration/event_cache.rs | 81 +++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index ddbb5ceab56..21c818f59bc 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -36,7 +36,7 @@ use tokio::sync::{ broadcast::{Receiver, Sender}, Notify, RwLock, RwLockReadGuard, RwLockWriteGuard, }; -use tracing::{trace, warn}; +use tracing::{debug, trace, warn}; use super::{ paginator::{Paginator, PaginatorState}, @@ -277,6 +277,10 @@ impl RoomEventCacheInner { } fn handle_account_data(&self, account_data: Vec>) { + if account_data.is_empty() { + return; + } + let mut handled_read_marker = false; trace!("Handling account data"); @@ -536,7 +540,22 @@ impl RoomEventCacheInner { room_events.push_gap(Gap { prev_token: prev_token.clone() }); } - room_events.push_events(sync_timeline_events.clone()); + let add_event_report = room_events.push_events(sync_timeline_events.clone()); + + if add_event_report.deduplicated_all_new_events() { + debug!( + "not storing previous batch token, because we deduplicated all new sync events" + ); + + // Remove the gap we just inserted. + let prev_gap_id = room_events + .rchunks() + .find_map(|c| c.is_gap().then_some(c.identifier())) + .expect("we just inserted the gap beforehand"); + room_events + .replace_gap_at([], prev_gap_id) + .expect("we obtained the valid position beforehand"); + } }) .await?; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 5f5bc44ba46..daf82d7a6e7 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1019,3 +1019,84 @@ async fn test_backpaginate_replace_empty_gap() { assert_event_matches_msg(&events[1], "world"); assert_eq!(events.len(), 2); } + +#[async_test] +async fn test_no_gap_stored_after_deduplicated_sync() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + let room_id = room_id!("!omelette:fromage.fr"); + + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + + let initial_events = vec![ + f.text_msg("hello").event_id(event_id!("$1")).into_raw_sync(), + f.text_msg("world").event_id(event_id!("$2")).into_raw_sync(), + f.text_msg("sup").event_id(event_id!("$3")).into_raw_sync(), + ]; + + // Start with a room with a few events, limited timeline and prev-batch token. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_bulk(initial_events.clone()) + .set_timeline_limited() + .set_timeline_prev_batch("prev-batch".to_owned()), + ) + .await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); + if events.is_empty() { + let update = stream.recv().await.expect("read error"); + assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + } + drop(events); + + // Backpagination will return nothing. + server + .mock_room_messages() + .ok("start-token-unused1".to_owned(), None, Vec::>::new(), Vec::new()) + .mock_once() + .mount() + .await; + + let pagination = room_event_cache.pagination(); + + // Run pagination once: it will consume the unique gap we had. + pagination.run_backwards(20, once).await.unwrap(); + + // Now simulate that the sync returns the same events (which can happen with + // simplified sliding sync). + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_bulk(initial_events) + .set_timeline_limited() + .set_timeline_prev_batch("prev-batch".to_owned()), + ) + .await; + + let update = stream.recv().await.expect("read error"); + assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + + // If this back-pagination fails, that's because we've stored a gap that's + // useless. It should be short-circuited because there's no previous gap. + let outcome = pagination.run_backwards(20, once).await.unwrap(); + assert!(outcome.reached_start); + + let (events, _stream) = room_event_cache.subscribe().await.unwrap(); + assert_event_matches_msg(&events[0], "hello"); + assert_event_matches_msg(&events[1], "world"); + assert_event_matches_msg(&events[2], "sup"); + assert_eq!(events.len(), 3); +} From 60f521cc237fa7df665ed95c2c036f5f13be2b5e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 17 Dec 2024 16:15:45 +0100 Subject: [PATCH 827/979] feat(event cache): don't add a previous gap if all events were deduplicated, after back-pagination --- .../matrix-sdk/src/event_cache/pagination.rs | 22 ++-- .../tests/integration/event_cache.rs | 102 ++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 7ceb38fc4e9..78261e51d86 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -209,14 +209,22 @@ impl RoomPagination { }; // And insert the new gap if needs be. - if let Some(new_gap) = new_gap { - if let Some(new_pos) = insert_new_gap_pos { - room_events - .insert_gap_at(new_gap, new_pos) - .expect("events_chunk_pos represents a valid chunk position"); - } else { - room_events.push_gap(new_gap); + // + // We only do this when at least one new, non-duplicated event, has been added + // to the chunk. Otherwise it means we've back-paginated all the + // known events. + if !add_event_report.deduplicated_all_new_events() { + if let Some(new_gap) = new_gap { + if let Some(new_pos) = insert_new_gap_pos { + room_events + .insert_gap_at(new_gap, new_pos) + .expect("events_chunk_pos represents a valid chunk position"); + } else { + room_events.push_gap(new_gap); + } } + } else { + debug!("not storing previous batch token, because we deduplicated all new back-paginated events"); } BackPaginationOutcome { events, reached_start } diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index daf82d7a6e7..7b880c7404f 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1100,3 +1100,105 @@ async fn test_no_gap_stored_after_deduplicated_sync() { assert_event_matches_msg(&events[2], "sup"); assert_eq!(events.len(), 3); } + +#[async_test] +async fn test_no_gap_stored_after_deduplicated_backpagination() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + let room_id = room_id!("!omelette:fromage.fr"); + + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + + // Start with a room with a single event, limited timeline and prev-batch token. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("sup").event_id(event_id!("$3")).into_raw_sync()) + .set_timeline_limited() + .set_timeline_prev_batch("prev-batch".to_owned()), + ) + .await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); + if events.is_empty() { + let update = stream.recv().await.expect("read error"); + assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + } + drop(events); + + // Now, simulate that we expanded the timeline window with sliding sync, by + // returning more items. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_bulk(vec![ + f.text_msg("hello").event_id(event_id!("$1")).into_raw_sync(), + f.text_msg("world").event_id(event_id!("$2")).into_raw_sync(), + f.text_msg("sup").event_id(event_id!("$3")).into_raw_sync(), + ]) + .set_timeline_limited() + .set_timeline_prev_batch("prev-batch2".to_owned()), + ) + .await; + + // For prev-batch2, the back-pagination returns nothing. + server + .mock_room_messages() + .from("prev-batch2") + .ok("start-token-unused".to_owned(), None, Vec::>::new(), Vec::new()) + .mock_once() + .mount() + .await; + + // For prev-batch, the back-pagination returns two events we already know, and a + // previous batch token. + server + .mock_room_messages() + .from("prev-batch") + .ok( + "start-token-unused".to_owned(), + Some("prev-batch3".to_owned()), + vec![ + // Items in reverse order, since this is back-pagination. + f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), + f.text_msg("hello").event_id(event_id!("$1")).into_raw_timeline(), + ], + Vec::new(), + ) + .mock_once() + .mount() + .await; + + let pagination = room_event_cache.pagination(); + + // Run pagination once: it will consume prev-batch2 first, which is the most + // recent token. + pagination.run_backwards(20, once).await.unwrap(); + + // Run pagination a second time: it will consume prev-batch, which is the least + // recent token. + pagination.run_backwards(20, once).await.unwrap(); + + // If this back-pagination fails, that's because we've stored a gap that's + // useless. It should be short-circuited because storing the previous gap was + // useless. + let outcome = pagination.run_backwards(20, once).await.unwrap(); + assert!(outcome.reached_start); + + let (events, _stream) = room_event_cache.subscribe().await.unwrap(); + assert_event_matches_msg(&events[0], "hello"); + assert_event_matches_msg(&events[1], "world"); + assert_event_matches_msg(&events[2], "sup"); + assert_eq!(events.len(), 3); +} From d00ff8fa1f35dacf3aecf6af8af3a7558c5536ff Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 18 Dec 2024 14:21:37 +0100 Subject: [PATCH 828/979] refactor(event cache): remove duplicated method `RoomEventCacheState::clear()` --- crates/matrix-sdk/src/event_cache/room/mod.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 21c818f59bc..21e6223d9d4 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -135,7 +135,7 @@ impl RoomEventCache { /// storage. pub async fn clear(&self) -> Result<()> { // Clear the linked chunk and persisted storage. - self.inner.state.write().await.clear().await?; + self.inner.state.write().await.reset().await?; // Clear the (temporary) events mappings. self.inner.all_events.write().await.clear(); @@ -723,14 +723,6 @@ mod private { Ok(Self { room, store, events, waited_for_initial_prev_token: false }) } - /// Clear all cached content for this [`RoomEventCacheState`]. - pub async fn clear(&mut self) -> Result<(), EventCacheError> { - self.events.reset(); - self.propagate_changes().await?; - self.waited_for_initial_prev_token = false; - Ok(()) - } - /// Removes the bundled relations from an event, if they were present. /// /// Only replaces the present if it contained bundled relations. From fe9354a88634d277843bda1b9e710181b4b89816 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 18 Dec 2024 14:31:34 +0100 Subject: [PATCH 829/979] test: make `test_room_keys_received_on_notification_client_trigger_redecryption` more stable When starting to back-paginate, in this test, we: - either have a previous-batch token, that points to the first event *before* the message was sent, - or have no previous-batch token, because we stopped sync before receiving the first sync result. Because of the behavior introduced in 944a9220, we don't restart back-paginating from the end, if we've reached the start. Now, if we are in the case described by the first bullet item, then we may backpaginate until the start of the room, and stop then, because we've back-paginated all events. And so we'll never see the message sent by Alice after we stopped sync'ing. One solution to get to the desired state is to clear the internal state of the room event cache, thus deleting the previous-batch token, thus causing the situation described in the second bullet item. This achieves what we want, that is, back-paginating from the end of the timeline. --- .../src/tests/timeline.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 0b63cfb1c26..7a2dcdc725f 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -558,12 +558,18 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { alice_sync.abort(); // Let's get the timeline and backpaginate to load the event. - let mut timeline = + let timeline = bob_room.timeline().await.expect("We should be able to get a timeline for our room"); let mut item = None; for _ in 0..10 { + { + // Clear any previously received previous-batch token. + let (room_event_cache, _drop_handles) = bob_room.event_cache().await.unwrap(); + room_event_cache.clear().await.unwrap(); + } + timeline .paginate_backwards(50) .await @@ -572,10 +578,9 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { if let Some(timeline_item) = timeline.item_by_event_id(&event_id).await { item = Some(timeline_item); break; - } else { - timeline = bob_room.timeline().await.expect("We should be able to reset our timeline"); - sleep(Duration::from_millis(100)).await } + + sleep(Duration::from_millis(100)).await } let item = item.expect("The event should be in the timeline by now"); From bc8c4f5e581af01037217be3fac26588ba55a66a Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 18 Dec 2024 18:41:51 +0100 Subject: [PATCH 830/979] fix(event cache): don't touch the linked chunk if an operation wouldn't cause meaningful changes See comment on top of `deduplicated_all_new_events`. --- .../matrix-sdk/src/event_cache/pagination.rs | 4 +- .../matrix-sdk/src/event_cache/room/events.rs | 119 ++++++++++-------- crates/matrix-sdk/src/event_cache/room/mod.rs | 10 +- 3 files changed, 71 insertions(+), 62 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 78261e51d86..180a03c12bc 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -185,7 +185,7 @@ impl RoomPagination { let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); // First, insert events. - let (add_event_report, insert_new_gap_pos) = if let Some(gap_id) = prev_gap_id { + let (added_unique_events, insert_new_gap_pos) = if let Some(gap_id) = prev_gap_id { // There is a prior gap, let's replace it by new events! trace!("replaced gap with new events from backpagination"); room_events @@ -213,7 +213,7 @@ impl RoomPagination { // We only do this when at least one new, non-duplicated event, has been added // to the chunk. Otherwise it means we've back-paginated all the // known events. - if !add_event_report.deduplicated_all_new_events() { + if added_unique_events { if let Some(new_gap) = new_gap { if let Some(new_pos) = insert_new_gap_pos { room_events diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 81e75bf79bc..bc7843ff9f0 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -95,17 +95,18 @@ impl RoomEvents { /// Push events after all events or gaps. /// /// The last event in `events` is the most recent one. - pub fn push_events(&mut self, events: I) -> AddEventReport + /// + /// Returns true if the linked chunk was modified, false otherwise. + pub fn push_events(&mut self, events: I) -> bool where I: IntoIterator, { let (unique_events, duplicated_event_ids) = self.filter_duplicated_events(events.into_iter()); - let report = AddEventReport { - num_new_unique: unique_events.len(), - num_duplicated: duplicated_event_ids.len(), - }; + if deduplicated_all_new_events(unique_events.len(), duplicated_event_ids.len()) { + return false; + } // Remove the _old_ duplicated events! // @@ -116,7 +117,7 @@ impl RoomEvents { // Push new `events`. self.chunks.push_items_back(unique_events); - report + true } /// Push a gap after all events or gaps. @@ -125,21 +126,18 @@ impl RoomEvents { } /// Insert events at a specified position. - pub fn insert_events_at( - &mut self, - events: I, - mut position: Position, - ) -> Result + /// + /// Returns true if the linked chunk was modified. + pub fn insert_events_at(&mut self, events: I, mut position: Position) -> Result where I: IntoIterator, { let (unique_events, duplicated_event_ids) = self.filter_duplicated_events(events.into_iter()); - let report = AddEventReport { - num_new_unique: unique_events.len(), - num_duplicated: duplicated_event_ids.len(), - }; + if deduplicated_all_new_events(unique_events.len(), duplicated_event_ids.len()) { + return Ok(false); + } // Remove the _old_ duplicated events! // @@ -150,7 +148,7 @@ impl RoomEvents { self.chunks.insert_items_at(unique_events, position)?; - Ok(report) + Ok(true) } /// Insert a gap at a specified position. @@ -163,23 +161,25 @@ impl RoomEvents { /// Because the `gap_identifier` can represent non-gap chunk, this method /// returns a `Result`. /// - /// This method returns a reference to the (first if many) newly created - /// `Chunk` that contains the `items`. + /// This method returns: + /// - a boolean indicating if we updated the linked chunk, + /// - a reference to the (first if many) newly created `Chunk` that contains + /// the `items`. pub fn replace_gap_at( &mut self, events: I, gap_identifier: ChunkIdentifier, - ) -> Result<(AddEventReport, Option), Error> + ) -> Result<(bool, Option), Error> where I: IntoIterator, { let (unique_events, duplicated_event_ids) = self.filter_duplicated_events(events.into_iter()); - let report = AddEventReport { - num_new_unique: unique_events.len(), - num_duplicated: duplicated_event_ids.len(), - }; + if deduplicated_all_new_events(unique_events.len(), duplicated_event_ids.len()) { + let pos = self.chunks.remove_gap_at(gap_identifier)?; + return Ok((false, pos)); + } // Remove the _old_ duplicated events! // @@ -196,7 +196,8 @@ impl RoomEvents { // Replace the gap by new events. Some(self.chunks.replace_gap_at(unique_events, gap_identifier)?.first_position()) }; - Ok((report, next_pos)) + + Ok((true, next_pos)) } /// Search for a chunk, and return its identifier. @@ -308,6 +309,29 @@ impl RoomEvents { } } +/// Whenever we add new events to the linked chunk, did we *at least add one*, +/// and all the added events were already known (deduplicated)? +/// +/// This is useful to know whether we need to store a previous-batch token (gap) +/// we received from a server-side request (sync or back-pagination), or if we +/// should *not* store it. +/// +/// Since there can be empty back-paginations with a previous-batch token (that +/// is, they don't contain any events), we need to make sure that there is *at +/// least* one new event that has been added. Otherwise, we might conclude +/// something wrong because a subsequent back-pagination might +/// return non-duplicated events. +/// +/// If we had already seen all the duplicated events that we're trying to add, +/// then it would be wasteful to store a previous-batch token, or even touch the +/// linked chunk: we would repeat back-paginations for events that we have +/// already seen, and possibly misplace them. And we should not be missing +/// events either: the already-known events would have their own previous-batch +/// token (it might already be consumed). +fn deduplicated_all_new_events(num_new_unique: usize, num_duplicated: usize) -> bool { + num_new_unique > 0 && num_new_unique == num_duplicated +} + // Private implementations, implementation specific. impl RoomEvents { /// Remove some events from `Self::chunks`. @@ -422,20 +446,6 @@ impl RoomEvents { } } -pub(in crate::event_cache) struct AddEventReport { - /// Number of new unique events that have been added. - num_new_unique: usize, - /// Number of events which have been deduplicated. - num_duplicated: usize, -} - -impl AddEventReport { - /// Were all the events (at least one) we added already known? - pub fn deduplicated_all_new_events(&self) -> bool { - self.num_new_unique > 0 && self.num_new_unique == self.num_duplicated - } -} - #[cfg(test)] mod tests { use assert_matches::assert_matches; @@ -505,28 +515,30 @@ mod tests { fn test_push_events_with_duplicates() { let (event_id_0, event_0) = new_event("$ev0"); let (event_id_1, event_1) = new_event("$ev1"); + let (event_id_2, event_2) = new_event("$ev1"); let mut room_events = RoomEvents::new(); - room_events.push_events([event_0.clone(), event_1]); + room_events.push_events([event_2.clone()]); assert_events_eq!( room_events.events(), [ - (event_id_0 at (0, 0)), - (event_id_1 at (0, 1)), + (event_id_2 at (0, 0)), ] ); - // Everything is alright. Now let's push a duplicated event. - room_events.push_events([event_0]); + // Everything is alright. Now let's push a duplicated event by simulating a + // wider sync. + room_events.push_events([event_0, event_1, event_2]); assert_events_eq!( room_events.events(), [ - // The first `event_id_0` has been removed. - (event_id_1 at (0, 0)), - (event_id_0 at (0, 1)), + // The first `event_id_2` has been removed. + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + (event_id_2 at (0, 2)), ] ); } @@ -552,12 +564,11 @@ mod tests { // Everything is alright. Now let's push a duplicated event. room_events.push_events([event_0]); - // The event has been removed, then the chunk was empty, so removed, and a new - // chunk has been created with identifier 3. + // Nothing has changed in the linked chunk. assert_events_eq!( room_events.events(), [ - (event_id_0 at (3, 0)), + (event_id_0 at (2, 0)), ] ); } @@ -852,10 +863,9 @@ mod tests { .unwrap(); // The next insert position is the next chunk's start. - let (report, pos) = room_events.replace_gap_at([], first_gap_id).unwrap(); + let (touched_linked_chunk, pos) = room_events.replace_gap_at([], first_gap_id).unwrap(); assert_eq!(pos, Some(Position::new(ChunkIdentifier::new(2), 0))); - assert_eq!(report.num_new_unique, 0); - assert_eq!(report.num_duplicated, 0); + assert!(touched_linked_chunk); // Remove the second gap. let second_gap_id = room_events @@ -864,10 +874,9 @@ mod tests { .unwrap(); // No next insert position. - let (report, pos) = room_events.replace_gap_at([], second_gap_id).unwrap(); + let (touched_linked_chunk, pos) = room_events.replace_gap_at([], second_gap_id).unwrap(); assert!(pos.is_none()); - assert_eq!(report.num_new_unique, 0); - assert_eq!(report.num_duplicated, 0); + assert!(touched_linked_chunk); } #[test] diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 21e6223d9d4..06b9fb3e1e8 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -540,9 +540,9 @@ impl RoomEventCacheInner { room_events.push_gap(Gap { prev_token: prev_token.clone() }); } - let add_event_report = room_events.push_events(sync_timeline_events.clone()); + let added_unique_events = room_events.push_events(sync_timeline_events.clone()); - if add_event_report.deduplicated_all_new_events() { + if !added_unique_events { debug!( "not storing previous batch token, because we deduplicated all new sync events" ); @@ -1524,7 +1524,7 @@ mod tests { assert_eq!(items[1].event_id().unwrap(), event_id2); // A new update with one of these events leads to deduplication. - let timeline = Timeline { limited: false, prev_batch: None, events: vec![ev1] }; + let timeline = Timeline { limited: false, prev_batch: None, events: vec![ev2] }; room_event_cache .inner .handle_joined_room_update(true, JoinedRoomUpdate { timeline, ..Default::default() }) @@ -1537,8 +1537,8 @@ mod tests { // element anymore), and it's added to the back of the list. let (items, _stream) = room_event_cache.subscribe().await.unwrap(); assert_eq!(items.len(), 2); - assert_eq!(items[0].event_id().unwrap(), event_id2); - assert_eq!(items[1].event_id().unwrap(), event_id1); + assert_eq!(items[0].event_id().unwrap(), event_id1); + assert_eq!(items[1].event_id().unwrap(), event_id2); } #[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support. From e4712be946a838cecae57375a8c40d1740c97a2b Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 16 Dec 2024 17:17:56 +0000 Subject: [PATCH 831/979] task(crypto): Support receiving stable identifier for MSC4147 --- crates/matrix-sdk-crypto/CHANGELOG.md | 4 + .../olm/group_sessions/sender_data_finder.rs | 2 +- crates/matrix-sdk-crypto/src/olm/session.rs | 2 +- .../src/store/integration_tests.rs | 2 +- .../src/types/events/olm_v1.rs | 137 +++++++++++++++++- 5 files changed, 136 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 5ddf3a801ad..b241e0f816d 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +- Accept stable identifier `sender_device_keys` for MSC4147 (Including device + keys with Olm-encrypted events). + ([#4420](https://github.com/matrix-org/matrix-rust-sdk/pull/4420)) + ## [0.9.0] - 2024-12-18 - Expose new API `DehydratedDevices::get_dehydrated_device_pickle_key`, `DehydratedDevices::save_dehydrated_device_pickle_key` diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs index fcf0394a81b..0f23f013f48 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data_finder.rs @@ -171,7 +171,7 @@ impl<'a> SenderDataFinder<'a> { room_key_event: &'a DecryptedRoomKeyEvent, ) -> Result { // Does the to-device message contain the device_keys property from MSC4147? - if let Some(sender_device_keys) = &room_key_event.device_keys { + if let Some(sender_device_keys) = &room_key_event.sender_device_keys { // Yes: use the device keys to continue. // Validate the signature of the DeviceKeys supplied. diff --git a/crates/matrix-sdk-crypto/src/olm/session.rs b/crates/matrix-sdk-crypto/src/olm/session.rs index 939c488e74d..a51fbaa9dfe 100644 --- a/crates/matrix-sdk-crypto/src/olm/session.rs +++ b/crates/matrix-sdk-crypto/src/olm/session.rs @@ -380,6 +380,6 @@ mod tests { // DecryptedOlmV1Event let event: DecryptedOlmV1Event = serde_json::from_str(&bob_session_result.plaintext).unwrap(); - assert_eq!(event.device_keys.unwrap(), alice.device_keys()); + assert_eq!(event.sender_device_keys.unwrap(), alice.device_keys()); } } diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 410d8dbde04..908415db347 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -1027,7 +1027,7 @@ macro_rules! cryptostore_integration_tests { recipient_keys: OlmV1Keys { ed25519: account.identity_keys().ed25519, }, - device_keys: None, + sender_device_keys: None, content: SecretSendContent::new(id.to_owned(), secret.to_owned()), }; diff --git a/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs b/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs index c9a04910b3f..06c8342106e 100644 --- a/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs +++ b/crates/matrix-sdk-crypto/src/types/events/olm_v1.rs @@ -153,10 +153,10 @@ impl AnyDecryptedOlmEvent { pub fn sender_device_keys(&self) -> Option<&DeviceKeys> { match self { AnyDecryptedOlmEvent::Custom(_) => None, - AnyDecryptedOlmEvent::RoomKey(e) => e.device_keys.as_ref(), - AnyDecryptedOlmEvent::ForwardedRoomKey(e) => e.device_keys.as_ref(), - AnyDecryptedOlmEvent::SecretSend(e) => e.device_keys.as_ref(), - AnyDecryptedOlmEvent::Dummy(e) => e.device_keys.as_ref(), + AnyDecryptedOlmEvent::RoomKey(e) => e.sender_device_keys.as_ref(), + AnyDecryptedOlmEvent::ForwardedRoomKey(e) => e.sender_device_keys.as_ref(), + AnyDecryptedOlmEvent::SecretSend(e) => e.sender_device_keys.as_ref(), + AnyDecryptedOlmEvent::Dummy(e) => e.sender_device_keys.as_ref(), } } } @@ -176,8 +176,8 @@ where /// The recipient's signing keys of the encrypted event. pub recipient_keys: OlmV1Keys, /// The device keys if supplied as per MSC4147 - #[serde(rename = "org.matrix.msc4147.device_keys")] - pub device_keys: Option, + #[serde(alias = "org.matrix.msc4147.device_keys")] + pub sender_device_keys: Option, /// The type of the event. pub content: C, } @@ -201,7 +201,7 @@ impl DecryptedOlmV1Event { recipient: recipient.to_owned(), keys: OlmV1Keys { ed25519: key }, recipient_keys: OlmV1Keys { ed25519: key }, - device_keys, + sender_device_keys: device_keys, content, } } @@ -264,10 +264,18 @@ impl<'de> Deserialize<'de> for AnyDecryptedOlmEvent { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use assert_matches::assert_matches; + use ruma::{device_id, owned_user_id, KeyId}; use serde_json::{json, Value}; + use vodozemac::{Curve25519PublicKey, Ed25519PublicKey, Ed25519Signature}; use super::AnyDecryptedOlmEvent; + use crate::types::{ + events::olm_v1::DecryptedRoomKeyEvent, DeviceKey, DeviceKeys, EventEncryptionAlgorithm, + Signatures, + }; const ED25519_KEY: &str = "aOfOnlaeMb5GW1TxkZ8pXnblkGMgAvps+lAukrdYaZk"; @@ -363,6 +371,80 @@ mod tests { }) } + /// Return the JSON for creating sender device keys, and the matching + /// `DeviceKeys` object that should be created when the JSON is + /// deserialized. + fn sender_device_keys() -> (Value, DeviceKeys) { + let sender_device_keys_json = json!({ + "user_id": "@u:s.co", + "device_id": "DEV", + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2" + ], + "keys": { + "curve25519:DEV": "c29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb28", + "ed25519:DEV": "b29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb28" + }, + "signatures": { + "@u:s.co": { + "ed25519:DEV": "mia28GKixFzOWKJ0h7Bdrdy2fjxiHCsst1qpe467FbW85H61UlshtKBoAXfTLlVfi0FX+/noJ8B3noQPnY+9Cg", + "ed25519:ssk": "mia28GKixFzOWKJ0h7Bdrdy2fjxiHCsst1qpe467FbW85H61UlshtKBoAXfTLlVfi0FX+/noJ8B3noQPnY+9Cg" + } + } + } + ); + + let user_id = owned_user_id!("@u:s.co"); + let device_id = device_id!("DEV"); + let ssk_id = device_id!("ssk"); + + let ed25519_device_key_id = KeyId::from_parts(ruma::DeviceKeyAlgorithm::Ed25519, device_id); + let curve25519_device_key_id = + KeyId::from_parts(ruma::DeviceKeyAlgorithm::Curve25519, device_id); + let ed25519_ssk_id = KeyId::from_parts(ruma::DeviceKeyAlgorithm::Ed25519, ssk_id); + + let mut keys = BTreeMap::new(); + keys.insert( + ed25519_device_key_id.clone(), + DeviceKey::Ed25519( + Ed25519PublicKey::from_base64("b29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb28") + .unwrap(), + ), + ); + keys.insert( + curve25519_device_key_id, + DeviceKey::Curve25519( + Curve25519PublicKey::from_base64("c29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb28") + .unwrap(), + ), + ); + + let mut signatures = Signatures::new(); + signatures.add_signature( + user_id.clone(), + ed25519_device_key_id, + Ed25519Signature::from_base64( + "mia28GKixFzOWKJ0h7Bdrdy2fjxiHCsst1qpe467FbW85H61UlshtKBoAXfTLlVfi0FX+/noJ8B3noQPnY+9Cg" + ).unwrap() + ); + signatures. add_signature( + user_id.clone(), + ed25519_ssk_id, + Ed25519Signature::from_base64( + "mia28GKixFzOWKJ0h7Bdrdy2fjxiHCsst1qpe467FbW85H61UlshtKBoAXfTLlVfi0FX+/noJ8B3noQPnY+9Cg" + ).unwrap() + ); + let sender_device_keys = DeviceKeys::new( + user_id, + device_id.to_owned(), + vec![EventEncryptionAlgorithm::OlmV1Curve25519AesSha2], + keys, + signatures, + ); + + (sender_device_keys_json, sender_device_keys) + } + #[test] fn deserialization() -> Result<(), serde_json::Error> { macro_rules! assert_deserialization_result { @@ -377,7 +459,7 @@ mod tests { } assert_deserialization_result!( - // `m.room_key + // `m.room_key` room_key_event => RoomKey, // `m.forwarded_room_key` @@ -392,4 +474,43 @@ mod tests { Ok(()) } + + #[test] + fn sender_device_keys_are_deserialized_unstable() { + let (sender_device_keys_json, sender_device_keys) = sender_device_keys(); + + // Given JSON for a room key event with sender_device_keys using the unstable + // prefix + let mut event_json = room_key_event(); + event_json + .as_object_mut() + .unwrap() + .insert("org.matrix.msc4147.device_keys".to_owned(), sender_device_keys_json); + + // When we deserialize it + let event: DecryptedRoomKeyEvent = serde_json::from_value(event_json) + .expect("JSON should deserialize to the right event type"); + + // Then it contains the sender_device_keys + assert_eq!(event.sender_device_keys, Some(sender_device_keys)); + } + + #[test] + fn sender_device_keys_are_deserialized() { + let (sender_device_keys_json, sender_device_keys) = sender_device_keys(); + + // Given JSON for a room key event with sender_device_keys + let mut event_json = room_key_event(); + event_json + .as_object_mut() + .unwrap() + .insert("sender_device_keys".to_owned(), sender_device_keys_json); + + // When we deserialize it + let event: DecryptedRoomKeyEvent = serde_json::from_value(event_json) + .expect("JSON should deserialize to the right event type"); + + // Then it contains the sender_device_keys + assert_eq!(event.sender_device_keys, Some(sender_device_keys)); + } } From f2942db316b5bfd4585c101a0966fc05d662d8f9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 19 Dec 2024 16:43:42 +0100 Subject: [PATCH 832/979] refactor: avoid use of `async_trait` for `RoomIdentityProvider` This is an 8 seconds (out of 22) decrease of the matrix-sdk compile times. --- .../src/identities/room_identity_state.rs | 40 ++++++++------- crates/matrix-sdk/src/room/mod.rs | 49 ++++++++++--------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs index 4e07887c7e8..1d7c65aa41f 100644 --- a/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs +++ b/crates/matrix-sdk-crypto/src/identities/room_identity_state.rs @@ -14,7 +14,7 @@ use std::collections::HashMap; -use async_trait::async_trait; +use matrix_sdk_common::BoxFuture; use ruma::{ events::{ room::member::{MembershipState, SyncRoomMemberEvent}, @@ -31,18 +31,17 @@ use crate::store::IdentityUpdates; /// /// This is implemented by `matrix_sdk::Room` and is a trait here so we can /// supply a mock when needed. -#[async_trait] pub trait RoomIdentityProvider: core::fmt::Debug { /// Is the user with the supplied ID a member of this room? - async fn is_member(&self, user_id: &UserId) -> bool; + fn is_member<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, bool>; /// Return a list of the [`UserIdentity`] of all members of this room - async fn member_identities(&self) -> Vec; + fn member_identities(&self) -> BoxFuture<'_, Vec>; /// Return the [`UserIdentity`] of the user with the supplied ID (even if /// they are not a member of this room) or None if this user does not /// exist. - async fn user_identity(&self, user_id: &UserId) -> Option; + fn user_identity<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option>; /// Return the [`IdentityState`] of the supplied user identity. /// Normally only overridden in tests. @@ -352,7 +351,7 @@ mod tests { sync::{Arc, Mutex}, }; - use async_trait::async_trait; + use matrix_sdk_common::BoxFuture; use matrix_sdk_test::async_test; use ruma::{ device_id, @@ -1030,23 +1029,28 @@ mod tests { } } - #[async_trait] impl RoomIdentityProvider for FakeRoom { - async fn is_member(&self, user_id: &UserId) -> bool { - self.users.lock().unwrap().get(user_id).map(|m| m.is_member).unwrap_or(false) + fn is_member<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, bool> { + Box::pin(async { + self.users.lock().unwrap().get(user_id).map(|m| m.is_member).unwrap_or(false) + }) } - async fn member_identities(&self) -> Vec { - self.users - .lock() - .unwrap() - .values() - .filter_map(|m| if m.is_member { Some(m.user_identity.clone()) } else { None }) - .collect() + fn member_identities(&self) -> BoxFuture<'_, Vec> { + Box::pin(async { + self.users + .lock() + .unwrap() + .values() + .filter_map(|m| if m.is_member { Some(m.user_identity.clone()) } else { None }) + .collect() + }) } - async fn user_identity(&self, user_id: &UserId) -> Option { - self.users.lock().unwrap().get(user_id).map(|m| m.user_identity.clone()) + fn user_identity<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option> { + Box::pin(async { + self.users.lock().unwrap().get(user_id).map(|m| m.user_identity.clone()) + }) } fn state_of(&self, user_identity: &UserIdentity) -> IdentityState { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index a4a089652a9..c8cf9a610ca 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -23,8 +23,6 @@ use std::{ }; use async_stream::stream; -#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] -use async_trait::async_trait; use eyeball::SharedObservable; use futures_core::Stream; use futures_util::{ @@ -47,6 +45,8 @@ use matrix_sdk_base::{ ComposerDraft, RoomInfoNotableUpdateReasons, RoomMemberships, StateChanges, StateStoreDataKey, StateStoreDataValue, }; +#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] +use matrix_sdk_common::BoxFuture; use matrix_sdk_common::{ deserialized_responses::SyncTimelineEvent, executor::{spawn, JoinHandle}, @@ -3363,34 +3363,37 @@ impl Room { } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] -#[async_trait] impl RoomIdentityProvider for Room { - async fn is_member(&self, user_id: &UserId) -> bool { - self.get_member(user_id).await.unwrap_or(None).is_some() + fn is_member<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, bool> { + Box::pin(async { self.get_member(user_id).await.unwrap_or(None).is_some() }) } - async fn member_identities(&self) -> Vec { - let members = self - .members(RoomMemberships::JOIN | RoomMemberships::INVITE) - .await - .unwrap_or_else(|_| Default::default()); + fn member_identities(&self) -> BoxFuture<'_, Vec> { + Box::pin(async { + let members = self + .members(RoomMemberships::JOIN | RoomMemberships::INVITE) + .await + .unwrap_or_else(|_| Default::default()); - let mut ret: Vec = Vec::new(); - for member in members { - if let Some(i) = self.user_identity(member.user_id()).await { - ret.push(i); + let mut ret: Vec = Vec::new(); + for member in members { + if let Some(i) = self.user_identity(member.user_id()).await { + ret.push(i); + } } - } - ret + ret + }) } - async fn user_identity(&self, user_id: &UserId) -> Option { - self.client - .encryption() - .get_user_identity(user_id) - .await - .unwrap_or(None) - .map(|u| u.underlying_identity()) + fn user_identity<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option> { + Box::pin(async { + self.client + .encryption() + .get_user_identity(user_id) + .await + .unwrap_or(None) + .map(|u| u.underlying_identity()) + }) } } From 3b31bbec0c89fc1f2763b76f1a71a66a7561651c Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 19 Dec 2024 18:11:40 +0100 Subject: [PATCH 833/979] test(snapshot): Use snapshot testing in sdk-common --- .github/workflows/ci.yml | 3 + CONTRIBUTING.md | 27 ++++ Cargo.lock | 20 +++ Cargo.toml | 4 + crates/matrix-sdk-common/Cargo.toml | 1 + .../src/deserialized_responses.rs | 138 +++++++++++++++++- ...__tests__snapshot_test_algorithm_info.snap | 13 ++ ..._tests__snapshot_test_encryption_info.snap | 15 ++ ...__tests__snapshot_test_shield_codes-2.snap | 5 + ...__tests__snapshot_test_shield_codes-3.snap | 5 + ...__tests__snapshot_test_shield_codes-4.snap | 5 + ...__tests__snapshot_test_shield_codes-5.snap | 5 + ...__tests__snapshot_test_shield_codes-6.snap | 5 + ...es__tests__snapshot_test_shield_codes.snap | 5 + ..._tests__snapshot_test_shield_states-2.snap | 10 ++ ..._tests__snapshot_test_shield_states-3.snap | 10 ++ ...s__tests__snapshot_test_shield_states.snap | 5 + ...ts__snapshot_test_sync_timeline_event.snap | 47 ++++++ ...s__snapshot_test_verification_level-2.snap | 5 + ...s__snapshot_test_verification_level-3.snap | 7 + ...s__snapshot_test_verification_level-4.snap | 7 + ...s__snapshot_test_verification_level-5.snap | 5 + ...sts__snapshot_test_verification_level.snap | 5 + ...__snapshot_test_verification_states-2.snap | 7 + ...__snapshot_test_verification_states-3.snap | 9 ++ ...__snapshot_test_verification_states-4.snap | 9 ++ ...__snapshot_test_verification_states-5.snap | 5 + ...ts__snapshot_test_verification_states.snap | 7 + 28 files changed, 383 insertions(+), 6 deletions(-) create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_algorithm_info.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_encryption_info.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-2.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-3.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-4.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-5.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-6.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-2.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-3.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_sync_timeline_event.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-2.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-3.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-4.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-5.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-2.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-3.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-4.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-5.snap create mode 100644 crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states.snap diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 881b5010dfe..1c6e6a8981a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,9 @@ concurrency: env: CARGO_TERM_COLOR: always + # Insta.rs is run directly via cargo test. We don't want insta.rs to create new snapshots files. + # Just want it to run the tests (option `no` instead of `auto`). + INSTA_UPDATE: no jobs: xtask: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 82a802cffe0..6a7385e4319 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,33 @@ integration tests that need a running synapse instance. These tests reside in synapse for testing purposes. +### Snapshot Testing + +You can add/review snapshot tests using [insta.rs](https://insta.rs) + +Every new struct/enum that derives `Serialize` `Deserialise` should have a snapshot test for it. +Any code change that breaks serialisation will then break a test, the author will then have to decide +how to handle migration and test it if needed. + + +And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool: + +Unix: +``` +curl -LsSf https://insta.rs/install.sh | sh +``` + +Windows: +``` +powershell -c "irm https://insta.rs/install.ps1 | iex" +``` + +Usual flow is to first run the test, then review them. +``` +cargo insta test +cargo insta review +``` + ## Pull requests Ideally, a PR should have a *proper title*, with *atomic logical commits*, and diff --git a/Cargo.lock b/Cargo.lock index f4f968755e7..f8d57bef3a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2595,6 +2595,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", +] + [[package]] name = "instant" version = "0.1.13" @@ -2789,6 +2802,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -3170,6 +3189,7 @@ dependencies = [ "getrandom", "gloo-timers", "imbl", + "insta", "js-sys", "matrix-sdk-test-macros", "proptest", diff --git a/Cargo.toml b/Cargo.toml index 5882bd8bd74..a1aa363abd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ hmac = "0.12.1" http = "1.1.0" imbl = "3.0.0" indexmap = "2.6.0" +insta = { version = "1.41.1", features = ["json"] } itertools = "0.13.0" js-sys = "0.3.69" mime = "0.3.17" @@ -124,6 +125,9 @@ debug = 0 # for the extra time of optimizing it for a clean build of matrix-sdk-ffi. quote = { opt-level = 2 } sha2 = { opt-level = 2 } +# faster runs for insta.rs snapshot testing +insta.opt-level = 3 +similar.opt-level = 3 # Custom profile with full debugging info, use `--profile dbg` to select [profile.dbg] diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 2aef55b7efd..6b7fdb0eb9d 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -46,6 +46,7 @@ assert_matches = { workspace = true } proptest = { workspace = true } matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" } wasm-bindgen-test = { workspace = true } +insta = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Enable the test macro. diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index e9fb73f94fb..0833cf3f4e9 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -909,21 +909,22 @@ mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; + use insta::{assert_json_snapshot, with_settings}; use ruma::{ - event_id, + device_id, event_id, events::{room::message::RoomMessageEventContent, AnySyncTimelineEvent}, serde::Raw, - user_id, + user_id, DeviceKeyAlgorithm, }; use serde::Deserialize; use serde_json::json; use super::{ - AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, - TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, - UnsignedEventLocation, VerificationState, WithheldCode, + AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, ShieldState, + ShieldStateCode, SyncTimelineEvent, TimelineEvent, TimelineEventKind, UnableToDecryptInfo, + UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, + VerificationState, WithheldCode, }; - use crate::deserialized_responses::{DeviceLinkProblem, ShieldStateCode, VerificationLevel}; fn example_event() -> serde_json::Value { json!({ @@ -1317,4 +1318,129 @@ mod tests { let reason = UnableToDecryptReason::UnknownMegolmMessageIndex; assert!(reason.is_missing_room_key()); } + + #[test] + fn snapshot_test_verification_level() { + assert_json_snapshot!(VerificationLevel::VerificationViolation); + assert_json_snapshot!(VerificationLevel::UnsignedDevice); + assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::InsecureSource)); + assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::MissingDevice)); + assert_json_snapshot!(VerificationLevel::UnverifiedIdentity); + } + + #[test] + fn snapshot_test_verification_states() { + assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::UnsignedDevice)); + assert_json_snapshot!(VerificationState::Unverified( + VerificationLevel::VerificationViolation + )); + assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None( + DeviceLinkProblem::InsecureSource, + ))); + assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None( + DeviceLinkProblem::MissingDevice, + ))); + assert_json_snapshot!(VerificationState::Verified); + } + + #[test] + fn snapshot_test_shield_states() { + assert_json_snapshot!(ShieldState::None); + assert_json_snapshot!(ShieldState::Red { + code: ShieldStateCode::UnverifiedIdentity, + message: "a message" + }); + assert_json_snapshot!(ShieldState::Grey { + code: ShieldStateCode::AuthenticityNotGuaranteed, + message: "authenticity of this message cannot be guaranteed", + }); + } + + #[test] + fn snapshot_test_shield_codes() { + assert_json_snapshot!(ShieldStateCode::AuthenticityNotGuaranteed); + assert_json_snapshot!(ShieldStateCode::UnknownDevice); + assert_json_snapshot!(ShieldStateCode::UnsignedDevice); + assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity); + assert_json_snapshot!(ShieldStateCode::SentInClear); + assert_json_snapshot!(ShieldStateCode::VerificationViolation); + } + + #[test] + fn snapshot_test_algorithm_info() { + let mut map = BTreeMap::new(); + map.insert(DeviceKeyAlgorithm::Curve25519, "claimedclaimedcurve25519".to_owned()); + map.insert(DeviceKeyAlgorithm::Ed25519, "claimedclaimeded25519".to_owned()); + let info = AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "curvecurvecurve".into(), + sender_claimed_keys: BTreeMap::from([ + (DeviceKeyAlgorithm::Curve25519, "claimedclaimedcurve25519".to_owned()), + (DeviceKeyAlgorithm::Ed25519, "claimedclaimeded25519".to_owned()), + ]), + }; + + assert_json_snapshot!(info) + } + + #[test] + fn snapshot_test_encryption_info() { + let info = EncryptionInfo { + sender: user_id!("@alice:localhost").to_owned(), + sender_device: Some(device_id!("ABCDEFGH").to_owned()), + algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "curvecurvecurve".into(), + sender_claimed_keys: Default::default(), + }, + verification_state: VerificationState::Verified, + }; + + with_settings!({sort_maps =>true}, { + assert_json_snapshot!(info) + }) + } + + #[test] + fn snapshot_test_sync_timeline_event() { + let room_event = SyncTimelineEvent { + kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { + event: Raw::new(&example_event()).unwrap().cast(), + encryption_info: EncryptionInfo { + sender: user_id!("@sender:example.com").to_owned(), + sender_device: Some(device_id!("ABCDEFGHIJ").to_owned()), + algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "xxx".to_owned(), + sender_claimed_keys: BTreeMap::from([ + ( + DeviceKeyAlgorithm::Ed25519, + "I3YsPwqMZQXHkSQbjFNEs7b529uac2xBpI83eN3LUXo".to_owned(), + ), + ( + DeviceKeyAlgorithm::Curve25519, + "qzdW3F5IMPFl0HQgz5w/L5Oi/npKUFn8Um84acIHfPY".to_owned(), + ), + ]), + }, + verification_state: VerificationState::Verified, + }, + unsigned_encryption_info: Some(BTreeMap::from([( + UnsignedEventLocation::RelationsThreadLatestEvent, + UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { + session_id: Some("xyz".to_owned()), + reason: UnableToDecryptReason::MissingMegolmSession { + withheld_code: Some(WithheldCode::Unverified), + }, + }), + )])), + }), + push_actions: Default::default(), + }; + + with_settings!({sort_maps =>true}, { + // We use directly the serde_json formatter here, because of a bug in insta + // not serializing custom BTreeMap key enum https://github.com/mitsuhiko/insta/issues/689 + assert_json_snapshot! { + serde_json::to_value(&room_event).unwrap(), + } + }); + } } diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_algorithm_info.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_algorithm_info.snap new file mode 100644 index 00000000000..94277ee0deb --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_algorithm_info.snap @@ -0,0 +1,13 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: info +--- +{ + "MegolmV1AesSha2": { + "curve25519_key": "curvecurvecurve", + "sender_claimed_keys": { + "ed25519": "claimedclaimeded25519", + "curve25519": "claimedclaimedcurve25519" + } + } +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_encryption_info.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_encryption_info.snap new file mode 100644 index 00000000000..96e5c2b2765 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_encryption_info.snap @@ -0,0 +1,15 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: info +--- +{ + "sender": "@alice:localhost", + "sender_device": "ABCDEFGH", + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "curvecurvecurve", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified" +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-2.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-2.snap new file mode 100644 index 00000000000..9b4668eadc4 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&code).unwrap()" +--- +"UnknownDevice" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-3.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-3.snap new file mode 100644 index 00000000000..d9719968ddb --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-3.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&code).unwrap()" +--- +"UnsignedDevice" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-4.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-4.snap new file mode 100644 index 00000000000..2962f08e5e1 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-4.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&code).unwrap()" +--- +"UnverifiedIdentity" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-5.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-5.snap new file mode 100644 index 00000000000..f547dd10e90 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-5.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&code).unwrap()" +--- +"SentInClear" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-6.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-6.snap new file mode 100644 index 00000000000..fddf47c7d44 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes-6.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&code).unwrap()" +--- +"VerificationViolation" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes.snap new file mode 100644 index 00000000000..ad123c7c7bf --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_codes.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&code).unwrap()" +--- +"AuthenticityNotGuaranteed" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-2.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-2.snap new file mode 100644 index 00000000000..e399c209db8 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +{ + "Red": { + "code": "UnverifiedIdentity", + "message": "a message" + } +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-3.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-3.snap new file mode 100644 index 00000000000..e3f80a05261 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states-3.snap @@ -0,0 +1,10 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +{ + "Grey": { + "code": "AuthenticityNotGuaranteed", + "message": "authenticity of this message cannot be guaranteed" + } +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states.snap new file mode 100644 index 00000000000..24a9120e3ac --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_shield_states.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +"None" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_sync_timeline_event.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_sync_timeline_event.snap new file mode 100644 index 00000000000..f40882141c1 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_sync_timeline_event.snap @@ -0,0 +1,47 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&room_event).unwrap()" +--- +{ + "kind": { + "Decrypted": { + "encryption_info": { + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": { + "curve25519": "qzdW3F5IMPFl0HQgz5w/L5Oi/npKUFn8Um84acIHfPY", + "ed25519": "I3YsPwqMZQXHkSQbjFNEs7b529uac2xBpI83eN3LUXo" + } + } + }, + "sender": "@sender:example.com", + "sender_device": "ABCDEFGHIJ", + "verification_state": "Verified" + }, + "event": { + "content": { + "body": "secret", + "msgtype": "m.text" + }, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message" + }, + "unsigned_encryption_info": { + "RelationsThreadLatestEvent": { + "UnableToDecrypt": { + "reason": { + "MissingMegolmSession": { + "withheld_code": "m.unverified" + } + }, + "session_id": "xyz" + } + } + } + } + } +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-2.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-2.snap new file mode 100644 index 00000000000..fc816009b11 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&level).unwrap()" +--- +"UnsignedDevice" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-3.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-3.snap new file mode 100644 index 00000000000..2d27ef8c55d --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-3.snap @@ -0,0 +1,7 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&level).unwrap()" +--- +{ + "None": "InsecureSource" +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-4.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-4.snap new file mode 100644 index 00000000000..180ef3de9cb --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-4.snap @@ -0,0 +1,7 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&level).unwrap()" +--- +{ + "None": "MissingDevice" +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-5.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-5.snap new file mode 100644 index 00000000000..540df71e467 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level-5.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&level).unwrap()" +--- +"UnverifiedIdentity" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level.snap new file mode 100644 index 00000000000..e68abafa23a --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_level.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&level).unwrap()" +--- +"VerificationViolation" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-2.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-2.snap new file mode 100644 index 00000000000..716d8ff1374 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +{ + "Unverified": "VerificationViolation" +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-3.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-3.snap new file mode 100644 index 00000000000..fae5d49deb1 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-3.snap @@ -0,0 +1,9 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +{ + "Unverified": { + "None": "InsecureSource" + } +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-4.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-4.snap new file mode 100644 index 00000000000..a721a9e0a9e --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-4.snap @@ -0,0 +1,9 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +{ + "Unverified": { + "None": "MissingDevice" + } +} diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-5.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-5.snap new file mode 100644 index 00000000000..6a8559c1f7c --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states-5.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +"Verified" diff --git a/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states.snap b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states.snap new file mode 100644 index 00000000000..80c229057d8 --- /dev/null +++ b/crates/matrix-sdk-common/src/snapshots/matrix_sdk_common__deserialized_responses__tests__snapshot_test_verification_states.snap @@ -0,0 +1,7 @@ +--- +source: crates/matrix-sdk-common/src/deserialized_responses.rs +expression: "serde_json::to_value(&state).unwrap()" +--- +{ + "Unverified": "UnsignedDevice" +} From 5f5e979e16bbe3c44e9e71ff383f53c6d2895247 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 19 Dec 2024 17:05:13 +0100 Subject: [PATCH 834/979] refactor!: Put the RequestConfig argument of Client::send() into a builder method Instead of `Client::send(request, request_config)`, consumers can now do `Client::send(request).with_request_config(request_config)`. --- bindings/matrix-sdk-ffi/src/room.rs | 15 ++--- crates/matrix-sdk/CHANGELOG.md | 7 ++ crates/matrix-sdk/src/account.rs | 34 +++++----- crates/matrix-sdk/src/client/futures.rs | 7 ++ crates/matrix-sdk/src/client/mod.rs | 42 ++++++------ .../matrix-sdk/src/encryption/backups/mod.rs | 14 ++-- .../src/encryption/identities/devices.rs | 2 +- .../src/encryption/identities/users.rs | 2 +- crates/matrix-sdk/src/encryption/mod.rs | 24 +++---- .../src/encryption/verification/sas.rs | 2 +- .../src/matrix_auth/login_builder.rs | 3 +- crates/matrix-sdk/src/matrix_auth/mod.rs | 6 +- crates/matrix-sdk/src/media.rs | 18 ++--- .../src/notification_settings/mod.rs | 53 +++++++++------ crates/matrix-sdk/src/oidc/mod.rs | 3 +- crates/matrix-sdk/src/pusher.rs | 4 +- crates/matrix-sdk/src/room/futures.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 67 +++++++++---------- crates/matrix-sdk/src/room_preview.rs | 6 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 10 +-- crates/matrix-sdk/src/test_utils/mocks.rs | 4 +- crates/matrix-sdk/src/widget/matrix.rs | 8 +-- examples/get_profiles/src/main.rs | 2 +- 23 files changed, 178 insertions(+), 157 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 3cef3ca8e67..e76395b381e 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -449,15 +449,12 @@ impl Room { let int_score = score.map(|value| value.into()); self.inner .client() - .send( - report_content::v3::Request::new( - self.inner.room_id().into(), - event_id, - int_score, - reason, - ), - None, - ) + .send(report_content::v3::Request::new( + self.inner.room_id().into(), + event_id, + int_score, + reason, + )) .await?; Ok(()) } diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 2906a2568b6..ba1803f3daa 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Refactor + +- [**breaking**] Move the optional `RequestConfig` argument of the + `Client::send()` method to the `with_request_config()` builder method. You + should call `Client::send(request).with_request_config(request_config).awat` + now instead. + ## [0.9.0] - 2024-12-18 ### Bug Fixes diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index b077d610641..c591c7f8fb3 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -91,7 +91,7 @@ impl Account { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; let request = get_display_name::v3::Request::new(user_id.to_owned()); let request_config = self.client.request_config().force_auth(); - let response = self.client.send(request, Some(request_config)).await?; + let response = self.client.send(request).with_request_config(request_config).await?; Ok(response.displayname) } @@ -115,7 +115,7 @@ impl Account { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; let request = set_display_name::v3::Request::new(user_id.to_owned(), name.map(ToOwned::to_owned)); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -147,7 +147,7 @@ impl Account { let config = Some(RequestConfig::new().force_auth()); - let response = self.client.send(request, config).await?; + let response = self.client.send(request).with_request_config(config).await?; if let Some(url) = response.avatar_url.clone() { // If an avatar is found cache it. let _ = self @@ -181,7 +181,7 @@ impl Account { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; let request = set_avatar_url::v3::Request::new(user_id.to_owned(), url.map(ToOwned::to_owned)); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -291,7 +291,11 @@ impl Account { user_id: &UserId, ) -> Result { let request = get_profile::v3::Request::new(user_id.to_owned()); - Ok(self.client.send(request, Some(RequestConfig::short_retry().force_auth())).await?) + Ok(self + .client + .send(request) + .with_request_config(RequestConfig::short_retry().force_auth()) + .await?) } /// Change the password of the account. @@ -344,7 +348,7 @@ impl Account { let request = assign!(change_password::v3::Request::new(new_password.to_owned()), { auth: auth_data, }); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Deactivate this account definitively. @@ -398,7 +402,7 @@ impl Account { auth: auth_data, erase: erase_data, }); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Get the registered [Third Party Identifiers][3pid] on the homeserver of @@ -428,7 +432,7 @@ impl Account { /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types pub async fn get_3pids(&self) -> Result { let request = get_3pids::v3::Request::new(); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Request a token to validate an email address as a [Third Party @@ -499,7 +503,7 @@ impl Account { email.to_owned(), send_attempt, ); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Request a token to validate a phone number as a [Third Party @@ -576,7 +580,7 @@ impl Account { phone_number.to_owned(), send_attempt, ); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Add a [Third Party Identifier][3pid] on the homeserver for this @@ -619,7 +623,7 @@ impl Account { assign!(add_3pid::v3::Request::new(client_secret.to_owned(), sid.to_owned()), { auth: auth_data }); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Delete a [Third Party Identifier][3pid] from the homeserver for this @@ -679,7 +683,7 @@ impl Account { let request = assign!(delete_3pid::v3::Request::new(medium, address.to_owned()), { id_server: id_server.map(ToOwned::to_owned), }); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Get the content of an account data event of statically-known type. @@ -749,7 +753,7 @@ impl Account { let request = get_global_account_data::v3::Request::new(own_user.to_owned(), event_type); - match self.client.send(request, None).await { + match self.client.send(request).await { Ok(r) => Ok(Some(r.account_data)), Err(e) => { if let Some(kind) = e.client_api_error_kind() { @@ -802,7 +806,7 @@ impl Account { let request = set_global_account_data::v3::Request::new(own_user.to_owned(), &content)?; - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Set the given raw account data event. @@ -816,7 +820,7 @@ impl Account { let request = set_global_account_data::v3::Request::new_raw(own_user.to_owned(), event_type, content); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Marks the room identified by `room_id` as a "direct chat" with each diff --git a/crates/matrix-sdk/src/client/futures.rs b/crates/matrix-sdk/src/client/futures.rs index 6f477a2cb58..da5d8d8764b 100644 --- a/crates/matrix-sdk/src/client/futures.rs +++ b/crates/matrix-sdk/src/client/futures.rs @@ -77,6 +77,13 @@ impl SendRequest { self } + /// Use the given [`RequestConfig`] for this send request, instead of the + /// one provided by default. + pub fn with_request_config(mut self, request_config: impl Into>) -> Self { + self.config = request_config.into(); + self + } + /// Get a subscriber to observe the progress of sending the request /// body. #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 2f70e6b8822..c0802de757e 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -480,7 +480,7 @@ impl Client { /// # anyhow::Ok(()) }; /// ``` pub async fn get_capabilities(&self) -> HttpResult { - let res = self.send(get_capabilities::v3::Request::new(), None).await?; + let res = self.send(get_capabilities::v3::Request::new()).await?; Ok(res.capabilities) } @@ -563,7 +563,7 @@ impl Client { request.limit = limit; } - self.send(request, None).await + self.send(request).await } /// Get the user id of the current owner of the client. @@ -1186,7 +1186,7 @@ impl Client { room_alias: &RoomAliasId, ) -> HttpResult { let request = get_alias::v3::Request::new(room_alias.to_owned()); - self.send(request, None).await + self.send(request).await } /// Checks if a room alias is not in use yet. @@ -1213,7 +1213,7 @@ impl Client { /// Creates a new room alias associated with a room. pub async fn create_room_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> HttpResult<()> { let request = create_alias::v3::Request::new(alias.to_owned(), room_id.to_owned()); - self.send(request, None).await?; + self.send(request).await?; Ok(()) } @@ -1351,7 +1351,7 @@ impl Client { debug!("Didn't find filter locally"); let user_id = self.user_id().ok_or(Error::AuthenticationRequired)?; let request = FilterUploadRequest::new(user_id.to_owned(), definition); - let response = self.send(request, None).await?; + let response = self.send(request).await?; self.inner.base_client.receive_filter_upload(filter_name, &response).await?; @@ -1369,7 +1369,7 @@ impl Client { /// * `room_id` - The `RoomId` of the room to be joined. pub async fn join_room_by_id(&self, room_id: &RoomId) -> Result { let request = join_room_by_id::v3::Request::new(room_id.to_owned()); - let response = self.send(request, None).await?; + let response = self.send(request).await?; let base_room = self.base_client().room_joined(&response.room_id).await?; Ok(Room::new(self.clone(), base_room)) } @@ -1391,7 +1391,7 @@ impl Client { let request = assign!(join_room_by_id_or_alias::v3::Request::new(alias.to_owned()), { via: server_names.to_owned(), }); - let response = self.send(request, None).await?; + let response = self.send(request).await?; let base_room = self.base_client().room_joined(&response.room_id).await?; Ok(Room::new(self.clone(), base_room)) } @@ -1438,7 +1438,7 @@ impl Client { since: since.map(ToOwned::to_owned), server: server.map(ToOwned::to_owned), }); - self.send(request, None).await + self.send(request).await } /// Create a room with the given parameters. @@ -1473,7 +1473,7 @@ impl Client { pub async fn create_room(&self, request: create_room::v3::Request) -> Result { let invite = request.invite.clone(); let is_direct_room = request.is_direct; - let response = self.send(request, None).await?; + let response = self.send(request).await?; let base_room = self.base_client().get_or_create_room(&response.room_id, RoomState::Joined); @@ -1557,7 +1557,7 @@ impl Client { &self, request: get_public_rooms_filtered::v3::Request, ) -> HttpResult { - self.send(request, None).await + self.send(request).await } /// Send an arbitrary request to the server, without updating client state. @@ -1592,17 +1592,13 @@ impl Client { /// let request = profile::get_profile::v3::Request::new(user_id); /// /// // Start the request using Client::send() - /// let response = client.send(request, None).await?; + /// let response = client.send(request).await?; /// /// // Check the corresponding Response struct to find out what types are /// // returned /// # anyhow::Ok(()) }; /// ``` - pub fn send( - &self, - request: Request, - config: Option, - ) -> SendRequest + pub fn send(&self, request: Request) -> SendRequest where Request: OutgoingRequest + Clone + Debug, HttpError: From>, @@ -1610,7 +1606,7 @@ impl Client { SendRequest { client: self.clone(), request, - config, + config: None, send_progress: Default::default(), homeserver_override: None, } @@ -1828,7 +1824,7 @@ impl Client { pub async fn devices(&self) -> HttpResult { let request = get_devices::v3::Request::new(); - self.send(request, None).await + self.send(request).await } /// Delete the given devices from the server. @@ -1879,7 +1875,7 @@ impl Client { let mut request = delete_devices::v3::Request::new(devices.to_owned()); request.auth = auth_data; - self.send(request, None).await + self.send(request).await } /// Change the display name of a device owned by the current user. @@ -1899,7 +1895,7 @@ impl Client { let mut request = update_device::v3::Request::new(device_id.to_owned()); request.display_name = Some(display_name.to_owned()); - self.send(request, None).await + self.send(request).await } /// Synchronize the client's state with the latest state on the server. @@ -2023,7 +2019,7 @@ impl Client { request_config.timeout += timeout; } - let response = self.send(request, Some(request_config)).await?; + let response = self.send(request).with_request_config(request_config).await?; let next_batch = response.next_batch.clone(); let response = self.process_sync(response).await?; @@ -2340,7 +2336,7 @@ impl Client { /// Gets information about the owner of a given access token. pub async fn whoami(&self) -> HttpResult { let request = whoami::v3::Request::new(); - self.send(request, None).await + self.send(request).await } /// Subscribes a new receiver to client SessionChange broadcasts. @@ -2451,7 +2447,7 @@ impl Client { ) -> Result { let request = assign!(knock_room::v3::Request::new(room_id_or_alias), { reason, via: server_names }); - let response = self.send(request, None).await?; + let response = self.send(request).await?; let base_room = self.inner.base_client.room_knocked(&response.room_id).await?; Ok(Room::new(self.clone(), base_room)) } diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index f7b6bc921bc..ba1e99f06dc 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -136,7 +136,7 @@ impl Backups { let algorithm = Raw::new(&backup_info)?.cast(); let request = create_backup_version::v3::Request::new(algorithm); - let response = self.client.send(request, Default::default()).await?; + let response = self.client.send(request).await?; let version = response.version; // Reset any state we might have had before the new backup was created. @@ -454,7 +454,7 @@ impl Backups { if let Some(version) = backup_keys.backup_version { let request = get_backup_keys_for_room::v3::Request::new(version.clone(), room_id.to_owned()); - let response = self.client.send(request, Default::default()).await?; + let response = self.client.send(request).await?; // Transform response to standard format (map of room ID -> room key). let response = get_backup_keys::v3::Response::new(BTreeMap::from([( @@ -493,7 +493,7 @@ impl Backups { room_id.to_owned(), session_id.to_owned(), ); - let response = self.client.send(request, Default::default()).await?; + let response = self.client.send(request).await?; // Transform response to standard format (map of room ID -> room key). let response = get_backup_keys::v3::Response::new(BTreeMap::from([( @@ -604,7 +604,7 @@ impl Backups { version: String, ) -> Result<(), Error> { let request = get_backup_keys::v3::Request::new(version.clone()); - let response = self.client.send(request, Default::default()).await?; + let response = self.client.send(request).await?; let olm_machine = self.client.olm_machine().await; let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?; @@ -626,7 +626,7 @@ impl Backups { ) -> Result, Error> { let request = get_latest_backup_info::v3::Request::new(); - match self.client.send(request, None).await { + match self.client.send(request).await { Ok(r) => Ok(Some(r)), Err(e) => { if let Some(kind) = e.client_api_error_kind() { @@ -645,7 +645,7 @@ impl Backups { async fn delete_backup_from_server(&self, version: String) -> Result<(), Error> { let request = ruma::api::client::backup::delete_backup_version::v3::Request::new(version); - let ret = match self.client.send(request, Default::default()).await { + let ret = match self.client.send(request).await { Ok(_) => Ok(()), Err(e) => { if let Some(kind) = e.client_api_error_kind() { @@ -679,7 +679,7 @@ impl Backups { let add_backup_keys = add_backup_keys::v3::Request::new(request.version, request.rooms); - match self.client.send(add_backup_keys, Default::default()).await { + match self.client.send(add_backup_keys).await { Ok(response) => { olm_machine.mark_request_as_sent(request_id, &response).await?; diff --git a/crates/matrix-sdk/src/encryption/identities/devices.rs b/crates/matrix-sdk/src/encryption/identities/devices.rs index 947b3675101..6c1dcc7bdf4 100644 --- a/crates/matrix-sdk/src/encryption/identities/devices.rs +++ b/crates/matrix-sdk/src/encryption/identities/devices.rs @@ -290,7 +290,7 @@ impl Device { /// ``` pub async fn verify(&self) -> Result<(), ManualVerifyError> { let request = self.inner.verify().await?; - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index 0ce4e303e5d..0edeae8b997 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -366,7 +366,7 @@ impl UserIdentity { CryptoUserIdentity::Other(identity) => identity.verify().await?, }; - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 2344fbc7779..4c730711189 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -281,7 +281,7 @@ impl CrossSigningResetHandle { let mut upload_request = self.upload_request.clone(); upload_request.auth = auth; - while let Err(e) = self.client.send(upload_request.clone(), None).await { + while let Err(e) = self.client.send(upload_request.clone()).await { if *self.is_cancelled.lock().await { return Ok(()); } @@ -291,7 +291,7 @@ impl CrossSigningResetHandle { } } - self.client.send(self.signatures_request.clone(), None).await?; + self.client.send(self.signatures_request.clone()).await?; Ok(()) } @@ -411,7 +411,7 @@ impl Client { ) -> Result { let request = assign!(get_keys::v3::Request::new(), { device_keys }); - let response = self.send(request, None).await?; + let response = self.send(request).await?; self.mark_request_as_sent(request_id, &response).await?; self.encryption().update_state_after_keys_query(&response).await; @@ -523,7 +523,7 @@ impl Client { .get_missing_sessions(users) .await? { - let response = self.send(request, None).await?; + let response = self.send(request).await?; self.mark_request_as_sent(&request_id, &response).await?; } @@ -551,7 +551,7 @@ impl Client { "Uploading public encryption keys", ); - let response = self.send(request.clone(), None).await?; + let response = self.send(request.clone()).await?; self.mark_request_as_sent(request_id, &response).await?; Ok(response) @@ -582,7 +582,7 @@ impl Client { request.messages.clone(), ); - self.send(request, None).await + self.send(request).await } pub(crate) async fn send_verification_request( @@ -632,7 +632,7 @@ impl Client { self.mark_request_as_sent(r.request_id(), &response).await?; } AnyOutgoingRequest::SignatureUpload(request) => { - let response = self.send(request.clone(), None).await?; + let response = self.send(request.clone()).await?; self.mark_request_as_sent(r.request_id(), &response).await?; } AnyOutgoingRequest::RoomMessage(request) => { @@ -640,7 +640,7 @@ impl Client { self.mark_request_as_sent(r.request_id(), &response).await?; } AnyOutgoingRequest::KeysClaim(request) => { - let response = self.send(request.clone(), None).await?; + let response = self.send(request.clone()).await?; self.mark_request_as_sent(r.request_id(), &response).await?; } } @@ -1146,8 +1146,8 @@ impl Encryption { if let Some(req) = upload_keys_req { self.client.send_outgoing_request(req).await?; } - self.client.send(upload_signing_keys_req, None).await?; - self.client.send(upload_signatures_req, None).await?; + self.client.send(upload_signing_keys_req).await?; + self.client.send(upload_signatures_req).await?; Ok(()) } @@ -1209,7 +1209,7 @@ impl Encryption { self.client.send_outgoing_request(req).await?; } - if let Err(error) = self.client.send(upload_signing_keys_req.clone(), None).await { + if let Err(error) = self.client.send(upload_signing_keys_req.clone()).await { if let Some(auth_type) = CrossSigningResetAuthType::new(&self.client, &error).await? { let client = self.client.clone(); @@ -1223,7 +1223,7 @@ impl Encryption { Err(error.into()) } } else { - self.client.send(upload_signatures_req, None).await?; + self.client.send(upload_signatures_req).await?; Ok(None) } diff --git a/crates/matrix-sdk/src/encryption/verification/sas.rs b/crates/matrix-sdk/src/encryption/verification/sas.rs index 9b9bb7f8dc3..7b8f1df7a08 100644 --- a/crates/matrix-sdk/src/encryption/verification/sas.rs +++ b/crates/matrix-sdk/src/encryption/verification/sas.rs @@ -86,7 +86,7 @@ impl SasVerification { } if let Some(s) = signature { - self.client.send(s, None).await?; + self.client.send(s).await?; } Ok(()) diff --git a/crates/matrix-sdk/src/matrix_auth/login_builder.rs b/crates/matrix-sdk/src/matrix_auth/login_builder.rs index b92393b7545..a263af4346f 100644 --- a/crates/matrix-sdk/src/matrix_auth/login_builder.rs +++ b/crates/matrix-sdk/src/matrix_auth/login_builder.rs @@ -182,7 +182,8 @@ impl LoginBuilder { refresh_token: self.request_refresh_token, }); - let response = client.send(request, Some(RequestConfig::short_retry())).await?; + let response = + client.send(request).with_request_config(RequestConfig::short_retry()).await?; self.auth .receive_login_response( &response, diff --git a/crates/matrix-sdk/src/matrix_auth/mod.rs b/crates/matrix-sdk/src/matrix_auth/mod.rs index 901f87a37b2..8beb76de189 100644 --- a/crates/matrix-sdk/src/matrix_auth/mod.rs +++ b/crates/matrix-sdk/src/matrix_auth/mod.rs @@ -98,7 +98,7 @@ impl MatrixAuth { /// appropriate method for the next step. pub async fn get_login_types(&self) -> HttpResult { let request = get_login_types::v3::Request::new(); - self.client.send(request, None).await + self.client.send(request).await } /// Get the URL to use to log in via Single Sign-On. @@ -617,7 +617,7 @@ impl MatrixAuth { _ => None, }; - let response = self.client.send(request, None).await?; + let response = self.client.send(request).await?; if let Some(session) = MatrixSession::from_register_response(&response) { let _ = self .set_session( @@ -632,7 +632,7 @@ impl MatrixAuth { /// Log out the current user. pub async fn logout(&self) -> HttpResult { let request = logout::v3::Request::new(); - self.client.send(request, None).await + self.client.send(request).await } /// Get the current access token and optional refresh token for this diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index 137756ff561..008c15f93b8 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -196,7 +196,7 @@ impl Media { content_type: Some(content_type.essence_str().to_owned()), }); - self.client.send(request, Some(request_config)) + self.client.send(request).with_request_config(request_config) } /// Returns a reasonable upload timeout for an upload, based on the size of @@ -240,7 +240,7 @@ impl Media { // Note: this request doesn't have any parameters. let request = media::create_mxc_uri::v1::Request::default(); - let response = self.client.send(request, None).await?; + let response = self.client.send(request).await?; Ok(PreallocatedMxcUri { uri: response.content_uri, @@ -278,7 +278,7 @@ impl Media { let request_config = self.client.request_config().timeout(timeout); - if let Err(err) = self.client.send(request, Some(request_config)).await { + if let Err(err) = self.client.send(request).with_request_config(request_config).await { match err.client_api_error_kind() { Some(ErrorKind::CannotOverwriteMedia) => { Err(Error::Media(MediaError::CannotOverwriteMedia)) @@ -446,11 +446,11 @@ impl Media { let content = if use_auth { let request = authenticated_media::get_content::v1::Request::from_uri(&file.url)?; - self.client.send(request, request_config).await?.file + self.client.send(request).with_request_config(request_config).await?.file } else { #[allow(deprecated)] let request = media::get_content::v3::Request::from_url(&file.url)?; - self.client.send(request, None).await?.file + self.client.send(request).await?.file }; #[cfg(feature = "e2e-encryption")] @@ -486,7 +486,7 @@ impl Media { request.method = Some(settings.method.clone()); request.animated = Some(settings.animated); - self.client.send(request, request_config).await?.file + self.client.send(request).with_request_config(request_config).await?.file } else { #[allow(deprecated)] let request = { @@ -500,15 +500,15 @@ impl Media { request }; - self.client.send(request, None).await?.file + self.client.send(request).await?.file } } else if use_auth { let request = authenticated_media::get_content::v1::Request::from_uri(uri)?; - self.client.send(request, request_config).await?.file + self.client.send(request).with_request_config(request_config).await?.file } else { #[allow(deprecated)] let request = media::get_content::v3::Request::from_url(uri)?; - self.client.send(request, None).await?.file + self.client.send(request).await?.file } } }; diff --git a/crates/matrix-sdk/src/notification_settings/mod.rs b/crates/matrix-sdk/src/notification_settings/mod.rs index 58a89f2c03e..dfb036bf1cd 100644 --- a/crates/matrix-sdk/src/notification_settings/mod.rs +++ b/crates/matrix-sdk/src/notification_settings/mod.rs @@ -453,32 +453,39 @@ impl NotificationSettings { match command { Command::DeletePushRule { kind, rule_id } => { let request = delete_pushrule::v3::Request::new(kind.clone(), rule_id.clone()); - self.client.send(request, request_config).await.map_err(|error| { - error!("Unable to delete {kind} push rule `{rule_id}`: {error}"); - NotificationSettingsError::UnableToRemovePushRule - })?; + self.client.send(request).with_request_config(request_config).await.map_err( + |error| { + error!("Unable to delete {kind} push rule `{rule_id}`: {error}"); + NotificationSettingsError::UnableToRemovePushRule + }, + )?; } Command::SetRoomPushRule { room_id, notify: _ } => { let push_rule = command.to_push_rule()?; let request = set_pushrule::v3::Request::new(push_rule); - self.client.send(request, request_config).await.map_err(|error| { - error!("Unable to set room push rule `{room_id}`: {error}"); - NotificationSettingsError::UnableToAddPushRule - })?; + self.client.send(request).with_request_config(request_config).await.map_err( + |error| { + error!("Unable to set room push rule `{room_id}`: {error}"); + NotificationSettingsError::UnableToAddPushRule + }, + )?; } Command::SetOverridePushRule { rule_id, room_id: _, notify: _ } => { let push_rule = command.to_push_rule()?; let request = set_pushrule::v3::Request::new(push_rule); - self.client.send(request, request_config).await.map_err(|error| { - error!("Unable to set override push rule `{rule_id}`: {error}"); - NotificationSettingsError::UnableToAddPushRule - })?; + self.client.send(request).with_request_config(request_config).await.map_err( + |error| { + error!("Unable to set override push rule `{rule_id}`: {error}"); + NotificationSettingsError::UnableToAddPushRule + }, + )?; } Command::SetKeywordPushRule { keyword: _ } => { let push_rule = command.to_push_rule()?; let request = set_pushrule::v3::Request::new(push_rule); self.client - .send(request, request_config) + .send(request) + .with_request_config(request_config) .await .map_err(|_| NotificationSettingsError::UnableToAddPushRule)?; } @@ -488,10 +495,12 @@ impl NotificationSettings { rule_id.clone(), *enabled, ); - self.client.send(request, request_config).await.map_err(|error| { - error!("Unable to set {kind} push rule `{rule_id}` enabled: {error}"); - NotificationSettingsError::UnableToUpdatePushRule - })?; + self.client.send(request).with_request_config(request_config).await.map_err( + |error| { + error!("Unable to set {kind} push rule `{rule_id}` enabled: {error}"); + NotificationSettingsError::UnableToUpdatePushRule + }, + )?; } Command::SetPushRuleActions { kind, rule_id, actions } => { let request = set_pushrule_actions::v3::Request::new( @@ -499,10 +508,12 @@ impl NotificationSettings { rule_id.clone(), actions.clone(), ); - self.client.send(request, request_config).await.map_err(|error| { - error!("Unable to set {kind} push rule `{rule_id}` actions: {error}"); - NotificationSettingsError::UnableToUpdatePushRule - })?; + self.client.send(request).with_request_config(request_config).await.map_err( + |error| { + error!("Unable to set {kind} push rule `{rule_id}` actions: {error}"); + NotificationSettingsError::UnableToUpdatePushRule + }, + )?; } } } diff --git a/crates/matrix-sdk/src/oidc/mod.rs b/crates/matrix-sdk/src/oidc/mod.rs index 7f87bd34713..4c20e085153 100644 --- a/crates/matrix-sdk/src/oidc/mod.rs +++ b/crates/matrix-sdk/src/oidc/mod.rs @@ -349,8 +349,7 @@ impl Oidc { /// /// [MSC3861]: https://github.com/matrix-org/matrix-spec-proposals/pull/3861 pub async fn fetch_authentication_issuer(&self) -> Result { - let response = - self.client.send(get_authentication_issuer::msc2965::Request::new(), None).await?; + let response = self.client.send(get_authentication_issuer::msc2965::Request::new()).await?; Ok(response.issuer) } diff --git a/crates/matrix-sdk/src/pusher.rs b/crates/matrix-sdk/src/pusher.rs index 36e21d9af6d..78d222eff8d 100644 --- a/crates/matrix-sdk/src/pusher.rs +++ b/crates/matrix-sdk/src/pusher.rs @@ -36,14 +36,14 @@ impl Pusher { /// Sets a given pusher pub async fn set(&self, pusher: ruma::api::client::push::Pusher) -> Result<()> { let request = set_pusher::v3::Request::post(pusher); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } /// Deletes a pusher by its ids pub async fn delete(&self, pusher_ids: PusherIds) -> Result<()> { let request = set_pusher::v3::Request::delete(pusher_ids); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } } diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index f1b91463fe7..4c1878f870e 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -224,7 +224,7 @@ impl<'a> IntoFuture for SendRawMessageLikeEvent<'a> { content, ); - let response = room.client.send(request, request_config).await?; + let response = room.client.send(request).with_request_config(request_config).await?; Span::current().record("event_id", tracing::field::debug(&response.event_id)); info!("Sent event in room"); diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index c8cf9a610ca..50ea5341984 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -203,7 +203,7 @@ impl Room { } let request = leave_room::v3::Request::new(self.inner.room_id().to_owned()); - self.client.send(request, None).await?; + self.client.send(request).await?; self.client.base_client().room_left(self.room_id()).await?; Ok(()) } @@ -315,7 +315,7 @@ impl Room { pub async fn messages(&self, options: MessagesOptions) -> Result { let room_id = self.inner.room_id(); let request = options.into_request(room_id); - let http_response = self.client.send(request, None).await?; + let http_response = self.client.send(request).await?; #[allow(unused_mut)] let mut response = Messages { @@ -472,7 +472,7 @@ impl Room { let request = get_room_event::v3::Request::new(self.room_id().to_owned(), event_id.to_owned()); - let raw_event = self.client.send(request, request_config).await?.event; + let raw_event = self.client.send(request).with_request_config(request_config).await?.event; let event = self.try_decrypt_event(raw_event).await?; // Save the event into the event cache, if it's set up. @@ -502,7 +502,7 @@ impl Room { LazyLoadOptions::Enabled { include_redundant_members: false }; } - let response = self.client.send(request, request_config).await?; + let response = self.client.send(request).with_request_config(request_config).await?; let target_event = if let Some(event) = response.event { Some(self.try_decrypt_event(event).await?) @@ -555,11 +555,11 @@ impl Room { let request = get_member_events::v3::Request::new(self.inner.room_id().to_owned()); let response = self .client - .send( - request.clone(), + .send(request.clone()) + .with_request_config( // In some cases it can take longer than 30s to load: // https://github.com/element-hq/synapse/issues/16872 - Some(RequestConfig::new().timeout(Duration::from_secs(60)).retry_limit(3)), + RequestConfig::new().timeout(Duration::from_secs(60)).retry_limit(3), ) .await?; @@ -587,7 +587,7 @@ impl Room { StateEventType::RoomEncryption, "".to_owned(), ); - let response = match self.client.send(request, None).await { + let response = match self.client.send(request).await { Ok(response) => { Some(response.content.deserialize_as::()?) } @@ -1059,7 +1059,7 @@ impl Room { &content, )?; - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Set the given raw account data event in this room. @@ -1100,7 +1100,7 @@ impl Room { content, ); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Adds a tag to the room, or updates it if it already exists. @@ -1145,7 +1145,7 @@ impl Room { tag.to_string(), tag_info, ); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Removes a tag from the room. @@ -1161,7 +1161,7 @@ impl Room { self.inner.room_id().to_owned(), tag.to_string(), ); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Add or remove the `m.favourite` flag for this room. @@ -1259,7 +1259,7 @@ impl Room { let request = set_global_account_data::v3::Request::new(user_id.to_owned(), &content)?; - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -1335,7 +1335,7 @@ impl Room { ban_user::v3::Request::new(self.room_id().to_owned(), user_id.to_owned()), { reason: reason.map(ToOwned::to_owned) } ); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -1352,7 +1352,7 @@ impl Room { unban_user::v3::Request::new(self.room_id().to_owned(), user_id.to_owned()), { reason: reason.map(ToOwned::to_owned) } ); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -1370,7 +1370,7 @@ impl Room { kick_user::v3::Request::new(self.room_id().to_owned(), user_id.to_owned()), { reason: reason.map(ToOwned::to_owned) } ); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -1383,7 +1383,7 @@ impl Room { pub async fn invite_user_by_id(&self, user_id: &UserId) -> Result<()> { let recipient = InvitationRecipient::UserId { user_id: user_id.to_owned() }; let request = invite_user::v3::Request::new(self.room_id().to_owned(), recipient); - self.client.send(request, None).await?; + self.client.send(request).await?; // Force a future room members reload before sending any event to prevent UTDs // that can happen when some event is sent after a room member has been invited @@ -1402,7 +1402,7 @@ impl Room { pub async fn invite_user_by_3pid(&self, invite_id: Invite3pid) -> Result<()> { let recipient = InvitationRecipient::ThirdPartyId(invite_id); let request = invite_user::v3::Request::new(self.room_id().to_owned(), recipient); - self.client.send(request, None).await?; + self.client.send(request).await?; // Force a future room members reload before sending any event to prevent UTDs // that can happen when some event is sent after a room member has been invited @@ -1497,7 +1497,7 @@ impl Room { typing, ); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -1538,7 +1538,7 @@ impl Room { ); request.thread = thread; - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) }) .await @@ -1564,7 +1564,7 @@ impl Room { private_read_receipt, }); - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -2413,7 +2413,7 @@ impl Room { self.ensure_room_joined()?; let request = send_state_event::v3::Request::new(self.room_id().to_owned(), state_key, &content)?; - let response = self.client.send(request, None).await?; + let response = self.client.send(request).await?; Ok(response) } @@ -2467,7 +2467,7 @@ impl Room { content.into_raw_state_event_content(), ); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Strips all information out of an event of the room. @@ -2517,7 +2517,7 @@ impl Room { { reason: reason.map(ToOwned::to_owned) } ); - self.client.send(request, None).await + self.client.send(request).await } /// Returns true if the user with the given user_id is able to redact @@ -2860,7 +2860,7 @@ impl Room { } let request = forget_room::v3::Request::new(self.inner.room_id().to_owned()); - let _response = self.client.send(request, None).await?; + let _response = self.client.send(request).await?; // If it was a DM, remove the room from the `m.direct` global account data. if self.inner.direct_targets_length() != 0 { @@ -2971,7 +2971,7 @@ impl Room { score.map(Into::into), reason, ); - Ok(self.client.send(request, None).await?) + Ok(self.client.send(request).await?) } /// Set a flag on the room to indicate that the user has explicitly marked @@ -2987,7 +2987,7 @@ impl Room { &content, )?; - self.client.send(request, None).await?; + self.client.send(request).await?; Ok(()) } @@ -3189,14 +3189,11 @@ impl Room { pub async fn load_pinned_events(&self) -> Result>> { let response = self .client - .send( - get_state_events_for_key::v3::Request::new( - self.room_id().to_owned(), - StateEventType::RoomPinnedEvents, - "".to_owned(), - ), - None, - ) + .send(get_state_events_for_key::v3::Request::new( + self.room_id().to_owned(), + StateEventType::RoomPinnedEvents, + "".to_owned(), + )) .await; match response { diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index a5385fde0d9..bcb52b2e6d6 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -241,7 +241,7 @@ impl RoomPreview { via, ); - let response = client.send(request, None).await?; + let response = client.send(request).await?; // The server returns a `Left` room state for rooms the user has not joined. Be // more precise than that, and set it to `None` if we haven't joined @@ -294,8 +294,8 @@ impl RoomPreview { let joined_members_request = joined_members::v3::Request::new(room_id.to_owned()); let (state, joined_members) = - try_join!(async { client.send(state_request, None).await }, async { - client.send(joined_members_request, None).await + try_join!(async { client.send(state_request).await }, async { + client.send(joined_members_request).await })?; // Converting from usize to u64 will always work, up to 64-bits devices; diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index fee7155b94c..9e6494b155d 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -578,10 +578,12 @@ impl SlidingSync { debug!("Sending request"); // Prepare the request. - let request = - self.inner.client.send(request, Some(request_config)).with_homeserver_override( - self.inner.version.overriding_url().map(ToString::to_string), - ); + let request = self + .inner + .client + .send(request) + .with_request_config(request_config) + .with_homeserver_override(self.inner.version.overriding_url().map(ToString::to_string)); // Send the request and get a response with end-to-end encryption support. // diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 19ffee007ea..4891a76bdef 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -1156,7 +1156,7 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// ) /// .unwrap(); /// - /// let response = room.client().send(r, None).await.unwrap(); + /// let response = room.client().send(r).await.unwrap(); /// // The delayed `m.room.message` event type should be mocked by the server. /// assert_eq!("$some_id", response.delay_id); /// # anyhow::Ok(()) }); @@ -1383,7 +1383,7 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { /// &AnyStateEventContent::RoomCreate(RoomCreateEventContent::new_v11()), /// ) /// .unwrap(); - /// let response = room.client().send(r, None).await.unwrap(); + /// let response = room.client().send(r).await.unwrap(); /// // The delayed `m.room.message` event type should be mocked by the server. /// assert_eq!("$some_id", response.delay_id); /// diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index 6365779d333..ea2546b3393 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -55,7 +55,7 @@ impl MatrixDriver { /// Requests an OpenID token for the current user. pub(crate) async fn get_open_id(&self) -> Result { let user_id = self.room.own_user_id().to_owned(); - self.room.client.send(OpenIdRequest::new(user_id), None).await.map_err(Error::Http) + self.room.client.send(OpenIdRequest::new(user_id)).await.map_err(Error::Http) } /// Reads the latest `limit` events of a given `event_type` from the room. @@ -146,7 +146,7 @@ impl MatrixDriver { delayed_event_parameters, Raw::::from_json(content), ); - self.room.client.send(r, None).await.map(|r| r.into())? + self.room.client.send(r).await.map(|r| r.into())? } (Some(key), Some(delayed_event_parameters)) => { @@ -157,7 +157,7 @@ impl MatrixDriver { delayed_event_parameters, Raw::::from_json(content), ); - self.room.client.send(r, None).await.map(|r| r.into())? + self.room.client.send(r).await.map(|r| r.into())? } }) } @@ -172,7 +172,7 @@ impl MatrixDriver { action: UpdateAction, ) -> Result { let r = delayed_events::update_delayed_event::unstable::Request::new(delay_id, action); - self.room.client.send(r, None).await.map_err(Error::Http) + self.room.client.send(r).await.map_err(Error::Http) } /// Starts forwarding new room events. Once the returned `EventReceiver` diff --git a/examples/get_profiles/src/main.rs b/examples/get_profiles/src/main.rs index c5af1c90574..222f51bab32 100644 --- a/examples/get_profiles/src/main.rs +++ b/examples/get_profiles/src/main.rs @@ -22,7 +22,7 @@ async fn get_profile(client: Client, mxid: &UserId) -> MatrixResult let request = profile::get_profile::v3::Request::new(mxid.to_owned()); // Start the request using matrix_sdk::Client::send - let resp = client.send(request, None).await?; + let resp = client.send(request).await?; // Use the response and construct a UserProfile struct. // See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/struct.Response.html From f8a9d12c88fc18e93328201e4d97f4ae442f86f6 Mon Sep 17 00:00:00 2001 From: Daniel Salinas Date: Thu, 12 Dec 2024 16:00:17 -0500 Subject: [PATCH 835/979] Use a type alias to allow bindings to take advantage of custom types --- bindings/matrix-sdk-ffi/src/event.rs | 5 +++-- .../matrix-sdk-ffi/src/session_verification.rs | 6 +++--- bindings/matrix-sdk-ffi/src/timeline/content.rs | 7 ++++--- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 15 ++++++++------- bindings/matrix-sdk-ffi/src/utils.rs | 13 ++++++++++++- .../src/timeline/event_item/content/polls.rs | 4 ++-- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index 2be9989959e..188c75e9ce4 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -17,6 +17,7 @@ use ruma::{ use crate::{ room_member::MembershipState, ruma::{MessageType, NotifyType}, + utils::Timestamp, ClientError, }; @@ -33,8 +34,8 @@ impl TimelineEvent { self.0.sender().to_string() } - pub fn timestamp(&self) -> u64 { - self.0.origin_server_ts().0.into() + pub fn timestamp(&self) -> Timestamp { + self.0.origin_server_ts().into() } pub fn event_type(&self) -> Result { diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index 6cc4dea8b76..6843c3e2312 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -13,7 +13,7 @@ use ruma::UserId; use tracing::{error, info}; use super::RUNTIME; -use crate::error::ClientError; +use crate::{error::ClientError, utils::Timestamp}; #[derive(uniffi::Object)] pub struct SessionVerificationEmoji { @@ -46,7 +46,7 @@ pub struct SessionVerificationRequestDetails { device_id: String, display_name: Option, /// First time this device was seen in milliseconds since epoch. - first_seen_timestamp: u64, + first_seen_timestamp: Timestamp, } #[matrix_sdk_ffi_macros::export(callback_interface)] @@ -242,7 +242,7 @@ impl SessionVerificationController { flow_id: request.flow_id().into(), device_id: other_device_data.device_id().into(), display_name: other_device_data.display_name().map(str::to_string), - first_seen_timestamp: other_device_data.first_time_seen_ts().get().into(), + first_seen_timestamp: other_device_data.first_time_seen_ts().into(), }); } } diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index 3c182c65e82..86624fb1ab5 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -22,6 +22,7 @@ use super::ProfileDetails; use crate::{ error::ClientError, ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind}, + utils::Timestamp, }; impl From for TimelineItemContent { @@ -187,7 +188,7 @@ pub enum TimelineItemContent { max_selections: u64, answers: Vec, votes: HashMap>, - end_time: Option, + end_time: Option, has_been_edited: bool, }, CallInvite, @@ -319,7 +320,7 @@ pub struct Reaction { #[derive(Clone, uniffi::Record)] pub struct ReactionSenderData { pub sender_id: String, - pub timestamp: u64, + pub timestamp: Timestamp, } #[derive(Clone, uniffi::Enum)] @@ -481,7 +482,7 @@ impl From for TimelineItemContent { .map(|i| PollAnswer { id: i.id, text: i.text }) .collect(), votes: value.votes, - end_time: value.end_time, + end_time: value.end_time.map(|t| t.into()), has_been_edited: value.has_been_edited, } } diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 89f226dc6a0..0d0894b6443 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -75,6 +75,7 @@ use crate::{ VideoInfo, }, task_handle::TaskHandle, + utils::Timestamp, RUNTIME, }; @@ -986,7 +987,7 @@ impl TimelineItem { pub fn as_virtual(self: Arc) -> Option { use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem; match self.0.as_virtual()? { - VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: ts.0.into() }), + VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }), VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker), } } @@ -1081,7 +1082,7 @@ pub struct EventTimelineItem { is_own: bool, is_editable: bool, content: TimelineItemContent, - timestamp: u64, + timestamp: Timestamp, reactions: Vec, local_send_state: Option, read_receipts: HashMap, @@ -1101,7 +1102,7 @@ impl From for EventTimelineItem { .into_iter() .map(|(sender_id, info)| ReactionSenderData { sender_id: sender_id.to_string(), - timestamp: info.timestamp.0.into(), + timestamp: info.timestamp.into(), }) .collect(), }) @@ -1118,7 +1119,7 @@ impl From for EventTimelineItem { is_own: item.is_own(), is_editable: item.is_editable(), content: item.content().clone().into(), - timestamp: item.timestamp().0.into(), + timestamp: item.timestamp().into(), reactions, local_send_state: item.send_state().map(|s| s.into()), read_receipts, @@ -1131,12 +1132,12 @@ impl From for EventTimelineItem { #[derive(Clone, uniffi::Record)] pub struct Receipt { - pub timestamp: Option, + pub timestamp: Option, } impl From for Receipt { fn from(value: ruma::events::receipt::Receipt) -> Self { - Receipt { timestamp: value.ts.map(|ts| ts.0.into()) } + Receipt { timestamp: value.ts.map(|ts| ts.into()) } } } @@ -1260,7 +1261,7 @@ pub enum VirtualTimelineItem { DateDivider { /// A timestamp in milliseconds since Unix Epoch on that day in local /// time. - ts: u64, + ts: Timestamp, }, /// The user's own read marker. diff --git a/bindings/matrix-sdk-ffi/src/utils.rs b/bindings/matrix-sdk-ffi/src/utils.rs index 8868573e344..19e6a4a565f 100644 --- a/bindings/matrix-sdk-ffi/src/utils.rs +++ b/bindings/matrix-sdk-ffi/src/utils.rs @@ -15,9 +15,20 @@ use std::{mem::ManuallyDrop, ops::Deref}; use async_compat::TOKIO1 as RUNTIME; -use ruma::UInt; +use ruma::{MilliSecondsSinceUnixEpoch, UInt}; use tracing::warn; +#[derive(Debug, Clone)] +pub struct Timestamp(u64); + +impl From for Timestamp { + fn from(date: MilliSecondsSinceUnixEpoch) -> Self { + Self(date.0.into()) + } +} + +uniffi::custom_newtype!(Timestamp, u64); + pub(crate) fn u64_to_uint(u: u64) -> UInt { UInt::new(u).unwrap_or_else(|| { warn!("u64 -> UInt conversion overflowed, falling back to UInt::MAX"); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs index 8112bc7b147..b758caf124d 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/polls.rs @@ -146,7 +146,7 @@ impl PollState { .iter() .map(|i| ((*i.0).to_owned(), i.1.iter().map(|i| i.to_string()).collect())) .collect(), - end_time: self.end_event_timestamp.map(|millis| millis.0.into()), + end_time: self.end_event_timestamp, has_been_edited: self.has_been_edited, } } @@ -178,7 +178,7 @@ pub struct PollResult { pub max_selections: u64, pub answers: Vec, pub votes: HashMap>, - pub end_time: Option, + pub end_time: Option, pub has_been_edited: bool, } From 36427b0e1240721de3439aaaa90038c43565e516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 20 Dec 2024 12:35:34 +0100 Subject: [PATCH 836/979] fix(ui): Consider banned rooms as rooms we left in the non-left rooms matcher Recently we started to differentiate between rooms we've been banned from from rooms we have left on our own. Sadly the non-left rooms matcher only checked if the room state is not equal to the Left state. This then accidentally moved all the banned rooms to be considered as non-left. We replace the single if expression with a match and list all the states, this way we're going to be notified by the compiler that we need to consider any new states we add in the future. --- crates/matrix-sdk-ui/CHANGELOG.md | 7 +++++++ .../src/room_list_service/filters/non_left.rs | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 908df65b4e1..1be4ef45ec8 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Bug Fixes + +- Don't consider rooms in the banned state to be non-left rooms. This bug was + introduced due to the introduction of the banned state for rooms, and the + non-left room filter did not take the new room stat into account. + ([#4448](https://github.com/matrix-org/matrix-rust-sdk/pull/4448)) + ## [0.9.0] - 2024-12-18 ### Bug Fixes diff --git a/crates/matrix-sdk-ui/src/room_list_service/filters/non_left.rs b/crates/matrix-sdk-ui/src/room_list_service/filters/non_left.rs index 8f59575a787..05ceccd361c 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/filters/non_left.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/filters/non_left.rs @@ -28,7 +28,10 @@ where F: Fn(&Room) -> RoomState, { fn matches(&self, room: &Room) -> bool { - (self.state)(room) != RoomState::Left + match (self.state)(room) { + RoomState::Joined | RoomState::Invited | RoomState::Knocked => true, + RoomState::Left | RoomState::Banned => false, + } } } @@ -59,6 +62,10 @@ mod tests { let matcher = NonLeftRoomMatcher { state: |_| RoomState::Left }; assert!(!matcher.matches(&room)); + // When a room is in the banned state, it doesn't match either. + let matcher = NonLeftRoomMatcher { state: |_| RoomState::Banned }; + assert!(!matcher.matches(&room)); + // When a room has been joined, it does match (unless it's empty). let matcher = NonLeftRoomMatcher { state: |_| RoomState::Joined }; assert!(matcher.matches(&room)); From be89e3aacb31b2863c6dd999f37ebd31451cfa0f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 13 Dec 2024 08:56:13 +0100 Subject: [PATCH 837/979] feat(ui): Add `TimelineBuilder::with_vectordiffs_as_inputs`. This patch adds `with_vectordiffs_as_inputs` on `TimelineBuilder` and `vectordiffs_as_inputs` on `TimelineSettings`. This new flag allows to transition from one system to another for the `Timeline`, when enabled, the `Timeline` will accept `VectorDiff` for the inputs instead of `Vec`. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 8 ++++++++ crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 44bba18431f..97afe7800f4 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -145,6 +145,14 @@ impl TimelineBuilder { self } + /// Use `VectorDiff`s as the new “input mechanism” for the `Timeline`. + /// + /// Read `TimelineSettings::vectordiffs_as_inputs` to learn more. + pub fn with_vectordiffs_as_inputs(mut self) -> Self { + self.settings.vectordiffs_as_inputs = true; + self + } + /// Create a [`Timeline`] with the options set on this builder. #[tracing::instrument( skip(self), diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index a90aafd9d91..d317f38a481 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -131,13 +131,22 @@ pub(super) struct TimelineController { pub(super) struct TimelineSettings { /// Should the read receipts and read markers be handled? pub(super) track_read_receipts: bool, + /// Event filter that controls what's rendered as a timeline item (and thus /// what can carry read receipts). pub(super) event_filter: Arc, + /// Are unparsable events added as timeline items of their own kind? pub(super) add_failed_to_parse: bool, + /// Should the timeline items be grouped by day or month? pub(super) date_divider_mode: DateDividerMode, + + /// Whether `VectorDiff` is the “input mechanism” to use. + /// + /// This mechanism will replace the existing one, but this runtime feature + /// flag is necessary for the transition and the testing phase. + pub(super) vectordiffs_as_inputs: bool, } #[cfg(not(tarpaulin_include))] @@ -146,6 +155,7 @@ impl fmt::Debug for TimelineSettings { f.debug_struct("TimelineSettings") .field("track_read_receipts", &self.track_read_receipts) .field("add_failed_to_parse", &self.add_failed_to_parse) + .field("vectordiffs_as_inputs", &self.vectordiffs_as_inputs) .finish_non_exhaustive() } } @@ -157,6 +167,7 @@ impl Default for TimelineSettings { event_filter: Arc::new(default_event_filter), add_failed_to_parse: true, date_divider_mode: DateDividerMode::Daily, + vectordiffs_as_inputs: false, } } } From 1c2fb1ab7293a02c65aecb7cddfc202fb7be7c43 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 11:51:37 +0100 Subject: [PATCH 838/979] refactor(sdk): Add `RoomEventCacheUpdate::UpdateTimelineEvents`. This patch adds a new variant to `RoomEventCacheUpdate`, namely `UpdateTimelineEvents. It's going to replace `AddTimelineEvents` soon once it's stable enough. This is a transition. They are read by the `Timeline` if and only if `TimelineSettings::vectordiffs_as_inputs` is turned on. --- crates/matrix-sdk/src/event_cache/mod.rs | 11 ++++++++++ crates/matrix-sdk/src/event_cache/room/mod.rs | 18 ++++++++++++--- .../tests/integration/event_cache.rs | 22 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 37c34f50e91..20a3e90c359 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -34,6 +34,7 @@ use std::{ }; use eyeball::Subscriber; +use eyeball_im::VectorDiff; use matrix_sdk_base::{ deserialized_responses::{AmbiguityChange, SyncTimelineEvent, TimelineEvent}, event_cache::store::{EventCacheStoreError, EventCacheStoreLock}, @@ -543,6 +544,7 @@ pub enum RoomEventCacheUpdate { }, /// The room has received new timeline events. + // TODO: remove once `UpdateTimelineEvents` is stabilized AddTimelineEvents { /// All the new events that have been added to the room's timeline. events: Vec, @@ -551,6 +553,15 @@ pub enum RoomEventCacheUpdate { origin: EventsOrigin, }, + /// The room has received updates for the timeline as _diffs_. + UpdateTimelineEvents { + /// Diffs to apply to the timeline. + diffs: Vec>, + + /// Where the diffs are coming from. + origin: EventsOrigin, + }, + /// The room has received new ephemeral events. AddEphemeralEvents { /// XXX: this is temporary, until read receipts are handled in the event diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 06b9fb3e1e8..3ac7278a3ff 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -533,8 +533,8 @@ impl RoomEventCacheInner { // Add the previous back-pagination token (if present), followed by the timeline // events themselves. - { - state + let sync_timeline_events_diffs = { + let sync_timeline_events_diffs = state .with_events_mut(|room_events| { if let Some(prev_token) = &prev_batch { room_events.push_gap(Gap { prev_token: prev_token.clone() }); @@ -556,6 +556,8 @@ impl RoomEventCacheInner { .replace_gap_at([], prev_gap_id) .expect("we obtained the valid position beforehand"); } + + room_events.updates_as_vector_diffs() }) .await?; @@ -566,7 +568,9 @@ impl RoomEventCacheInner { cache.events.insert(event_id.to_owned(), (self.room_id.clone(), ev.clone())); } } - } + + sync_timeline_events_diffs + }; // Now that all events have been added, we can trigger the // `pagination_token_notifier`. @@ -576,6 +580,7 @@ impl RoomEventCacheInner { // The order of `RoomEventCacheUpdate`s is **really** important here. { + // TODO: remove once `UpdateTimelineEvents` is stabilized. if !sync_timeline_events.is_empty() { let _ = self.sender.send(RoomEventCacheUpdate::AddTimelineEvents { events: sync_timeline_events, @@ -583,6 +588,13 @@ impl RoomEventCacheInner { }); } + if !sync_timeline_events_diffs.is_empty() { + let _ = self.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents { + diffs: sync_timeline_events_diffs, + origin: EventsOrigin::Sync, + }); + } + if !ephemeral_events.is_empty() { let _ = self .sender diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 7b880c7404f..98ee29db207 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -84,6 +84,8 @@ async fn test_event_cache_receives_events() { assert_let_timeout!( Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() ); + // It does also receive the update as `VectorDiff`. + assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv()); // Which contains the event that was sent beforehand. assert_eq!(events.len(), 1); @@ -170,6 +172,8 @@ async fn test_ignored_unignored() { assert_let_timeout!( Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() ); + // It does also receive the update as `VectorDiff`. + assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv()); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "i don't like this dexter"); @@ -197,6 +201,10 @@ async fn wait_for_initial_events( update = room_stream.recv().await.expect("read error"); } assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + + let update = room_stream.recv().await.expect("read error"); + + assert_matches!(update, RoomEventCacheUpdate::UpdateTimelineEvents { .. }); } else { assert_eq!(events.len(), 1); } @@ -810,6 +818,10 @@ async fn test_limited_timeline_with_storage() { assert_let_timeout!( Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() ); + // It does also receive the update as `VectorDiff`. + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv() + ); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "hey yo"); } else { @@ -832,6 +844,8 @@ async fn test_limited_timeline_with_storage() { assert_let_timeout!( Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() ); + // It does also receive the update as `VectorDiff`. + assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv()); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "gappy!"); @@ -1058,6 +1072,9 @@ async fn test_no_gap_stored_after_deduplicated_sync() { if events.is_empty() { let update = stream.recv().await.expect("read error"); assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + // It does also receive the update as `VectorDiff`. + let update = stream.recv().await.expect("read error"); + assert_matches!(update, RoomEventCacheUpdate::UpdateTimelineEvents { .. }); } drop(events); @@ -1099,6 +1116,8 @@ async fn test_no_gap_stored_after_deduplicated_sync() { assert_event_matches_msg(&events[1], "world"); assert_event_matches_msg(&events[2], "sup"); assert_eq!(events.len(), 3); + + assert!(stream.is_empty()); } #[async_test] @@ -1133,6 +1152,9 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { if events.is_empty() { let update = stream.recv().await.expect("read error"); assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + // It does also receive the update as `VectorDiff`. + let update = stream.recv().await.expect("read error"); + assert_matches!(update, RoomEventCacheUpdate::UpdateTimelineEvents { .. }); } drop(events); From e28073361d41d25dd48a9de8a12f74b963ac198e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 13:54:23 +0100 Subject: [PATCH 839/979] feat(ui): Add blank `handle_remote_events_with_diffs`. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 29 ++++++++++++---- .../src/timeline/controller/mod.rs | 21 ++++++++++++ .../src/timeline/controller/state.rs | 33 +++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 97afe7800f4..ab850666ef2 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -163,6 +163,7 @@ impl TimelineBuilder { )] pub async fn build(self) -> Result { let Self { room, settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self; + let settings_vectordiffs_as_inputs = settings.vectordiffs_as_inputs; let client = room.client(); let event_cache = client.event_cache(); @@ -284,16 +285,32 @@ impl TimelineBuilder { inner.clear().await; } + // TODO: remove once `UpdateTimelineEvents` is stabilized. RoomEventCacheUpdate::AddTimelineEvents { events, origin } => { - trace!("Received new timeline events."); + if !settings_vectordiffs_as_inputs { + trace!("Received new timeline events."); - inner.add_events_at( - events.into_iter(), - TimelineNewItemPosition::End { origin: match origin { + inner.add_events_at( + events.into_iter(), + TimelineNewItemPosition::End { origin: match origin { + EventsOrigin::Sync => RemoteEventOrigin::Sync, + } + } + ).await; + } + } + + RoomEventCacheUpdate::UpdateTimelineEvents { diffs, origin } => { + if settings_vectordiffs_as_inputs { + trace!("Received new timeline events diffs"); + + inner.handle_remote_events_with_diffs( + diffs, + match origin { EventsOrigin::Sync => RemoteEventOrigin::Sync, } - } - ).await; + ).await; + } } RoomEventCacheUpdate::AddEphemeralEvents { events } => { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index d317f38a481..2422bdfa01b 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -676,6 +676,27 @@ impl TimelineController

{ state.add_remote_events_at(events, position, &self.room_data_provider, &self.settings).await } + /// Handle updates on events as [`VectorDiff`]s. + pub(super) async fn handle_remote_events_with_diffs( + &self, + diffs: Vec>, + origin: RemoteEventOrigin, + ) { + if diffs.is_empty() { + return; + } + + let mut state = self.state.write().await; + state + .handle_remote_events_with_diffs( + diffs, + origin, + &self.room_data_provider, + &self.settings, + ) + .await + } + pub(super) async fn clear(&self) { self.state.write().await.clear(); } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index bfff486a6fe..e2ef1d6c62b 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -19,6 +19,7 @@ use std::{ sync::{Arc, RwLock}, }; +use eyeball_im::VectorDiff; use itertools::Itertools as _; use matrix_sdk::{ deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer, send_queue::SendHandle, @@ -153,6 +154,25 @@ impl TimelineState { handle_many_res } + /// Handle updates on events as [`VectorDiff`]s. + pub(super) async fn handle_remote_events_with_diffs( + &mut self, + diffs: Vec>, + origin: RemoteEventOrigin, + room_data: &RoomData, + settings: &TimelineSettings, + ) where + RoomData: RoomDataProvider, + { + if diffs.is_empty() { + return; + } + + let mut transaction = self.transaction(); + transaction.handle_remote_events_with_diffs(diffs, origin, room_data, settings).await; + transaction.commit(); + } + /// Marks the given event as fully read, using the read marker received from /// sync. pub(super) fn handle_fully_read_marker(&mut self, fully_read_event_id: OwnedEventId) { @@ -416,6 +436,19 @@ impl TimelineStateTransaction<'_> { total } + /// Handle updates on events as [`VectorDiff`]s. + pub(super) async fn handle_remote_events_with_diffs( + &mut self, + diffs: Vec>, + origin: RemoteEventOrigin, + room_data_provider: &RoomData, + settings: &TimelineSettings, + ) where + RoomData: RoomDataProvider, + { + todo!() + } + fn check_no_unused_unique_ids(&self) { let duplicates = self .items From c1f82324508f860a0852a4b707e4afba4b0f0601 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 15:31:34 +0100 Subject: [PATCH 840/979] task(ui): Support `VectorDiff::Append` in `TimelineStateTransaction::handle_remote_events_with_diffs`. This patch updates `TimelineStateTransaction::handle_remote_events_with_diffs` to support `VectorDiff::Append`. --- .../src/timeline/controller/state.rs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index e2ef1d6c62b..737fc2c9cdd 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -446,7 +446,26 @@ impl TimelineStateTransaction<'_> { ) where RoomData: RoomDataProvider, { - todo!() + let mut day_divider_adjuster = DayDividerAdjuster::default(); + + for diff in diffs { + match diff { + VectorDiff::Append { values: events } => { + for event in events { + self.handle_remote_event( + event, + TimelineItemPosition::End { origin }, + room_data_provider, + settings, + &mut day_divider_adjuster, + ) + .await; + } + } + + v => unimplemented!("{v:?}"), + } + } } fn check_no_unused_unique_ids(&self) { From 23c09b2c9dd708bf0b2aef478c604aa077fb9364 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 15:37:38 +0100 Subject: [PATCH 841/979] task(ui): Support `VectorDiff::PushFront` in `TimelineStateTransaction::handle_remote_events_with_diffs`. This patch updates `TimelineStateTransaction::handle_remote_events_with_diffs` to support `VectorDiff::PushFront`. --- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 737fc2c9cdd..7f6fbe4397a 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -463,6 +463,17 @@ impl TimelineStateTransaction<'_> { } } + VectorDiff::PushFront { value: event } => { + self.handle_remote_event( + event, + TimelineItemPosition::Start { origin }, + room_data_provider, + settings, + &mut day_divider_adjuster, + ) + .await; + } + v => unimplemented!("{v:?}"), } } From 3f17325bacdb2619d3f8152a5693784584a2fd67 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 15:38:04 +0100 Subject: [PATCH 842/979] task(ui): Support `VectorDiff::PushBack` in `TimelineStateTransaction::handle_remote_events_with_diffs`. This patch updates `TimelineStateTransaction::handle_remote_events_with_diffs` to support `VectorDiff::PushBack`. --- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 7f6fbe4397a..eca24cfe71f 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -474,6 +474,17 @@ impl TimelineStateTransaction<'_> { .await; } + VectorDiff::PushBack { value: event } => { + self.handle_remote_event( + event, + TimelineItemPosition::End { origin }, + room_data_provider, + settings, + &mut day_divider_adjuster, + ) + .await; + } + v => unimplemented!("{v:?}"), } } From eca3749b2895687e036266cbf8350a6b50971c4c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 15:40:01 +0100 Subject: [PATCH 843/979] task(ui): Support `VectorDiff::Clear` in `TimelineStateTransaction::handle_remote_events_with_diffs`. This patch updates `TimelineStateTransaction::handle_remote_events_with_diffs` to support `VectorDiff::Clear`. --- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index eca24cfe71f..cbc93172483 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -485,6 +485,10 @@ impl TimelineStateTransaction<'_> { .await; } + VectorDiff::Clear => { + self.clear(); + } + v => unimplemented!("{v:?}"), } } From 02ab57870acb1482a7a1e1000b1cd940a02471d4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 9 Dec 2024 15:56:52 +0100 Subject: [PATCH 844/979] task(ui): Add `ObservableItems::insert_remote_event`. This patch adds the `ObservavbleItems::insert_remote_event` method. This is going to be useful to implement `VectorDiff::Insert` inside `TimelineStateTransaction::handle_remote_events_with_diffs`. --- .../timeline/controller/observable_items.rs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index e5f62d70716..a02614862de 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -220,6 +220,13 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> { self.all_remote_events.push_back(event_meta); } + /// Insert a new remote event at a specific index. + /// + /// Not to be confused with inserting a timeline item! + pub fn insert_remote_event(&mut self, event_index: usize, event_meta: EventMeta) { + self.all_remote_events.insert(event_index, event_meta); + } + /// Get a remote event by using an event ID. pub fn get_remote_event_by_event_id_mut( &mut self, @@ -1189,6 +1196,18 @@ impl AllRemoteEvents { self.0.push_back(event_meta) } + /// Insert a new remote event at a specific index. + fn insert(&mut self, event_index: usize, event_meta: EventMeta) { + // If there is an associated `timeline_item_index`, shift all the + // `timeline_item_index` that come after this one. + if let Some(new_timeline_item_index) = event_meta.timeline_item_index { + self.increment_all_timeline_item_index_after(new_timeline_item_index); + } + + // Insert the event. + self.0.insert(event_index, event_meta) + } + /// Remove one remote event at a specific index, and return it if it exists. fn remove(&mut self, event_index: usize) -> Option { // Remove the event. @@ -1399,6 +1418,37 @@ mod all_remote_events_tests { ); } + #[test] + fn test_insert() { + let mut events = AllRemoteEvents::default(); + + // Insert on an empty set, nothing particular. + events.insert(0, event_meta("$ev0", Some(0))); + + // Insert at the end with no `timeline_item_index`. + events.insert(1, event_meta("$ev1", None)); + + // Insert at the end with a `timeline_item_index`. + events.insert(2, event_meta("$ev2", Some(1))); + + // Insert at the start, with a `timeline_item_index`. + events.insert(0, event_meta("$ev3", Some(0))); + + assert_events!( + events, + [ + // `timeline_item_index` is untouched + ("$ev3", Some(0)), + // `timeline_item_index` has been shifted once + ("$ev0", Some(1)), + // no `timeline_item_index` + ("$ev1", None), + // `timeline_item_index` has been shifted once + ("$ev2", Some(2)), + ] + ); + } + #[test] fn test_remove() { let mut events = AllRemoteEvents::default(); From b25fd830ece2dd9b1057b1c721c12d4971971d35 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 10 Dec 2024 09:23:14 +0100 Subject: [PATCH 845/979] task(ui): Add `AllRemoteEvents::range`. This patch adds the `AllRemoteEvents::range` method. This is going to be useful to support `VectorDiff::Insert` inside `TimelineStateTransaction::handle_remote_events_with_diffs`. --- .../timeline/controller/observable_items.rs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index a02614862de..5a0724ff124 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -15,7 +15,7 @@ use std::{ cmp::Ordering, collections::{vec_deque::Iter, VecDeque}, - ops::Deref, + ops::{Deref, RangeBounds}, sync::Arc, }; @@ -1167,6 +1167,15 @@ impl AllRemoteEvents { self.0.iter() } + /// Return a front-to-back iterator covering ranges of all remote events + /// described by `range`. + pub fn range(&self, range: R) -> Iter<'_, EventMeta> + where + R: RangeBounds, + { + self.0.range(range) + } + /// Remove all remote events. fn clear(&mut self) { self.0.clear(); @@ -1337,6 +1346,34 @@ mod all_remote_events_tests { } } + #[test] + fn test_range() { + let mut events = AllRemoteEvents::default(); + + // Push some events. + events.push_back(event_meta("$ev0", None)); + events.push_back(event_meta("$ev1", None)); + events.push_back(event_meta("$ev2", None)); + + assert_eq!(events.iter().count(), 3); + + // Test a few combinations. + assert_eq!(events.range(..).count(), 3); + assert_eq!(events.range(1..).count(), 2); + assert_eq!(events.range(0..=1).count(), 2); + + // Iterate on some of them. + let mut some_events = events.range(1..); + + assert_matches!(some_events.next(), Some(EventMeta { event_id, .. }) => { + assert_eq!(event_id.as_str(), "$ev1"); + }); + assert_matches!(some_events.next(), Some(EventMeta { event_id, .. }) => { + assert_eq!(event_id.as_str(), "$ev2"); + }); + assert!(some_events.next().is_none()); + } + #[test] fn test_clear() { let mut events = AllRemoteEvents::default(); From 409fccb7090feaaad78e2c39bda2b4e75f2edacb Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 10 Dec 2024 09:38:03 +0100 Subject: [PATCH 846/979] task(ui): Support `VectorDiff::Insert` in `TimelineStateTransaction::handle_remote_events_with_diffs`. This patch updates `TimelineStateTransaction::handle_remote_events_with_diffs` to support `VectorDiff::Insert`. --- .../src/timeline/controller/state.rs | 22 ++++++- .../src/timeline/event_handler.rs | 60 ++++++++++++++++++- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index cbc93172483..9a9050ace11 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -485,6 +485,17 @@ impl TimelineStateTransaction<'_> { .await; } + VectorDiff::Insert { index: event_index, value: event } => { + self.handle_remote_event( + event, + TimelineItemPosition::At { event_index, origin }, + room_data_provider, + settings, + &mut day_divider_adjuster, + ) + .await; + } + VectorDiff::Clear => { self.clear(); } @@ -550,7 +561,8 @@ impl TimelineStateTransaction<'_> { // Retrieve the origin of the event. let origin = match position { TimelineItemPosition::End { origin } - | TimelineItemPosition::Start { origin } => origin, + | TimelineItemPosition::Start { origin } + | TimelineItemPosition::At { origin, .. } => origin, TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self .items @@ -826,6 +838,10 @@ impl TimelineStateTransaction<'_> { self.items.push_back_remote_event(event_meta.base_meta()); } + TimelineItemPosition::At { event_index, .. } => { + self.items.insert_remote_event(event_index, event_meta.base_meta()); + } + TimelineItemPosition::UpdateDecrypted { .. } => { if let Some(event) = self.items.get_remote_event_by_event_id_mut(event_meta.event_id) @@ -846,7 +862,9 @@ impl TimelineStateTransaction<'_> { if settings.track_read_receipts && matches!( position, - TimelineItemPosition::Start { .. } | TimelineItemPosition::End { .. } + TimelineItemPosition::Start { .. } + | TimelineItemPosition::End { .. } + | TimelineItemPosition::At { .. } ) { self.load_read_receipts_for_event(event_meta.event_id, room_data_provider).await; diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 475c1f30a14..3cb790740ea 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -290,6 +290,15 @@ pub(super) enum TimelineItemPosition { origin: RemoteEventOrigin, }, + /// One item is inserted to the timeline. + At { + /// Where to insert the remote event. + event_index: usize, + + /// The origin of the new item. + origin: RemoteEventOrigin, + }, + /// A single item is updated, after it's been successfully decrypted. /// /// This happens when an item that was a UTD must be replaced with the @@ -595,7 +604,9 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { replacement: PendingEdit, ) { match position { - TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted { .. } => { + TimelineItemPosition::Start { .. } + | TimelineItemPosition::At { .. } + | TimelineItemPosition::UpdateDecrypted { .. } => { // Only insert the edit if there wasn't any other edit // before. // @@ -1039,7 +1050,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Remote { event_id, raw_event, position, txn_id, encryption_info, .. } => { let origin = match *position { TimelineItemPosition::Start { origin } - | TimelineItemPosition::End { origin } => origin, + | TimelineItemPosition::End { origin } + | TimelineItemPosition::At { origin, .. } => origin, // For updates, reuse the origin of the encrypted event. TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self @@ -1108,6 +1120,50 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { self.items.push_front(item, Some(0)); } + Flow::Remote { position: TimelineItemPosition::At { event_index, .. }, .. } => { + let all_remote_events = self.items.all_remote_events(); + let event_index = *event_index; + + // Look for the closest `timeline_item_index` at the left of `event_index`. + let timeline_item_index = all_remote_events + .range(0..=event_index) + .rev() + .find_map(|event_meta| event_meta.timeline_item_index) + // The new `timeline_item_index` is the previous + 1. + .map(|timeline_item_index| timeline_item_index + 1); + + // No index? Look for the closest `timeline_item_index` at the right of + // `event_index`. + let timeline_item_index = timeline_item_index.or_else(|| { + all_remote_events + .range(event_index + 1..) + .find_map(|event_meta| event_meta.timeline_item_index) + }); + + // Still no index? Well, it means there is no existing `timeline_item_index` + // so we are inserting at the last non-local item position as a fallback. + let timeline_item_index = timeline_item_index.unwrap_or_else(|| { + self.items + .iter() + .enumerate() + .rev() + .find_map(|(timeline_item_index, timeline_item)| { + (!timeline_item.as_event()?.is_local_echo()) + .then_some(timeline_item_index + 1) + }) + .unwrap_or(0) + }); + + trace!( + ?event_index, + ?timeline_item_index, + "Adding new remote timeline at specific event index" + ); + + let item = self.meta.new_timeline_item(item); + self.items.insert(timeline_item_index, item, Some(event_index)); + } + Flow::Remote { position: TimelineItemPosition::End { .. }, txn_id, event_id, .. } => { From 2358e4c32f2156c057ef44f170f6a368b30087d1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 10 Dec 2024 10:54:23 +0100 Subject: [PATCH 847/979] task(ui): Support `VectorDiff::Remove,` in `TimelineStateTransaction::handle_remote_events_with_diffs`. This patch updates `TimelineStateTransaction::handle_remote_events_with_diffs` to support `VectorDiff::Remove,`. --- .../timeline/controller/observable_items.rs | 8 +++-- .../src/timeline/controller/state.rs | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 5a0724ff124..0d326c5a71a 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -1162,6 +1162,11 @@ mod observable_items_tests { pub struct AllRemoteEvents(VecDeque); impl AllRemoteEvents { + /// Return a reference to a remote event. + pub fn get(&self, event_index: usize) -> Option<&EventMeta> { + self.0.get(event_index) + } + /// Return a front-to-back iterator over all remote events. pub fn iter(&self) -> Iter<'_, EventMeta> { self.0.iter() @@ -1289,8 +1294,7 @@ impl AllRemoteEvents { } /// Notify that a timeline item has been removed at - /// `new_timeline_item_index`. If `event_index` is `Some(_)`, it means the - /// remote event at `event_index` must be unmapped. + /// `new_timeline_item_index`. fn timeline_item_has_been_removed_at(&mut self, timeline_item_index_to_remove: usize) { for event_meta in self.0.iter_mut() { let mut remove_timeline_item_index = false; diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 9a9050ace11..44b9ced240f 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -496,6 +496,10 @@ impl TimelineStateTransaction<'_> { .await; } + VectorDiff::Remove { index: event_index } => { + self.remove_timeline_item(event_index, &mut day_divider_adjuster); + } + VectorDiff::Clear => { self.clear(); } @@ -503,6 +507,9 @@ impl TimelineStateTransaction<'_> { v => unimplemented!("{v:?}"), } } + + self.adjust_day_dividers(day_divider_adjuster); + self.check_no_unused_unique_ids(); } fn check_no_unused_unique_ids(&self) { @@ -739,6 +746,34 @@ impl TimelineStateTransaction<'_> { TimelineEventHandler::new(self, ctx).handle_event(date_divider_adjuster, event_kind).await } + /// Remove one timeline item by its `event_index`. + fn remove_timeline_item( + &mut self, + event_index: usize, + day_divider_adjuster: &mut DayDividerAdjuster, + ) { + day_divider_adjuster.mark_used(); + + // We need to be careful here. + // + // We must first remove the timeline item, which will update the mapping between + // remote events and timeline items. Removing the timeline item will “unlink” + // this mapping as the remote event will be updated to map to nothing. Only + // after that, we can remove the remote event. Doing this in the other order + // will update the mapping twice, and will result in a corrupted state. + + // Remove the timeline item first. + if let Some(event_meta) = self.items.all_remote_events().get(event_index) { + // Fetch the `timeline_item_index` associated to the remote event. + if let Some(timeline_item_index) = event_meta.timeline_item_index { + let _removed_timeline_item = self.items.remove(timeline_item_index); + } + + // Now we can remove the remote event. + self.items.remove_remote_event(event_index); + } + } + fn clear(&mut self) { let has_local_echoes = self.items.iter().any(|item| item.is_local_echo()); From c1ff5ff49f79526aa03f3354c4353de3ad2f363d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 13 Dec 2024 09:10:08 +0100 Subject: [PATCH 848/979] refactor(ui): Deduplicate remote events conditionnally. The `Timeline` has its own remote event deduplication mechanism. But we are transitioning to receive updates from the `EventCache` via `VectorDiff`, which are emitted via `RoomEvents`, which already runs its own deduplication mechanism (`matrix_sdk::event_cache::Deduplicator`). Deduplication from the `EventCache` will generate `VectorDiff::Remove` for example. It can create a conflict with the `Timeline` deduplication mechanism. This patch updates the deduplication mechanism from the `Timeline` when adding or updating remote events to be conditionnal: when `TimelineSettings::vectordiffs_as_inputs` is enabled, the deduplication mechanism of the `Timeline` is silent, it does nothing, otherwise it runs. --- .../src/timeline/controller/state.rs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 44b9ced240f..5a95e670d19 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -842,33 +842,38 @@ impl TimelineStateTransaction<'_> { room_data_provider: &P, settings: &TimelineSettings, ) { - // Detect if an event already exists in [`ObservableItems::all_remote_events`]. - // - // Returns its position, in this case. - fn event_already_exists( + /// Remove duplicated events. + /// + /// If `VectorDiff`s are the inputs of the `Timeline`, this is not + /// necessary, as they are generated by the `EventCache`, which supports + /// its own deduplication algorithm. + fn deduplicate( new_event_id: &EventId, - all_remote_events: &AllRemoteEvents, - ) -> Option { - all_remote_events.iter().position(|EventMeta { event_id, .. }| event_id == new_event_id) + items: &mut ObservableItemsTransaction<'_>, + settings: &TimelineSettings, + ) { + if settings.vectordiffs_as_inputs { + return; + } + + if let Some(pos) = items + .all_remote_events() + .iter() + .position(|EventMeta { event_id, .. }| event_id == new_event_id) + { + items.remove_remote_event(pos); + } } match position { TimelineItemPosition::Start { .. } => { - if let Some(pos) = - event_already_exists(event_meta.event_id, self.items.all_remote_events()) - { - self.items.remove_remote_event(pos); - } + deduplicate(event_meta.event_id, &mut self.items, settings); self.items.push_front_remote_event(event_meta.base_meta()) } TimelineItemPosition::End { .. } => { - if let Some(pos) = - event_already_exists(event_meta.event_id, self.items.all_remote_events()) - { - self.items.remove_remote_event(pos); - } + deduplicate(event_meta.event_id, &mut self.items, settings); self.items.push_back_remote_event(event_meta.base_meta()); } From 39afb531ef4624cde51847712fa0ba6c28d08bbd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 13 Dec 2024 09:47:21 +0100 Subject: [PATCH 849/979] task(ui): `DayDivider` has been renamed `DateDivider`. This patch updates this branch to `main` where `DayDivider` has been renamed `DateDivider`. --- .../src/timeline/controller/state.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 5a95e670d19..69baa6f8985 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -446,7 +446,8 @@ impl TimelineStateTransaction<'_> { ) where RoomData: RoomDataProvider, { - let mut day_divider_adjuster = DayDividerAdjuster::default(); + let mut date_divider_adjuster = + DateDividerAdjuster::new(settings.date_divider_mode.clone()); for diff in diffs { match diff { @@ -457,7 +458,7 @@ impl TimelineStateTransaction<'_> { TimelineItemPosition::End { origin }, room_data_provider, settings, - &mut day_divider_adjuster, + &mut date_divider_adjuster, ) .await; } @@ -469,7 +470,7 @@ impl TimelineStateTransaction<'_> { TimelineItemPosition::Start { origin }, room_data_provider, settings, - &mut day_divider_adjuster, + &mut date_divider_adjuster, ) .await; } @@ -480,7 +481,7 @@ impl TimelineStateTransaction<'_> { TimelineItemPosition::End { origin }, room_data_provider, settings, - &mut day_divider_adjuster, + &mut date_divider_adjuster, ) .await; } @@ -491,13 +492,13 @@ impl TimelineStateTransaction<'_> { TimelineItemPosition::At { event_index, origin }, room_data_provider, settings, - &mut day_divider_adjuster, + &mut date_divider_adjuster, ) .await; } VectorDiff::Remove { index: event_index } => { - self.remove_timeline_item(event_index, &mut day_divider_adjuster); + self.remove_timeline_item(event_index, &mut date_divider_adjuster); } VectorDiff::Clear => { @@ -508,7 +509,7 @@ impl TimelineStateTransaction<'_> { } } - self.adjust_day_dividers(day_divider_adjuster); + self.adjust_date_dividers(date_divider_adjuster); self.check_no_unused_unique_ids(); } From 38e35b99d01fe2b69a1e06c183da64bada1f0461 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 13 Dec 2024 09:56:08 +0100 Subject: [PATCH 850/979] test(ui): Increase the `recursion_limit`. Since we have added a new variant to `RoomEventCacheUpdate`, a macro hits the recursion limit. It needs to be updated in order for tests to run again. --- crates/matrix-sdk-ui/tests/integration/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk-ui/tests/integration/main.rs b/crates/matrix-sdk-ui/tests/integration/main.rs index da8bc38c3ee..59158ececd2 100644 --- a/crates/matrix-sdk-ui/tests/integration/main.rs +++ b/crates/matrix-sdk-ui/tests/integration/main.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![recursion_limit = "256"] + use itertools::Itertools as _; use matrix_sdk::deserialized_responses::TimelineEvent; use ruma::{events::AnyStateEvent, serde::Raw, EventId, RoomId}; From 054f5e28f627cbb1cfbcc629ef3aa031c1d38d99 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 18 Dec 2024 12:24:07 +0100 Subject: [PATCH 851/979] fix(common): Use a trick to avoid hitting the `recursion_limit` too quickly. This patch adds a trick around `SyncTimelineEvent` to avoid reaching the `recursion_limit` too quickly. Read the documentation in this patch to learn more. --- crates/matrix-sdk-base/Cargo.toml | 5 ++- crates/matrix-sdk-common/Cargo.toml | 4 +++ .../src/deserialized_responses.rs | 33 +++++++++++++++++++ .../matrix-sdk-ui/tests/integration/main.rs | 2 -- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 825c524062a..363b1c8036c 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -30,7 +30,10 @@ uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"] # Private feature, see # https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory # details. -test-send-sync = ["matrix-sdk-crypto?/test-send-sync"] +test-send-sync = [ + "matrix-sdk-common/test-send-sync", + "matrix-sdk-crypto?/test-send-sync", +] # "message-ids" feature doesn't do anything and is deprecated. message-ids = [] diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 6b7fdb0eb9d..a70573eaf82 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -18,6 +18,10 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [features] js = ["wasm-bindgen-futures"] uniffi = ["dep:uniffi"] +# Private feature, see +# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory +# details. +test-send-sync = [] [dependencies] async-trait = { workspace = true } diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 0833cf3f4e9..69458887539 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -308,6 +308,22 @@ pub struct EncryptionInfo { /// Previously, this differed from [`TimelineEvent`] by wrapping an /// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays /// they are essentially identical, and one of them should probably be removed. +// +// 🚨 Note about this type, please read! 🚨 +// +// `SyncTimelineEvent` is heavily used across the SDK crates. In some cases, we +// are reaching a [`recursion_limit`] when the compiler is trying to figure out +// if `SyncTimelineEvent` implements `Sync` when it's embedded in other types. +// +// We want to help the compiler so that one doesn't need to increase the +// `recursion_limit`. We stop the recursive check by (un)safely implement `Sync` +// and `Send` on `SyncTimelineEvent` directly. +// +// See +// https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 +// which has addressed this issue first +// +// [`recursion_limit`]: https://doc.rust-lang.org/reference/attributes/limits.html#the-recursion_limit-attribute #[derive(Clone, Debug, Serialize)] pub struct SyncTimelineEvent { /// The event itself, together with any information on decryption. @@ -318,6 +334,23 @@ pub struct SyncTimelineEvent { pub push_actions: Vec, } +// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. +#[cfg(not(feature = "test-send-sync"))] +unsafe impl Send for SyncTimelineEvent {} + +// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. +#[cfg(not(feature = "test-send-sync"))] +unsafe impl Sync for SyncTimelineEvent {} + +#[cfg(feature = "test-send-sync")] +#[test] +// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. +fn test_send_sync_for_sync_timeline_event() { + fn assert_send_sync() {} + + assert_send_sync::(); +} + impl SyncTimelineEvent { /// Create a new `SyncTimelineEvent` from the given raw event. /// diff --git a/crates/matrix-sdk-ui/tests/integration/main.rs b/crates/matrix-sdk-ui/tests/integration/main.rs index 59158ececd2..da8bc38c3ee 100644 --- a/crates/matrix-sdk-ui/tests/integration/main.rs +++ b/crates/matrix-sdk-ui/tests/integration/main.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![recursion_limit = "256"] - use itertools::Itertools as _; use matrix_sdk::deserialized_responses::TimelineEvent; use ruma::{events::AnyStateEvent, serde::Raw, EventId, RoomId}; From d8dd72fd9cb814f0f65415783d1b5851e9ba9f25 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 16 Dec 2024 16:38:15 +0100 Subject: [PATCH 852/979] refactor(ui): Deduplicate timeline items conditionnally. A previous patch deduplicates the remote events conditionnally. This patch does the same but for timeline items. The `Timeline` has its own deduplication algorithm (for remote events, and for timeline items). The `Timeline` is about to receive its updates via the `EventCache` which has its own deduplication mechanism (`matrix_sdk::event_cache::Deduplicator`). To avoid conflicts between the two, we conditionnally deduplicate timeline items based on `TimelineSettings::vectordiffs_as_inputs`. This patch takes the liberty to refactor the deduplication mechanism of the timeline items to make it explicit with its own methods, so that it can be re-used for `TimelineItemPosition::At`. A specific short-circuit was present before, which is no more possible with the rewrite to a generic mechanism. Consequently, when a local timeline item becomes a remote timeline item, it was previously updated (via `ObservableItems::replace`), but now the local timeline item is removed (via `ObservableItems::remove`), and then the remote timeline item is inserted (via `ObservableItems::insert`). Depending of whether a virtual timeline item like a date divider is around, the position of the removal and the insertion might not be the same (!), which is perfectly fine as the date divider will be re-computed anyway. The result is exactly the same, but the `VectorDiff` updates emitted by the `Timeline` are a bit different (different paths, same result). This is why this patch needs to update a couple of tests. --- .../src/timeline/controller/mod.rs | 1 + .../src/timeline/controller/state.rs | 9 +- .../src/timeline/event_handler.rs | 237 ++++++++++++------ .../matrix-sdk-ui/src/timeline/tests/echo.rs | 23 +- .../src/timeline/tests/shields.rs | 12 +- .../tests/integration/timeline/echo.rs | 6 +- .../tests/integration/timeline/queue.rs | 9 +- .../src/tests/timeline.rs | 49 +++- 8 files changed, 234 insertions(+), 112 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 2422bdfa01b..52993074163 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -789,6 +789,7 @@ impl TimelineController

{ txn_id, send_handle, content, + &self.settings, ) .await; } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 69baa6f8985..6e8b9ce4e99 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -223,6 +223,7 @@ impl TimelineState { txn_id: OwnedTransactionId, send_handle: Option, content: TimelineEventKind, + settings: &TimelineSettings, ) { let ctx = TimelineEventContext { sender: own_user_id, @@ -240,7 +241,7 @@ impl TimelineState { let mut date_divider_adjuster = DateDividerAdjuster::new(date_divider_mode); - TimelineEventHandler::new(&mut txn, ctx) + TimelineEventHandler::new(&mut txn, ctx, settings) .handle_event(&mut date_divider_adjuster, content) .await; @@ -744,14 +745,16 @@ impl TimelineStateTransaction<'_> { }; // Handle the event to create or update a timeline item. - TimelineEventHandler::new(self, ctx).handle_event(date_divider_adjuster, event_kind).await + TimelineEventHandler::new(self, ctx, settings) + .handle_event(date_divider_adjuster, event_kind) + .await } /// Remove one timeline item by its `event_index`. fn remove_timeline_item( &mut self, event_index: usize, - day_divider_adjuster: &mut DayDividerAdjuster, + day_divider_adjuster: &mut DateDividerAdjuster, ) { day_divider_adjuster.mark_used(); diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 3cb790740ea..482a8d3073e 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -46,13 +46,14 @@ use ruma::{ }, serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, + TransactionId, }; use tracing::{debug, error, field::debug, info, instrument, trace, warn}; use super::{ controller::{ ObservableItemsTransaction, ObservableItemsTransactionEntry, PendingEdit, PendingEditKind, - TimelineMetadata, TimelineStateTransaction, + TimelineMetadata, TimelineSettings, TimelineStateTransaction, }, date_dividers::DateDividerAdjuster, event_item::{ @@ -337,15 +338,17 @@ pub(super) struct TimelineEventHandler<'a, 'o> { meta: &'a mut TimelineMetadata, ctx: TimelineEventContext, result: HandleEventResult, + settings: &'a TimelineSettings, } impl<'a, 'o> TimelineEventHandler<'a, 'o> { pub(super) fn new( state: &'a mut TimelineStateTransaction<'o>, ctx: TimelineEventContext, + settings: &'a TimelineSettings, ) -> Self { let TimelineStateTransaction { items, meta, .. } = state; - Self { items, meta, ctx, result: HandleEventResult::default() } + Self { items, meta, ctx, result: HandleEventResult::default(), settings } } /// Handle an event. @@ -1100,27 +1103,44 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("Adding new local timeline item"); let item = self.meta.new_timeline_item(item); + self.items.push_back(item, None); } - Flow::Remote { position: TimelineItemPosition::Start { .. }, event_id, .. } => { - if self - .items - .iter() - .filter_map(|ev| ev.as_event()?.event_id()) - .any(|id| id == event_id) - { - trace!("Skipping back-paginated event that has already been seen"); - return; - } + Flow::Remote { + position: TimelineItemPosition::Start { .. }, event_id, txn_id, .. + } => { + let removed_duplicated_timeline_item = Self::deduplicate_local_timeline_item( + self.items, + &mut item, + Some(event_id), + txn_id.as_ref().map(AsRef::as_ref), + self.meta, + self.settings, + ); + let item = new_timeline_item(self.meta, item, removed_duplicated_timeline_item); trace!("Adding new remote timeline item at the start"); - let item = self.meta.new_timeline_item(item); self.items.push_front(item, Some(0)); } - Flow::Remote { position: TimelineItemPosition::At { event_index, .. }, .. } => { + Flow::Remote { + position: TimelineItemPosition::At { event_index, .. }, + event_id, + txn_id, + .. + } => { + let removed_duplicated_timeline_item = Self::deduplicate_local_timeline_item( + self.items, + &mut item, + Some(event_id), + txn_id.as_ref().map(AsRef::as_ref), + self.meta, + self.settings, + ); + let item = new_timeline_item(self.meta, item, removed_duplicated_timeline_item); + let all_remote_events = self.items.all_remote_events(); let event_index = *event_index; @@ -1160,69 +1180,21 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { "Adding new remote timeline at specific event index" ); - let item = self.meta.new_timeline_item(item); self.items.insert(timeline_item_index, item, Some(event_index)); } Flow::Remote { - position: TimelineItemPosition::End { .. }, txn_id, event_id, .. + position: TimelineItemPosition::End { .. }, event_id, txn_id, .. } => { - // Look if we already have a corresponding item somewhere, based on the - // transaction id (if a local echo) or the event id (if a - // duplicate remote event). - let result = rfind_event_item(self.items, |it| { - txn_id.is_some() && it.transaction_id() == txn_id.as_deref() - || it.event_id() == Some(event_id) - }); - - let mut removed_event_item_id = None; - - if let Some((idx, old_item)) = result { - if old_item.as_remote().is_some() { - // Item was previously received from the server. This should be very rare - // normally, but with the sliding- sync proxy, it is actually very - // common. - // NOTE: SS proxy workaround. - trace!(?item, old_item = ?*old_item, "Received duplicate event"); - - if old_item.content.is_redacted() && !item.content.is_redacted() { - warn!("Got original form of an event that was previously redacted"); - item.content = item.content.redact(&self.meta.room_version); - item.reactions.clear(); - } - } - - // TODO: Check whether anything is different about the - // old and new item? - - transfer_details(&mut item, &old_item); - - let old_item_id = old_item.internal_id; - - if idx == self.items.len() - 1 { - // If the old item is the last one and no date divider - // changes need to happen, replace and return early. - trace!(idx, "Replacing existing event"); - self.items.replace(idx, TimelineItem::new(item, old_item_id.to_owned())); - return; - } - - // In more complex cases, remove the item before re-adding the item. - trace!("Removing local echo or duplicate timeline item"); - removed_event_item_id = Some(self.items.remove(idx).internal_id.clone()); - - // no return here, below code for adding a new event - // will run to re-add the removed item - } - - trace!("Adding new remote timeline item after all non-pending events"); - let new_item = match removed_event_item_id { - // If a previous version of the same item (usually a local - // echo) was removed and we now need to add it again, reuse - // the previous item's ID. - Some(id) => TimelineItem::new(item, id), - None => self.meta.new_timeline_item(item), - }; + let removed_duplicated_timeline_item = Self::deduplicate_local_timeline_item( + self.items, + &mut item, + Some(event_id), + txn_id.as_ref().map(AsRef::as_ref), + self.meta, + self.settings, + ); + let item = new_timeline_item(self.meta, item, removed_duplicated_timeline_item); // Local events are always at the bottom. Let's find the latest remote event // and insert after it, otherwise, if there is no remote event, insert at 0. @@ -1260,16 +1232,16 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { if timeline_item_index == self.items.len() { trace!("Adding new remote timeline item at the back"); - self.items.push_back(new_item, event_index); + self.items.push_back(item, event_index); } else if timeline_item_index == 0 { trace!("Adding new remote timeline item at the front"); - self.items.push_front(new_item, event_index); + self.items.push_front(item, event_index); } else { trace!( timeline_item_index, "Adding new remote timeline item at specific index" ); - self.items.insert(timeline_item_index, new_item, event_index); + self.items.insert(timeline_item_index, item, event_index); } } @@ -1294,6 +1266,86 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } + /// Remove the local timeline item matching the `event_id` or the + /// `transaction_id` of `new_event_timeline_item` if it exists. + /// + /// Let's also try to deduplicate remote events. If `VectorDiff`s are the + /// inputs of the `Timeline`, this is not necessary, as they are + /// generated by the `EventCache`, which supports its own deduplication + /// algorithm. + // + // Note: this method doesn't take `&mut self` to avoid a borrow checker + // conflict with `TimelineEventHandler::add_item`. + fn deduplicate_local_timeline_item( + items: &mut ObservableItemsTransaction<'_>, + new_event_timeline_item: &mut EventTimelineItem, + event_id: Option<&EventId>, + transaction_id: Option<&TransactionId>, + metadata: &TimelineMetadata, + settings: &TimelineSettings, + ) -> Option> { + // Start with the canonical case: detect a local timeline item that matches + // `event_id` or `transaction_id`. + if let Some((local_timeline_item_index, local_timeline_item)) = + rfind_event_item(items, |event_timeline_item| { + if event_timeline_item.is_local_echo() { + event_id == event_timeline_item.event_id() + || (transaction_id.is_some() + && transaction_id == event_timeline_item.transaction_id()) + } else { + false + } + }) + { + trace!( + ?event_id, + ?transaction_id, + ?local_timeline_item_index, + "Removing local timeline item" + ); + + transfer_details(new_event_timeline_item, &local_timeline_item); + + // Remove the local timeline item. + return Some(items.remove(local_timeline_item_index)); + }; + + if !settings.vectordiffs_as_inputs { + if let Some((remote_timeline_item_index, remote_timeline_item)) = + rfind_event_item(items, |event_timeline_item| { + if event_timeline_item.is_remote_event() { + event_id == event_timeline_item.event_id() + } else { + false + } + }) + { + trace!( + ?event_id, + ?transaction_id, + ?remote_timeline_item_index, + "Removing remote timeline item (it is a duplicate)" + ); + + if remote_timeline_item.content.is_redacted() + && !new_event_timeline_item.content.is_redacted() + { + warn!("Got original form of an event that was previously redacted"); + new_event_timeline_item.content = + new_event_timeline_item.content.redact(&metadata.room_version); + new_event_timeline_item.reactions.clear(); + } + + transfer_details(new_event_timeline_item, &remote_timeline_item); + + // Remove the remote timeline item. + return Some(items.remove(remote_timeline_item_index)); + } + } + + None + } + /// After updating the timeline item `new_item` which id is /// `target_event_id`, update other items that are responses to this item. fn maybe_update_responses( @@ -1353,14 +1405,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } } -/// Transfer `TimelineDetails` that weren't available on the original item and -/// have been fetched separately (only `reply_to` for now) from `old_item` to -/// `item`, given two items for an event that was re-received. +/// Transfer `TimelineDetails` that weren't available on the original +/// item and have been fetched separately (only `reply_to` for +/// now) from `old_item` to `item`, given two items for an event +/// that was re-received. /// -/// `old_item` *should* always be a local echo usually, but with the sliding -/// sync proxy, we often re-receive remote events that aren't remote echoes. -fn transfer_details(item: &mut EventTimelineItem, old_item: &EventTimelineItem) { - let TimelineItemContent::Message(msg) = &mut item.content else { return }; +/// `old_item` *should* always be a local timeline item usually, but it +/// can be a remote timeline item. +fn transfer_details(new_item: &mut EventTimelineItem, old_item: &EventTimelineItem) { + let TimelineItemContent::Message(msg) = &mut new_item.content else { return }; let TimelineItemContent::Message(old_msg) = &old_item.content else { return }; let Some(in_reply_to) = &mut msg.in_reply_to else { return }; @@ -1370,3 +1423,23 @@ fn transfer_details(item: &mut EventTimelineItem, old_item: &EventTimelineItem) in_reply_to.event = old_in_reply_to.event.clone(); } } + +/// Create a new timeline item from an [`EventTimelineItem`]. +/// +/// It is possible that the new timeline item replaces a duplicated timeline +/// event (see [`TimelineEventHandler::deduplicate_local_timeline_item`]) in +/// case it replaces a local timeline item. +fn new_timeline_item( + metadata: &mut TimelineMetadata, + event_timeline_item: EventTimelineItem, + replaced_timeline_item: Option>, +) -> Arc { + match replaced_timeline_item { + // Reuse the internal ID. + Some(to_replace_timeline_item) => { + TimelineItem::new(event_timeline_item, to_replace_timeline_item.internal_id.clone()) + } + + None => metadata.new_timeline_item(event_timeline_item), + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index 533c1c2c56d..683f643ec85 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -24,7 +24,7 @@ use ruma::{ events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, user_id, MilliSecondsSinceUnixEpoch, }; -use stream_assert::assert_next_matches; +use stream_assert::{assert_next_matches, assert_pending}; use super::TestTimeline; use crate::timeline::{ @@ -121,9 +121,18 @@ async fn test_remote_echo_full_trip() { .await; // The local echo is replaced with the remote echo. - let item = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value); + assert_next_matches!(stream, VectorDiff::Remove { index: 1 }); + let item = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); assert!(!item.as_event().unwrap().is_local_echo()); assert_eq!(*item.unique_id(), id); + + // The date divider is adjusted. + // A new date divider is inserted, and the older one is removed. + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); + assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); + + assert_pending!(stream); } #[async_test] @@ -168,12 +177,14 @@ async fn test_remote_echo_new_position() { .await; // … the remote echo replaces the previous event. - let item = assert_next_matches!(stream, VectorDiff::Set { index: 3, value } => value); + assert_next_matches!(stream, VectorDiff::Remove { index: 3 }); + let item = assert_next_matches!(stream, VectorDiff::Insert { index: 2, value} => value); assert!(!item.as_event().unwrap().is_local_echo()); - // … the date divider is removed (because both bob's and alice's message are - // from the same day according to server timestamps). - assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); + // Date divider is updated. + assert_next_matches!(stream, VectorDiff::Remove { index: 3 }); + + assert_pending!(stream); } #[async_test] diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index b87177e4ed0..c7f50c26731 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -14,7 +14,7 @@ use ruma::{ AnyMessageLikeEventContent, }, }; -use stream_assert::assert_next_matches; +use stream_assert::{assert_next_matches, assert_pending}; use crate::timeline::{tests::TestTimeline, EventSendState}; @@ -108,7 +108,8 @@ async fn test_local_sent_in_clear_shield() { "type": "m.room.message", }))) .await; - let item = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value); + assert_next_matches!(stream, VectorDiff::Remove { index: 1 }); + let item = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); let event_item = item.as_event().unwrap(); // Then the remote echo should now be showing the shield. @@ -118,6 +119,13 @@ async fn test_local_sent_in_clear_shield() { shield, Some(ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }) ); + + // Date divider is adjusted. + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + assert!(date_divider.is_date_divider()); + assert_next_matches!(stream, VectorDiff::Remove { index: 2 }); + + assert_pending!(stream); } #[async_test] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs index f00ad135d05..2d715963ab4 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs @@ -121,16 +121,18 @@ async fn test_echo() { server.reset().await; // Local echo is replaced with the remote echo. + assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 1 }); let remote_echo = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => value); + assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => value); let item = remote_echo.as_event().unwrap(); assert!(item.is_own()); assert_eq!(item.timestamp(), MilliSecondsSinceUnixEpoch(uint!(152038280))); // The date divider is also replaced. let date_divider = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => value); + assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => value); assert!(date_divider.is_date_divider()); + assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); } #[async_test] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs index 32a11923dfc..d9d27689180 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs @@ -508,20 +508,21 @@ async fn test_no_duplicate_date_divider() { assert_eq!(value.event_id().unwrap(), "$PyHxV5mYzjetBUT3qZq7V95GOzxb02EP"); }); - // The second message is replaced -> [First DD Second] - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => { + // The second message is replaced -> [First Second DD] + assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); + assert_next_matches!(timeline_stream, VectorDiff::Insert { index: 1, value } => { let value = value.as_event().unwrap(); assert_eq!(value.content().as_message().unwrap().body(), "Second."); assert_eq!(value.event_id().unwrap(), "$5E2kLK/Sg342bgBU9ceEIEPYpbFaqJpZ"); }); - // A new date divider is inserted -> [DD First DD Second] + // A new date divider is inserted -> [DD First Second DD] assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { assert!(value.is_date_divider()); }); // The useless date divider is removed. -> [DD First Second] - assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); + assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 3 }); assert_pending!(timeline_stream); } diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 7a2dcdc725f..723f71bee08 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -41,6 +41,7 @@ use matrix_sdk_ui::{ timeline::{EventSendState, ReactionStatus, RoomExt, TimelineItem, TimelineItemContent}, }; use similar_asserts::assert_eq; +use stream_assert::assert_pending; use tokio::{ spawn, task::JoinHandle, @@ -274,22 +275,42 @@ async fn test_stale_local_echo_time_abort_edit() { // - or the remote echo comes up faster. // // Handle both orderings. - while let Ok(Some(vector_diff)) = timeout(Duration::from_secs(3), stream.next()).await { - let VectorDiff::Set { index: 0, value: echo } = vector_diff else { - panic!("unexpected diff: {vector_diff:#?}"); - }; + { + let mut diffs = Vec::with_capacity(3); + + while let Ok(Some(vector_diff)) = timeout(Duration::from_secs(5), stream.next()).await { + diffs.push(vector_diff); + } + + assert!(diffs.len() >= 3); + + for diff in diffs { + match diff { + VectorDiff::Set { index: 0, value: event } + | VectorDiff::PushBack { value: event } + | VectorDiff::Insert { index: 0, value: event } => { + if event.is_local_echo() { + // If the sender profile wasn't available, we may receive an update about + // it; ignore it. + if !has_sender_profile && event.sender_profile().is_ready() { + has_sender_profile = true; + continue; + } + + assert_matches!(event.send_state(), Some(EventSendState::Sent { .. })); + } + + assert!(event.is_editable()); + assert_eq!(event.content().as_message().unwrap().body(), "hi!"); + } + + VectorDiff::Remove { index } => assert_eq!(index, 0), - if echo.is_local_echo() { - // If the sender profile wasn't available, we may receive an update about it; - // ignore it. - if !has_sender_profile && echo.sender_profile().is_ready() { - has_sender_profile = true; - continue; + diff => { + panic!("unexpected diff: {diff:?}"); + } } - assert_matches!(echo.send_state(), Some(EventSendState::Sent { .. })); } - assert!(echo.is_editable()); - assert_eq!(echo.content().as_message().unwrap().body(), "hi!"); } // Now do a crime: try to edit the local echo. @@ -310,6 +331,8 @@ async fn test_stale_local_echo_time_abort_edit() { assert_eq!(remote_echo.content().as_message().unwrap().body(), "bonjour"); alice_sync.abort(); + + assert_pending!(stream); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] From f1842ba5d07a45c7115dd2da849de489191faae1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 17 Dec 2024 17:30:44 +0100 Subject: [PATCH 853/979] refactor(ui): `Timeline` receives pagination events as `VectorDiff`s! This patch allows the paginated events of a `Timeline` to be received via `RoomEventCacheUpdate::UpdateTimelineEvents` as `VectorDiff`s. --- .../src/timeline/controller/mod.rs | 2 +- .../matrix-sdk-ui/src/timeline/pagination.rs | 16 +++++++----- .../matrix-sdk/src/event_cache/pagination.rs | 11 +++++++- .../tests/integration/event_cache.rs | 25 ++++++++++++++++++- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 52993074163..36409f66222 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -124,7 +124,7 @@ pub(super) struct TimelineController { pub(crate) room_data_provider: P, /// Settings applied to this timeline. - settings: TimelineSettings, + pub(super) settings: TimelineSettings, } #[derive(Clone)] diff --git a/crates/matrix-sdk-ui/src/timeline/pagination.rs b/crates/matrix-sdk-ui/src/timeline/pagination.rs index abe49acac0f..175ffe46a83 100644 --- a/crates/matrix-sdk-ui/src/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/src/timeline/pagination.rs @@ -77,12 +77,16 @@ impl super::Timeline { let num_events = events.len(); trace!("Back-pagination succeeded with {num_events} events"); - // TODO(hywan): Remove, and let spread events via - // `matrix_sdk::event_cache::RoomEventCacheUpdate` from - // `matrix_sdk::event_cache::RoomPagination::run_backwards`. - self.controller - .add_events_at(events.into_iter(), TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }) - .await; + // If `TimelineSettings::vectordiffs_as_inputs` is enabled, + // we don't need to add events manually: everything we need + // is to let the `EventCache` receive the events from this + // pagination, and emit its updates as `VectorDiff`s, which + // will be handled by the `Timeline` naturally. + if !self.controller.settings.vectordiffs_as_inputs { + self.controller + .add_events_at(events.into_iter(), TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }) + .await; + } if num_events == 0 && !reached_start { // As an exceptional contract: if there were no events in the response, diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 180a03c12bc..3c14f3dd34d 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -28,7 +28,7 @@ use super::{ events::{Gap, RoomEvents}, RoomEventCacheInner, }, - BackPaginationOutcome, Result, + BackPaginationOutcome, EventsOrigin, Result, RoomEventCacheUpdate, }; /// An API object to run pagination queries on a [`super::RoomEventCache`]. @@ -227,6 +227,15 @@ impl RoomPagination { debug!("not storing previous batch token, because we deduplicated all new back-paginated events"); } + let sync_timeline_events_diffs = room_events.updates_as_vector_diffs(); + + if !sync_timeline_events_diffs.is_empty() { + let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents { + diffs: sync_timeline_events_diffs, + origin: EventsOrigin::Sync, + }); + } + BackPaginationOutcome { events, reached_start } }) .await?; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 98ee29db207..5f9f76782d9 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -281,6 +281,9 @@ async fn test_backpaginate_once() { assert_event_matches_msg(&events[1], "hello"); assert_eq!(events.len(), 2); + let next = room_stream.recv().now_or_never(); + assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + let next = room_stream.recv().now_or_never(); assert_matches!(next, None); } @@ -400,6 +403,14 @@ async fn test_backpaginate_many_times_with_many_iterations() { assert_event_matches_msg(&events[3], "heyo"); assert_eq!(events.len(), 4); + // First iteration. + let next = room_stream.recv().now_or_never(); + assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + + // Second iteration. + let next = room_stream.recv().now_or_never(); + assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + assert!(room_stream.is_empty()); } @@ -523,6 +534,14 @@ async fn test_backpaginate_many_times_with_one_iteration() { assert_event_matches_msg(&events[3], "heyo"); assert_eq!(events.len(), 4); + // First pagination. + let next = room_stream.recv().now_or_never(); + assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + + // Second pagination. + let next = room_stream.recv().now_or_never(); + assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + assert!(room_stream.is_empty()); } @@ -680,7 +699,7 @@ async fn test_backpaginating_without_token() { let room = server.sync_joined_room(&client, room_id).await; let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); - let (events, room_stream) = room_event_cache.subscribe().await.unwrap(); + let (events, mut room_stream) = room_event_cache.subscribe().await.unwrap(); assert!(events.is_empty()); assert!(room_stream.is_empty()); @@ -712,6 +731,9 @@ async fn test_backpaginating_without_token() { assert_event_matches_msg(&events[0], "hi"); assert_eq!(events.len(), 1); + let next = room_stream.recv().now_or_never(); + assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + assert!(room_stream.is_empty()); } @@ -772,6 +794,7 @@ async fn test_limited_timeline_resets_pagination() { server.sync_room(&client, JoinedRoomBuilder::new(room_id).set_timeline_limited()).await; // We receive an update about the limited timeline. + assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = room_stream.recv()); assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = room_stream.recv()); // The paginator state is reset: status set to Initial, hasn't hit the timeline From 51c76a15ad4158df7db8daca64ec9af0df132faf Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 18 Dec 2024 11:05:53 +0100 Subject: [PATCH 854/979] chore(ui): Make Clippy happy. --- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 6e8b9ce4e99..4eb4f019f3e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -798,7 +798,7 @@ impl TimelineStateTransaction<'_> { let mut idx = 0; while idx < self.items.len() { if self.items[idx].is_date_divider() - && self.items.get(idx + 1).map_or(true, |item| item.is_date_divider()) + && self.items.get(idx + 1).is_none_or(|item| item.is_date_divider()) { self.items.remove(idx); // don't increment idx because all elements have shifted From c4132252d36b72f4a71e614451cc98f24853e3a4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 20 Dec 2024 10:27:48 +0100 Subject: [PATCH 855/979] feat(ui): Enable `TimelineSettings::vectordiffs_as_inputs` if event cache storage is enabled. This patch automatically enables `TimelineSettings::vectordiffs_as_inputs` if and only if the event cache storage is enabled. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 9 +++++++-- crates/matrix-sdk/src/event_cache/mod.rs | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index ab850666ef2..2032ad5f4d2 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -162,12 +162,17 @@ impl TimelineBuilder { ) )] pub async fn build(self) -> Result { - let Self { room, settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self; - let settings_vectordiffs_as_inputs = settings.vectordiffs_as_inputs; + let Self { room, mut settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self; let client = room.client(); let event_cache = client.event_cache(); + // Enable `TimelineSettings::vectordiffs_as_inputs` if and only if the event + // cache storage is enabled. + settings.vectordiffs_as_inputs = event_cache.has_storage(); + + let settings_vectordiffs_as_inputs = settings.vectordiffs_as_inputs; + // Subscribe the event cache to sync responses, in case we hadn't done it yet. event_cache.subscribe()?; diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 20a3e90c359..6f288583992 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -175,6 +175,11 @@ impl EventCache { Ok(()) } + /// Check whether the storage is enabled or not. + pub fn has_storage(&self) -> bool { + self.inner.has_storage() + } + /// Starts subscribing the [`EventCache`] to sync responses, if not done /// before. /// From 1abb2efc51198954519230c229244e82b520a5f8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 20 Dec 2024 10:33:27 +0100 Subject: [PATCH 856/979] refactor(sdk): Rename two variables. --- crates/matrix-sdk/src/event_cache/room/mod.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 3ac7278a3ff..93c30e7513c 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -561,11 +561,15 @@ impl RoomEventCacheInner { }) .await?; - let mut cache = self.all_events.write().await; - for ev in &sync_timeline_events { - if let Some(event_id) = ev.event_id() { - self.append_related_event(&mut cache, ev); - cache.events.insert(event_id.to_owned(), (self.room_id.clone(), ev.clone())); + let mut all_events = self.all_events.write().await; + + for sync_timeline_event in &sync_timeline_events { + if let Some(event_id) = sync_timeline_event.event_id() { + self.append_related_event(&mut all_events, sync_timeline_event); + all_events.events.insert( + event_id.to_owned(), + (self.room_id.clone(), sync_timeline_event.clone()), + ); } } From f4b50db97233e14d8b39bd1d450c7514face62fd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 20 Dec 2024 12:29:36 +0100 Subject: [PATCH 857/979] test: Increase timeout for codecoverage. --- .../src/tests/timeline.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 723f71bee08..ebc049be649 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -47,7 +47,7 @@ use tokio::{ task::JoinHandle, time::{sleep, timeout}, }; -use tracing::{debug, warn}; +use tracing::{debug, trace, warn}; use crate::helpers::TestClientBuilder; @@ -278,11 +278,13 @@ async fn test_stale_local_echo_time_abort_edit() { { let mut diffs = Vec::with_capacity(3); - while let Ok(Some(vector_diff)) = timeout(Duration::from_secs(5), stream.next()).await { + while let Ok(Some(vector_diff)) = timeout(Duration::from_secs(15), stream.next()).await { diffs.push(vector_diff); } - assert!(diffs.len() >= 3); + trace!(?diffs, "Received diffs"); + + assert!(diffs.len() >= 2); for diff in diffs { match diff { @@ -642,7 +644,7 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { .expect("We should be able toe get a notification item for the given event"); // Alright, we should now receive an update that the event had been decrypted. - let _vector_diff = timeout(Duration::from_secs(5), stream.next()).await.unwrap().unwrap(); + let _vector_diff = timeout(Duration::from_secs(10), stream.next()).await.unwrap().unwrap(); // Let's fetch the event again. let item = From 667a8e684c116d291bef4ee07951c7d67ebe66fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 20 Dec 2024 11:46:16 +0100 Subject: [PATCH 858/979] chore: Fix a typo in the changelog --- crates/matrix-sdk/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index ba1803f3daa..544ffd81973 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -10,7 +10,7 @@ All notable changes to this project will be documented in this file. - [**breaking**] Move the optional `RequestConfig` argument of the `Client::send()` method to the `with_request_config()` builder method. You - should call `Client::send(request).with_request_config(request_config).awat` + should call `Client::send(request).with_request_config(request_config).await` now instead. ## [0.9.0] - 2024-12-18 From adb4428a69432eca3845ef2fb2dabad4d8dcfdf1 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 20 Dec 2024 19:03:06 +0100 Subject: [PATCH 859/979] test(crypto): Add some basic snapshot testing in crypto crate --- Cargo.lock | 1 + crates/matrix-sdk-crypto/Cargo.toml | 1 + crates/matrix-sdk-crypto/src/lib.rs | 21 +++++ .../src/olm/group_sessions/sender_data.rs | 57 +++++++++++++- ...r_data__tests__snapshot_sender_data-2.snap | 9 +++ ...r_data__tests__snapshot_sender_data-3.snap | 21 +++++ ...r_data__tests__snapshot_sender_data-4.snap | 44 +++++++++++ ...r_data__tests__snapshot_sender_data-5.snap | 44 +++++++++++ ...r_data__tests__snapshot_sender_data-6.snap | 44 +++++++++++ ...der_data__tests__snapshot_sender_data.snap | 10 +++ ...o__test__snapshot_decryption_settings.snap | 7 ++ ...o__test__snapshot_trust_requirement-2.snap | 5 ++ ...o__test__snapshot_trust_requirement-3.snap | 5 ++ ...pto__test__snapshot_trust_requirement.snap | 5 ++ crates/matrix-sdk-crypto/src/types/backup.rs | 36 ++++++++- crates/matrix-sdk-crypto/src/types/mod.rs | 77 +++++++++++++++++++ ...ests__snapshot_room_key_backup_info-2.snap | 10 +++ ..._tests__snapshot_room_key_backup_info.snap | 16 ++++ ...est__snapshot_backup_decryption_key-2.snap | 7 ++ ..._test__snapshot_backup_decryption_key.snap | 38 +++++++++ ...types__test__snapshot_secret_bundle-2.snap | 12 +++ ...__types__test__snapshot_secret_bundle.snap | 16 ++++ ...pto__types__test__snapshot_signatures.snap | 13 ++++ 23 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-2.snap create mode 100644 crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-3.snap create mode 100644 crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap create mode 100644 crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap create mode 100644 crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap create mode 100644 crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data.snap create mode 100644 crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_decryption_settings.snap create mode 100644 crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-2.snap create mode 100644 crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-3.snap create mode 100644 crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement.snap create mode 100644 crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info-2.snap create mode 100644 crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info.snap create mode 100644 crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key-2.snap create mode 100644 crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key.snap create mode 100644 crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle-2.snap create mode 100644 crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle.snap create mode 100644 crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_signatures.snap diff --git a/Cargo.lock b/Cargo.lock index f8d57bef3a6..9ae5eb6e07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3230,6 +3230,7 @@ dependencies = [ "hmac", "http", "indoc", + "insta", "itertools 0.13.0", "js_option", "matrix-sdk-common", diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index b3e3ccd1155..551c89a9bac 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -85,6 +85,7 @@ assert_matches2 = { workspace = true } futures-executor = { workspace = true } http = { workspace = true } indoc = "2.0.5" +insta = { workspace = true } matrix-sdk-test = { workspace = true } proptest = { workspace = true } similar-asserts = { workspace = true } diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 04079868a98..611cadcb1d3 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -1093,3 +1093,24 @@ pub enum RoomEventDecryptionResult { /// /// [1]: https://spec.matrix.org/unstable/client-server-api/#server-behaviour-4 pub mod tutorial {} + +#[cfg(test)] +mod test { + use insta::assert_json_snapshot; + + use crate::{DecryptionSettings, TrustRequirement}; + + #[test] + fn snapshot_trust_requirement() { + assert_json_snapshot!(TrustRequirement::Untrusted); + assert_json_snapshot!(TrustRequirement::CrossSignedOrLegacy); + assert_json_snapshot!(TrustRequirement::CrossSigned); + } + + #[test] + fn snapshot_decryption_settings() { + assert_json_snapshot!(DecryptionSettings { + sender_device_trust_requirement: TrustRequirement::Untrusted, + }); + } +} diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index 0be07403e00..d10758546d1 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -283,13 +283,16 @@ mod tests { use std::{cmp::Ordering, collections::BTreeMap}; use assert_matches2::assert_let; - use ruma::{device_id, owned_device_id, owned_user_id, user_id}; - use vodozemac::Ed25519PublicKey; + use insta::assert_json_snapshot; + use ruma::{ + device_id, owned_device_id, owned_user_id, user_id, DeviceKeyAlgorithm, DeviceKeyId, + }; + use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; use super::SenderData; use crate::{ olm::KnownSenderData, - types::{DeviceKeys, Signatures}, + types::{DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures}, }; #[test] @@ -479,4 +482,52 @@ mod tests { assert_eq!(sender_unverified.compare_trust_level(&sender_verified), Ordering::Less); assert_eq!(sender_verified.compare_trust_level(&sender_unverified), Ordering::Greater); } + + #[test] + fn snapshot_sender_data() { + assert_json_snapshot!(SenderData::UnknownDevice { + legacy_session: false, + owner_check_failed: true, + }); + + assert_json_snapshot!(SenderData::UnknownDevice { + legacy_session: true, + owner_check_failed: false, + }); + + assert_json_snapshot!(SenderData::DeviceInfo { + device_keys: DeviceKeys::new( + owned_user_id!("@foo:bar.baz"), + owned_device_id!("DEV"), + vec![ + EventEncryptionAlgorithm::MegolmV1AesSha2, + EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 + ], + BTreeMap::from_iter(vec![( + DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, device_id!("ABCDEFGH")), + DeviceKey::Curve25519(Curve25519PublicKey::from_bytes([0u8; 32])), + )]), + Default::default(), + ), + legacy_session: false, + }); + + assert_json_snapshot!(SenderData::VerificationViolation(KnownSenderData { + user_id: owned_user_id!("@foo:bar.baz"), + device_id: Some(owned_device_id!("DEV")), + master_key: Box::new(Ed25519PublicKey::from_slice(&[0u8; 32]).unwrap()), + })); + + assert_json_snapshot!(SenderData::SenderUnverified(KnownSenderData { + user_id: owned_user_id!("@foo:bar.baz"), + device_id: None, + master_key: Box::new(Ed25519PublicKey::from_slice(&[1u8; 32]).unwrap()), + })); + + assert_json_snapshot!(SenderData::SenderVerified(KnownSenderData { + user_id: owned_user_id!("@foo:bar.baz"), + device_id: None, + master_key: Box::new(Ed25519PublicKey::from_slice(&[1u8; 32]).unwrap()), + })); + } } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-2.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-2.snap new file mode 100644 index 00000000000..ffb7eccfa7f --- /dev/null +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-2.snap @@ -0,0 +1,9 @@ +--- +source: crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +expression: "SenderData::UnknownDevice { legacy_session: true, owner_check_failed: false, }" +--- +{ + "UnknownDevice": { + "legacy_session": true + } +} diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-3.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-3.snap new file mode 100644 index 00000000000..e1671d4e236 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-3.snap @@ -0,0 +1,21 @@ +--- +source: crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +expression: "SenderData::DeviceInfo\n{\n device_keys:\n DeviceKeys::new(owned_user_id!(\"@foo:bar.baz\"), owned_device_id!(\"DEV\"),\n vec!(EventEncryptionAlgorithm::MegolmV1AesSha2,\n EventEncryptionAlgorithm::OlmV1Curve25519AesSha2),\n BTreeMap::from_iter(vec![(DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519,\n device_id!(\"ABCDEFGH\")),\n DeviceKey::Curve25519(Curve25519PublicKey::from_bytes([0u8; 32])),)]),\n Default::default(),), legacy_session: false,\n}" +--- +{ + "DeviceInfo": { + "device_keys": { + "user_id": "@foo:bar.baz", + "device_id": "DEV", + "algorithms": [ + "m.megolm.v1.aes-sha2", + "m.olm.v1.curve25519-aes-sha2" + ], + "keys": { + "ed25519:ABCDEFGH": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "signatures": {} + }, + "legacy_session": false + } +} diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap new file mode 100644 index 00000000000..da1c8e4c40d --- /dev/null +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap @@ -0,0 +1,44 @@ +--- +source: crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +expression: "SenderData::VerificationViolation(KnownSenderData\n{\n user_id: owned_user_id!(\"@foo:bar.baz\"), device_id:\n Some(owned_device_id!(\"DEV\")), master_key:\n Box::new(Ed25519PublicKey::from_slice(&[0u8; 32]).unwrap()),\n})" +--- +{ + "VerificationViolation": { + "user_id": "@foo:bar.baz", + "device_id": "DEV", + "master_key": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } +} diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap new file mode 100644 index 00000000000..36f6beb3dcf --- /dev/null +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap @@ -0,0 +1,44 @@ +--- +source: crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +expression: "SenderData::SenderUnverified(KnownSenderData\n{\n user_id: owned_user_id!(\"@foo:bar.baz\"), device_id: None, master_key:\n Box::new(Ed25519PublicKey::from_slice(&[1u8; 32]).unwrap()),\n})" +--- +{ + "SenderUnverified": { + "user_id": "@foo:bar.baz", + "device_id": null, + "master_key": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } +} diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap new file mode 100644 index 00000000000..62e7b8e7ceb --- /dev/null +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap @@ -0,0 +1,44 @@ +--- +source: crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +expression: "SenderData::SenderVerified(KnownSenderData\n{\n user_id: owned_user_id!(\"@foo:bar.baz\"), device_id: None, master_key:\n Box::new(Ed25519PublicKey::from_slice(&[1u8; 32]).unwrap()),\n})" +--- +{ + "SenderVerified": { + "user_id": "@foo:bar.baz", + "device_id": null, + "master_key": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } +} diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data.snap new file mode 100644 index 00000000000..44cf973f6ad --- /dev/null +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data.snap @@ -0,0 +1,10 @@ +--- +source: crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +expression: "SenderData::UnknownDevice { legacy_session: false, owner_check_failed: true, }" +--- +{ + "UnknownDevice": { + "legacy_session": false, + "owner_check_failed": true + } +} diff --git a/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_decryption_settings.snap b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_decryption_settings.snap new file mode 100644 index 00000000000..6beabaeac11 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_decryption_settings.snap @@ -0,0 +1,7 @@ +--- +source: crates/matrix-sdk-crypto/src/lib.rs +expression: "DecryptionSettings\n{ sender_device_trust_requirement: TrustRequirement::Untrusted, }" +--- +{ + "sender_device_trust_requirement": "Untrusted" +} diff --git a/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-2.snap b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-2.snap new file mode 100644 index 00000000000..bf435f10d7d --- /dev/null +++ b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-crypto/src/lib.rs +expression: "TrustRequirement::CrossSignedOrLegacy" +--- +"CrossSignedOrLegacy" diff --git a/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-3.snap b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-3.snap new file mode 100644 index 00000000000..ed4cf22019c --- /dev/null +++ b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement-3.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-crypto/src/lib.rs +expression: "TrustRequirement::CrossSigned" +--- +"CrossSigned" diff --git a/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement.snap b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement.snap new file mode 100644 index 00000000000..1cf676489d3 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/snapshots/matrix_sdk_crypto__test__snapshot_trust_requirement.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-crypto/src/lib.rs +expression: "TrustRequirement::Untrusted" +--- +"Untrusted" diff --git a/crates/matrix-sdk-crypto/src/types/backup.rs b/crates/matrix-sdk-crypto/src/types/backup.rs index 45443c2b7ef..313a5d6cceb 100644 --- a/crates/matrix-sdk-crypto/src/types/backup.rs +++ b/crates/matrix-sdk-crypto/src/types/backup.rs @@ -108,10 +108,16 @@ impl Serialize for RoomKeyBackupInfo { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use assert_matches::assert_matches; - use serde_json::json; + use insta::{assert_json_snapshot, with_settings}; + use ruma::{user_id, DeviceKeyAlgorithm, KeyId}; + use serde_json::{json, Value}; + use vodozemac::{Curve25519PublicKey, Ed25519Signature}; use super::RoomKeyBackupInfo; + use crate::types::{MegolmV1AuthData, Signature, Signatures}; #[test] fn serialization() { @@ -146,4 +152,32 @@ mod tests { let serialized = serde_json::to_value(deserialized).unwrap(); assert_eq!(json, serialized); } + + #[test] + fn snapshot_room_key_backup_info() { + let info = RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(MegolmV1AuthData { + public_key: Curve25519PublicKey::from_bytes([2u8; 32]), + signatures: Signatures(BTreeMap::from([( + user_id!("@alice:localhost").to_owned(), + BTreeMap::from([( + KeyId::from_parts(DeviceKeyAlgorithm::Ed25519, "ABCDEFG".into()), + Ok(Signature::from(Ed25519Signature::from_slice(&[0u8; 64]).unwrap())), + )]), + )])), + extra: BTreeMap::from([("foo".to_owned(), Value::from("bar"))]), + }); + + with_settings!({sort_maps =>true}, { + assert_json_snapshot!(info) + }); + + let info = RoomKeyBackupInfo::Other { + algorithm: "caesar.cipher".to_owned(), + auth_data: BTreeMap::from([("foo".to_owned(), Value::from("bar"))]), + }; + + with_settings!({sort_maps =>true}, { + assert_json_snapshot!(info); + }) + } } diff --git a/crates/matrix-sdk-crypto/src/types/mod.rs b/crates/matrix-sdk-crypto/src/types/mod.rs index 8bb3f324583..a10c7d70535 100644 --- a/crates/matrix-sdk-crypto/src/types/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/mod.rs @@ -519,6 +519,8 @@ where #[cfg(test)] mod test { + use insta::{assert_debug_snapshot, assert_json_snapshot, with_settings}; + use ruma::{device_id, user_id}; use serde_json::json; use similar_asserts::assert_eq; @@ -547,4 +549,79 @@ mod test { assert_eq!(json, serialized, "A serialization cycle should yield the same result"); } + + #[test] + fn snapshot_backup_decryption_key() { + let decryption_key = BackupDecryptionKey { inner: Box::new([1u8; 32]) }; + assert_json_snapshot!(decryption_key); + + // should not log the key ! + assert_debug_snapshot!(decryption_key); + } + + #[test] + fn snapshot_signatures() { + let signatures = Signatures(BTreeMap::from([ + ( + user_id!("@alice:localhost").to_owned(), + BTreeMap::from([ + ( + DeviceKeyId::from_parts( + DeviceKeyAlgorithm::Ed25519, + device_id!("ABCDEFGH"), + ), + Ok(Signature::from(Ed25519Signature::from_slice(&[0u8; 64]).unwrap())), + ), + ( + DeviceKeyId::from_parts( + DeviceKeyAlgorithm::Curve25519, + device_id!("IJKLMNOP"), + ), + Ok(Signature::from(Ed25519Signature::from_slice(&[1u8; 64]).unwrap())), + ), + ]), + ), + ( + user_id!("@bob:localhost").to_owned(), + BTreeMap::from([( + DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, device_id!("ABCDEFGH")), + Err(InvalidSignature { source: "SOME+B64+SOME+B64+SOME+B64+==".to_owned() }), + )]), + ), + ])); + + with_settings!({sort_maps =>true}, { + assert_json_snapshot!(signatures) + }); + } + + #[test] + fn snapshot_secret_bundle() { + let secret_bundle = SecretsBundle { + cross_signing: CrossSigningSecrets { + master_key: "MSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSK".to_owned(), + user_signing_key: "USKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSK".to_owned(), + self_signing_key: "SSKSSKSSKSSKSSKSSKSSKSSKSSKSSKSSK".to_owned(), + }, + backup: Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2( + MegolmBackupV1Curve25519AesSha2Secrets { + key: BackupDecryptionKey::from_bytes(&[0u8; 32]), + backup_version: "v1.1".to_owned(), + }, + )), + }; + + assert_json_snapshot!(secret_bundle); + + let secret_bundle = SecretsBundle { + cross_signing: CrossSigningSecrets { + master_key: "MSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSK".to_owned(), + user_signing_key: "USKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSK".to_owned(), + self_signing_key: "SSKSSKSSKSSKSSKSSKSSKSSKSSKSSKSSK".to_owned(), + }, + backup: None, + }; + + assert_json_snapshot!(secret_bundle); + } } diff --git a/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info-2.snap b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info-2.snap new file mode 100644 index 00000000000..e83f1a24e0a --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/matrix-sdk-crypto/src/types/backup.rs +expression: info +--- +{ + "algorithm": "caesar.cipher", + "auth_data": { + "foo": "bar" + } +} diff --git a/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info.snap b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info.snap new file mode 100644 index 00000000000..f8e7084d030 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__backup__tests__snapshot_room_key_backup_info.snap @@ -0,0 +1,16 @@ +--- +source: crates/matrix-sdk-crypto/src/types/backup.rs +expression: info +--- +{ + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "auth_data": { + "foo": "bar", + "public_key": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI", + "signatures": { + "@alice:localhost": { + "ed25519:ABCDEFG": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + } + } +} diff --git a/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key-2.snap b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key-2.snap new file mode 100644 index 00000000000..f144f74d23f --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/matrix-sdk-crypto/src/types/mod.rs +expression: decryption_key +--- +BackupDecryptionKey( + "...", +) diff --git a/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key.snap b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key.snap new file mode 100644 index 00000000000..3e84e7de830 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_backup_decryption_key.snap @@ -0,0 +1,38 @@ +--- +source: crates/matrix-sdk-crypto/src/types/mod.rs +expression: "BackupDecryptionKey { inner: Box::new([1u8;32]) }" +--- +[ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 +] diff --git a/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle-2.snap b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle-2.snap new file mode 100644 index 00000000000..b4d49cdbd52 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle-2.snap @@ -0,0 +1,12 @@ +--- +source: crates/matrix-sdk-crypto/src/types/mod.rs +expression: secret_bundle +--- +{ + "cross_signing": { + "master_key": "MSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSK", + "user_signing_key": "USKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSK", + "self_signing_key": "SSKSSKSSKSSKSSKSSKSSKSSKSSKSSKSSK" + }, + "backup": null +} diff --git a/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle.snap b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle.snap new file mode 100644 index 00000000000..20a6b23772e --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_secret_bundle.snap @@ -0,0 +1,16 @@ +--- +source: crates/matrix-sdk-crypto/src/types/mod.rs +expression: secret_bundle +--- +{ + "cross_signing": { + "master_key": "MSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSKMSK", + "user_signing_key": "USKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSKUSK", + "self_signing_key": "SSKSSKSSKSSKSSKSSKSSKSSKSSKSSKSSK" + }, + "backup": { + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "backup_version": "v1.1" + } +} diff --git a/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_signatures.snap b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_signatures.snap new file mode 100644 index 00000000000..c0fc0e651e0 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/snapshots/matrix_sdk_crypto__types__test__snapshot_signatures.snap @@ -0,0 +1,13 @@ +--- +source: crates/matrix-sdk-crypto/src/types/mod.rs +expression: signatures +--- +{ + "@alice:localhost": { + "curve25519:IJKLMNOP": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ", + "ed25519:ABCDEFGH": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "@bob:localhost": { + "ed25519:ABCDEFGH": "SOME+B64+SOME+B64+SOME+B64+==" + } +} From c50358366fe7f25606cb5494071391c39f74f43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Sun, 22 Dec 2024 18:45:04 +0100 Subject: [PATCH 860/979] refactor!(sdk): Set thumbnail in `AttachmentConfig` with builder method instead of constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AttachmentConfig::with_thumbnail()` is replaced by `AttachmentConfig::new().thumbnail()`. Simplifies the use of `AttachmentConfig`, by avoiding code like: ```rust let config = if let Some(thumbnail) = thumbnail { AttachmentConfig::with_thumbnail(thumbnail) } else { AttachmentConfig::new() }; ``` --------- Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 25 ++++++++++------- crates/matrix-sdk/CHANGELOG.md | 3 +++ crates/matrix-sdk/src/attachment.rs | 14 +++++----- .../tests/integration/room/attachment/mod.rs | 27 ++++++++++--------- .../tests/integration/send_queue.rs | 10 ++++--- 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 0d0894b6443..30660a00a7c 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -137,9 +137,9 @@ impl Timeline { fn build_thumbnail_info( thumbnail_url: Option, thumbnail_info: Option, -) -> Result { +) -> Result, RoomError> { match (thumbnail_url, thumbnail_info) { - (None, None) => Ok(AttachmentConfig::new()), + (None, None) => Ok(None), (Some(thumbnail_url), Some(thumbnail_info)) => { let thumbnail_data = @@ -163,15 +163,18 @@ fn build_thumbnail_info( let mime_type = mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let thumbnail = - Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size }; - - Ok(AttachmentConfig::with_thumbnail(thumbnail)) + Ok(Some(Thumbnail { + data: thumbnail_data, + content_type: mime_type, + height, + width, + size, + })) } _ => { warn!("Ignoring thumbnail because either the thumbnail URL or info isn't defined"); - Ok(AttachmentConfig::new()) + Ok(None) } } } @@ -304,8 +307,10 @@ impl Timeline { let base_image_info = BaseImageInfo::try_from(&image_info) .map_err(|_| RoomError::InvalidAttachmentData)?; let attachment_info = AttachmentInfo::Image(base_image_info); + let thumbnail = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?; - let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)? + let attachment_config = AttachmentConfig::new() + .thumbnail(thumbnail) .info(attachment_info) .caption(caption) .formatted_caption(formatted_caption); @@ -338,8 +343,10 @@ impl Timeline { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) .map_err(|_| RoomError::InvalidAttachmentData)?; let attachment_info = AttachmentInfo::Video(base_video_info); + let thumbnail = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?; - let attachment_config = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)? + let attachment_config = AttachmentConfig::new() + .thumbnail(thumbnail) .info(attachment_info) .caption(caption) .formatted_caption(formatted_caption.map(Into::into)); diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 544ffd81973..52a99b3bc74 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -12,6 +12,9 @@ All notable changes to this project will be documented in this file. `Client::send()` method to the `with_request_config()` builder method. You should call `Client::send(request).with_request_config(request_config).await` now instead. +- [**breaking**] Remove the `AttachmentConfig::with_thumbnail()` constructor and + replace it with the `AttachmentConfig::thumbnail()` builder method. You should + call `AttachmentConfig::new().thumbnail(thumbnail)` now instead. ## [0.9.0] - 2024-12-18 diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index ee35cce5893..f5defb05ba4 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -188,21 +188,21 @@ pub struct AttachmentConfig { } impl AttachmentConfig { - /// Create a new default `AttachmentConfig` without providing a thumbnail. - /// - /// To provide a thumbnail use [`AttachmentConfig::with_thumbnail()`]. + /// Create a new empty `AttachmentConfig`. pub fn new() -> Self { Self::default() } - /// Create a new default `AttachmentConfig` with a `thumbnail`. + /// Set the thumbnail to send. /// /// # Arguments /// /// * `thumbnail` - The thumbnail of the media. If the `content_type` does - /// not support it (eg audio clips), it is ignored. - pub fn with_thumbnail(thumbnail: Thumbnail) -> Self { - Self { thumbnail: Some(thumbnail), ..Default::default() } + /// not support it (e.g. audio clips), it is ignored. + #[must_use] + pub fn thumbnail(mut self, thumbnail: Option) -> Self { + self.thumbnail = thumbnail; + self } /// Set the transaction ID to send. diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 8301ef60b77..8fc38451d6a 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -204,19 +204,20 @@ async fn test_room_attachment_send_info_thumbnail() { let _ = client.media().get_media_content(&thumbnail_request, true).await.unwrap_err(); // Send the attachment with a thumbnail. - let config = AttachmentConfig::with_thumbnail(Thumbnail { - data: b"Thumbnail".to_vec(), - content_type: mime::IMAGE_JPEG, - height: uint!(360), - width: uint!(480), - size: uint!(3600), - }) - .info(AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - size: None, - blurhash: None, - })); + let config = AttachmentConfig::new() + .thumbnail(Some(Thumbnail { + data: b"Thumbnail".to_vec(), + content_type: mime::IMAGE_JPEG, + height: uint!(360), + width: uint!(480), + size: uint!(3600), + })) + .info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + })); let response = room .send_attachment("image", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config) diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index f1e08e2e150..407e595e54e 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -79,13 +79,14 @@ async fn queue_attachment_with_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'st size: uint!(42), }; - let config = - AttachmentConfig::with_thumbnail(thumbnail).info(AttachmentInfo::Image(BaseImageInfo { + let config = AttachmentConfig::new().thumbnail(Some(thumbnail)).info(AttachmentInfo::Image( + BaseImageInfo { height: Some(uint!(13)), width: Some(uint!(37)), size: Some(uint!(42)), blurhash: None, - })); + }, + )); let handle = q .send_attachment(filename, content_type, data, config) @@ -1801,7 +1802,8 @@ async fn test_media_uploads() { let transaction_id = TransactionId::new(); let mentions = Mentions::with_user_ids([owned_user_id!("@ivan:sdk.rs")]); - let config = AttachmentConfig::with_thumbnail(thumbnail) + let config = AttachmentConfig::new() + .thumbnail(Some(thumbnail)) .txn_id(&transaction_id) .caption(Some("caption".to_owned())) .mentions(Some(mentions.clone())) From 1480fada6e185efcb888c90f14d92c84f6d7853c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 6 Jan 2025 12:24:01 +0100 Subject: [PATCH 861/979] refactor(event cache): make it clearer that vecdiff updates must be handled with `with_events_mut` Every caller to `with_events_mut` must propagate the vector diff updates, otherwise updates would be missing to the room event cache's observers. This slightly tweaks the signature to make this a bit clearer, and adjusts the code comment as well. --- .../matrix-sdk/src/event_cache/pagination.rs | 20 +++++++++---------- .../matrix-sdk/src/event_cache/room/events.rs | 1 - crates/matrix-sdk/src/event_cache/room/mod.rs | 14 ++++++++----- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 3c14f3dd34d..0d40c1046a0 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -167,7 +167,7 @@ impl RoomPagination { let new_gap = paginator.prev_batch_token().map(|prev_token| Gap { prev_token }); - let result = state + let (backpagination_outcome, updates_as_vector_diffs) = state .with_events_mut(move |room_events| { // Note: The chunk could be empty. // @@ -227,20 +227,18 @@ impl RoomPagination { debug!("not storing previous batch token, because we deduplicated all new back-paginated events"); } - let sync_timeline_events_diffs = room_events.updates_as_vector_diffs(); - - if !sync_timeline_events_diffs.is_empty() { - let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents { - diffs: sync_timeline_events_diffs, - origin: EventsOrigin::Sync, - }); - } - BackPaginationOutcome { events, reached_start } }) .await?; - Ok(Some(result)) + if !updates_as_vector_diffs.is_empty() { + let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents { + diffs: updates_as_vector_diffs, + origin: EventsOrigin::Sync, + }); + } + + Ok(Some(backpagination_outcome)) } /// Get the latest pagination token, as stored in the room events linked diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index bc7843ff9f0..eb352c99580 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -245,7 +245,6 @@ impl RoomEvents { /// See [`AsVector`] to learn more. /// /// [`Update`]: matrix_sdk_base::linked_chunk::Update - #[allow(unused)] // gonna be useful very soon! but we need it now for test purposes pub fn updates_as_vector_diffs(&mut self) -> Vec> { self.chunks_updates_as_vectordiffs.take() } diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 93c30e7513c..b0efde1395d 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -534,7 +534,7 @@ impl RoomEventCacheInner { // Add the previous back-pagination token (if present), followed by the timeline // events themselves. let sync_timeline_events_diffs = { - let sync_timeline_events_diffs = state + let (_, sync_timeline_events_diffs) = state .with_events_mut(|room_events| { if let Some(prev_token) = &prev_batch { room_events.push_gap(Gap { prev_token: prev_token.clone() }); @@ -556,8 +556,6 @@ impl RoomEventCacheInner { .replace_gap_at([], prev_gap_id) .expect("we obtained the valid position beforehand"); } - - room_events.updates_as_vector_diffs() }) .await?; @@ -641,6 +639,7 @@ fn chunk_debug_string(content: &ChunkContent) -> String mod private { use std::sync::Arc; + use eyeball_im::VectorDiff; use matrix_sdk_base::{ deserialized_responses::{SyncTimelineEvent, TimelineEventKind}, event_cache::{ @@ -846,13 +845,18 @@ mod private { /// Gives a temporary mutable handle to the underlying in-memory events, /// and will propagate changes to the storage once done. + /// + /// Returns the output of the given callback, as well as updates to the + /// linked chunk, as vector diff, so the caller may propagate + /// such updates, if needs be. pub async fn with_events_mut O>( &mut self, func: F, - ) -> Result { + ) -> Result<(O, Vec>), EventCacheError> { let output = func(&mut self.events); self.propagate_changes().await?; - Ok(output) + let updates_as_vector_diffs = self.events.updates_as_vector_diffs(); + Ok((output, updates_as_vector_diffs)) } } From 70fb7899e63727e2122a492ea1e6acacb799f6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:44:29 +0100 Subject: [PATCH 862/979] feat!(timeline): Allow to send attachments from bytes (#4451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes we can get the bytes directly, e.g. in Fractal we can get an image from the clipboard. It avoids to have to write the data to a temporary file only to have the data loaded back in memory by the SDK right after. The first commit to accept any type that implements `Into` for the filename is grouped here because it simplifies slightly the second commit. Note that we could also use `AttachmentSource` in the other `send_attachment` APIs, on `Room` and `RoomSendQueue`, for consistency. --------- Signed-off-by: Kévin Commaille --- crates/matrix-sdk-ui/CHANGELOG.md | 8 ++ crates/matrix-sdk-ui/src/timeline/futures.rs | 28 ++--- crates/matrix-sdk-ui/src/timeline/mod.rs | 57 +++++++++- .../tests/integration/timeline/media.rs | 103 +++++++++++++++++- crates/matrix-sdk/CHANGELOG.md | 2 + crates/matrix-sdk/src/room/futures.rs | 4 +- crates/matrix-sdk/src/room/mod.rs | 12 +- crates/matrix-sdk/src/send_queue/upload.rs | 3 +- 8 files changed, 189 insertions(+), 28 deletions(-) diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 1be4ef45ec8..9aa4649cbf7 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -13,6 +13,14 @@ All notable changes to this project will be documented in this file. non-left room filter did not take the new room stat into account. ([#4448](https://github.com/matrix-org/matrix-rust-sdk/pull/4448)) +### Features + +- [**breaking**] `Timeline::send_attachment()` now takes a type that implements + `Into` instead of a type that implements `Into`. + `AttachmentSource` allows to send an attachment either from a file, or with + the bytes and the filename of the attachment. Note that all types that + implement `Into` also implement `Into`. + ## [0.9.0] - 2024-12-18 ### Bug Fixes diff --git a/crates/matrix-sdk-ui/src/timeline/futures.rs b/crates/matrix-sdk-ui/src/timeline/futures.rs index 546b8ef5407..8109e0cb8c9 100644 --- a/crates/matrix-sdk-ui/src/timeline/futures.rs +++ b/crates/matrix-sdk-ui/src/timeline/futures.rs @@ -1,4 +1,4 @@ -use std::{fs, future::IntoFuture, path::PathBuf}; +use std::future::IntoFuture; use eyeball::{SharedObservable, Subscriber}; use matrix_sdk::{attachment::AttachmentConfig, TransmissionProgress}; @@ -6,11 +6,11 @@ use matrix_sdk_base::boxed_into_future; use mime::Mime; use tracing::{Instrument as _, Span}; -use super::{Error, Timeline}; +use super::{AttachmentSource, Error, Timeline}; pub struct SendAttachment<'a> { timeline: &'a Timeline, - path: PathBuf, + source: AttachmentSource, mime_type: Mime, config: AttachmentConfig, tracing_span: Span, @@ -21,13 +21,13 @@ pub struct SendAttachment<'a> { impl<'a> SendAttachment<'a> { pub(crate) fn new( timeline: &'a Timeline, - path: PathBuf, + source: AttachmentSource, mime_type: Mime, config: AttachmentConfig, ) -> Self { Self { timeline, - path, + source, mime_type, config, tracing_span: Span::current(), @@ -61,16 +61,18 @@ impl<'a> IntoFuture for SendAttachment<'a> { boxed_into_future!(extra_bounds: 'a); fn into_future(self) -> Self::IntoFuture { - let Self { timeline, path, mime_type, config, tracing_span, use_send_queue, send_progress } = - self; + let Self { + timeline, + source, + mime_type, + config, + tracing_span, + use_send_queue, + send_progress, + } = self; let fut = async move { - let filename = path - .file_name() - .ok_or(Error::InvalidAttachmentFileName)? - .to_str() - .ok_or(Error::InvalidAttachmentFileName)?; - let data = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?; + let (data, filename) = source.try_into_bytes_and_filename()?; if use_send_queue { let send_queue = timeline.room().send_queue(); diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 927180093cc..e4ce251b5fc 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -16,7 +16,7 @@ //! //! See [`Timeline`] for details. -use std::{path::PathBuf, pin::Pin, sync::Arc, task::Poll}; +use std::{fs, path::PathBuf, pin::Pin, sync::Arc, task::Poll}; use event_item::{extract_room_msg_edit_content, TimelineItemHandle}; use eyeball_im::VectorDiff; @@ -540,7 +540,7 @@ impl Timeline { /// /// # Arguments /// - /// * `path` - The path of the file to be sent. + /// * `source` - The source of the attachment to send. /// /// * `mime_type` - The attachment's mime type. /// @@ -551,11 +551,11 @@ impl Timeline { #[instrument(skip_all)] pub fn send_attachment( &self, - path: impl Into, + source: impl Into, mime_type: Mime, config: AttachmentConfig, ) -> SendAttachment<'_> { - SendAttachment::new(self, path.into(), mime_type, config) + SendAttachment::new(self, source.into(), mime_type, config) } /// Redact an event given its [`TimelineEventItemId`] and an optional @@ -885,3 +885,52 @@ impl Stream for TimelineStream { pub type TimelineEventFilterFn = dyn Fn(&AnySyncTimelineEvent, &RoomVersionId) -> bool + Send + Sync; + +/// A source for sending an attachment. +/// +/// The [`AttachmentSource::File`] variant can be constructed from any type that +/// implements `Into`. +#[derive(Debug, Clone)] +pub enum AttachmentSource { + /// The data of the attachment. + Data { + /// The bytes of the attachment. + bytes: Vec, + + /// The filename of the attachment. + filename: String, + }, + + /// An attachment loaded from a file. + /// + /// The bytes and the filename will be read from the file at the given path. + File(PathBuf), +} + +impl AttachmentSource { + /// Try to convert this attachment source into a `(bytes, filename)` tuple. + pub(crate) fn try_into_bytes_and_filename(self) -> Result<(Vec, String), Error> { + match self { + Self::Data { bytes, filename } => Ok((bytes, filename)), + Self::File(path) => { + let filename = path + .file_name() + .ok_or(Error::InvalidAttachmentFileName)? + .to_str() + .ok_or(Error::InvalidAttachmentFileName)? + .to_owned(); + let bytes = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?; + Ok((bytes, filename)) + } + } + } +} + +impl

From

{ txn_id, send_handle, content, - &self.settings, ) .await; } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4eb4f019f3e..dffda89f6ce 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -223,7 +223,6 @@ impl TimelineState { txn_id: OwnedTransactionId, send_handle: Option, content: TimelineEventKind, - settings: &TimelineSettings, ) { let ctx = TimelineEventContext { sender: own_user_id, @@ -241,7 +240,7 @@ impl TimelineState { let mut date_divider_adjuster = DateDividerAdjuster::new(date_divider_mode); - TimelineEventHandler::new(&mut txn, ctx, settings) + TimelineEventHandler::new(&mut txn, ctx) .handle_event(&mut date_divider_adjuster, content) .await; @@ -745,9 +744,7 @@ impl TimelineStateTransaction<'_> { }; // Handle the event to create or update a timeline item. - TimelineEventHandler::new(self, ctx, settings) - .handle_event(date_divider_adjuster, event_kind) - .await + TimelineEventHandler::new(self, ctx).handle_event(date_divider_adjuster, event_kind).await } /// Remove one timeline item by its `event_index`. @@ -846,39 +843,12 @@ impl TimelineStateTransaction<'_> { room_data_provider: &P, settings: &TimelineSettings, ) { - /// Remove duplicated events. - /// - /// If `VectorDiff`s are the inputs of the `Timeline`, this is not - /// necessary, as they are generated by the `EventCache`, which supports - /// its own deduplication algorithm. - fn deduplicate( - new_event_id: &EventId, - items: &mut ObservableItemsTransaction<'_>, - settings: &TimelineSettings, - ) { - if settings.vectordiffs_as_inputs { - return; - } - - if let Some(pos) = items - .all_remote_events() - .iter() - .position(|EventMeta { event_id, .. }| event_id == new_event_id) - { - items.remove_remote_event(pos); - } - } - match position { TimelineItemPosition::Start { .. } => { - deduplicate(event_meta.event_id, &mut self.items, settings); - self.items.push_front_remote_event(event_meta.base_meta()) } TimelineItemPosition::End { .. } => { - deduplicate(event_meta.event_id, &mut self.items, settings); - self.items.push_back_remote_event(event_meta.base_meta()); } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 482a8d3073e..a1262a78a2c 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -53,7 +53,7 @@ use tracing::{debug, error, field::debug, info, instrument, trace, warn}; use super::{ controller::{ ObservableItemsTransaction, ObservableItemsTransactionEntry, PendingEdit, PendingEditKind, - TimelineMetadata, TimelineSettings, TimelineStateTransaction, + TimelineMetadata, TimelineStateTransaction, }, date_dividers::DateDividerAdjuster, event_item::{ @@ -338,17 +338,15 @@ pub(super) struct TimelineEventHandler<'a, 'o> { meta: &'a mut TimelineMetadata, ctx: TimelineEventContext, result: HandleEventResult, - settings: &'a TimelineSettings, } impl<'a, 'o> TimelineEventHandler<'a, 'o> { pub(super) fn new( state: &'a mut TimelineStateTransaction<'o>, ctx: TimelineEventContext, - settings: &'a TimelineSettings, ) -> Self { let TimelineStateTransaction { items, meta, .. } = state; - Self { items, meta, ctx, result: HandleEventResult::default(), settings } + Self { items, meta, ctx, result: HandleEventResult::default() } } /// Handle an event. @@ -1115,8 +1113,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &mut item, Some(event_id), txn_id.as_ref().map(AsRef::as_ref), - self.meta, - self.settings, ); let item = new_timeline_item(self.meta, item, removed_duplicated_timeline_item); @@ -1136,8 +1132,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &mut item, Some(event_id), txn_id.as_ref().map(AsRef::as_ref), - self.meta, - self.settings, ); let item = new_timeline_item(self.meta, item, removed_duplicated_timeline_item); @@ -1191,8 +1185,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { &mut item, Some(event_id), txn_id.as_ref().map(AsRef::as_ref), - self.meta, - self.settings, ); let item = new_timeline_item(self.meta, item, removed_duplicated_timeline_item); @@ -1268,12 +1260,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { /// Remove the local timeline item matching the `event_id` or the /// `transaction_id` of `new_event_timeline_item` if it exists. - /// - /// Let's also try to deduplicate remote events. If `VectorDiff`s are the - /// inputs of the `Timeline`, this is not necessary, as they are - /// generated by the `EventCache`, which supports its own deduplication - /// algorithm. - // // Note: this method doesn't take `&mut self` to avoid a borrow checker // conflict with `TimelineEventHandler::add_item`. fn deduplicate_local_timeline_item( @@ -1281,8 +1267,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { new_event_timeline_item: &mut EventTimelineItem, event_id: Option<&EventId>, transaction_id: Option<&TransactionId>, - metadata: &TimelineMetadata, - settings: &TimelineSettings, ) -> Option> { // Start with the canonical case: detect a local timeline item that matches // `event_id` or `transaction_id`. @@ -1310,39 +1294,6 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return Some(items.remove(local_timeline_item_index)); }; - if !settings.vectordiffs_as_inputs { - if let Some((remote_timeline_item_index, remote_timeline_item)) = - rfind_event_item(items, |event_timeline_item| { - if event_timeline_item.is_remote_event() { - event_id == event_timeline_item.event_id() - } else { - false - } - }) - { - trace!( - ?event_id, - ?transaction_id, - ?remote_timeline_item_index, - "Removing remote timeline item (it is a duplicate)" - ); - - if remote_timeline_item.content.is_redacted() - && !new_event_timeline_item.content.is_redacted() - { - warn!("Got original form of an event that was previously redacted"); - new_event_timeline_item.content = - new_event_timeline_item.content.redact(&metadata.room_version); - new_event_timeline_item.reactions.clear(); - } - - transfer_details(new_event_timeline_item, &remote_timeline_item); - - // Remove the remote timeline item. - return Some(items.remove(remote_timeline_item_index)); - } - } - None } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 992ba6e6808..b8e30babab3 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -318,6 +318,7 @@ impl TimelineItemContent { as_variant!(self, Self::UnableToDecrypt) } + #[cfg(test)] pub(crate) fn is_redacted(&self) -> bool { matches!(self, Self::RedactedMessage) } diff --git a/crates/matrix-sdk-ui/src/timeline/pagination.rs b/crates/matrix-sdk-ui/src/timeline/pagination.rs index 175ffe46a83..e7322b215e9 100644 --- a/crates/matrix-sdk-ui/src/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/src/timeline/pagination.rs @@ -26,7 +26,6 @@ use matrix_sdk::event_cache::{ use tracing::{instrument, trace, warn}; use super::Error; -use crate::timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin}; impl super::Timeline { /// Add more events to the start of the timeline. @@ -77,17 +76,6 @@ impl super::Timeline { let num_events = events.len(); trace!("Back-pagination succeeded with {num_events} events"); - // If `TimelineSettings::vectordiffs_as_inputs` is enabled, - // we don't need to add events manually: everything we need - // is to let the `EventCache` receive the events from this - // pagination, and emit its updates as `VectorDiff`s, which - // will be handled by the `Timeline` naturally. - if !self.controller.settings.vectordiffs_as_inputs { - self.controller - .add_events_at(events.into_iter(), TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination }) - .await; - } - if num_events == 0 && !reached_start { // As an exceptional contract: if there were no events in the response, // and we've not hit the start of the timeline, retry until we get From e19bdbfd59141de101f7d867e6fd398cf4a47667 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 7 Jan 2025 14:51:07 +0100 Subject: [PATCH 889/979] test(ui): Adjust tests according to the new `Timeline` behaviour. --- .../src/timeline/event_item/content/mod.rs | 5 - .../matrix-sdk-ui/src/timeline/tests/basic.rs | 73 +--- .../matrix-sdk-ui/src/timeline/tests/echo.rs | 38 +-- .../src/timeline/tests/redaction.rs | 58 +--- .../tests/integration/timeline/pagination.rs | 312 ++++++++++-------- .../integration/timeline/sliding_sync.rs | 3 +- .../tests/integration/timeline/subscribe.rs | 2 + .../src/tests/sliding_sync/room.rs | 36 +- .../src/tests/timeline.rs | 10 + 9 files changed, 210 insertions(+), 327 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index b8e30babab3..9cd83213239 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -318,11 +318,6 @@ impl TimelineItemContent { as_variant!(self, Self::UnableToDecrypt) } - #[cfg(test)] - pub(crate) fn is_redacted(&self) -> bool { - matches!(self, Self::RedactedMessage) - } - // These constructors could also be `From` implementations, but that would // allow users to call them directly, which should not be supported pub(crate) fn message( diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index ba7abb9f7a3..bb0adbeba00 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -23,7 +23,7 @@ use ruma::{ receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ member::{MembershipState, RedactedRoomMemberEventContent, RoomMemberEventContent}, - message::{MessageType, RoomMessageEventContent}, + message::MessageType, name::RoomNameEventContent, topic::RedactedRoomTopicEventContent, }, @@ -280,77 +280,6 @@ async fn test_other_state() { assert_matches!(full_content, FullStateEventContent::Redacted(_)); } -#[async_test] -async fn test_dedup_pagination() { - let timeline = TestTimeline::new(); - - let event = timeline - .event_builder - .make_sync_message_event(*ALICE, RoomMessageEventContent::text_plain("o/")); - timeline.handle_live_event(SyncTimelineEvent::new(event.clone())).await; - // This cast is not actually correct, sync events aren't valid - // back-paginated events, as they are missing `room_id`. However, the - // timeline doesn't care about that `room_id` and casts back to - // `Raw` before attempting to deserialize. - timeline.handle_back_paginated_event(event.cast()).await; - - let timeline_items = timeline.controller.items().await; - assert_eq!(timeline_items.len(), 2); - assert_matches!( - timeline_items[0].kind, - TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)) - ); - assert_matches!(timeline_items[1].kind, TimelineItemKind::Event(_)); -} - -#[async_test] -async fn test_dedup_initial() { - let timeline = TestTimeline::new(); - - let f = &timeline.factory; - let event_a = f.text_msg("A").sender(*ALICE).into_sync(); - let event_b = f.text_msg("B").sender(*BOB).into_sync(); - let event_c = f.text_msg("C").sender(*CAROL).into_sync(); - - timeline - .controller - .add_events_at( - [ - // two events - event_a.clone(), - event_b.clone(), - // same events got duplicated in next sync response - event_a, - event_b, - // … and a new event also came in - event_c, - ] - .into_iter(), - TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, - ) - .await; - - let timeline_items = timeline.controller.items().await; - assert_eq!(timeline_items.len(), 4); - - assert!(timeline_items[0].is_date_divider()); - - let event1 = &timeline_items[1]; - let event2 = &timeline_items[2]; - let event3 = &timeline_items[3]; - - // Make sure the order is right. - assert_eq!(event1.as_event().unwrap().sender(), *ALICE); - assert_eq!(event2.as_event().unwrap().sender(), *BOB); - assert_eq!(event3.as_event().unwrap().sender(), *CAROL); - - // Make sure we reused IDs when deduplicating events. - assert_eq!(event1.unique_id().0, "0"); - assert_eq!(event2.unique_id().0, "1"); - assert_eq!(event3.unique_id().0, "2"); - assert_eq!(timeline_items[0].unique_id().0, "3"); -} - #[async_test] async fn test_internal_id_prefix() { let timeline = TestTimeline::with_internal_id_prefix("le_prefix_".to_owned()); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index 683f643ec85..f5a85de4495 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -18,7 +18,7 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; use matrix_sdk::{assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate}; use matrix_sdk_base::store::QueueWedgeError; -use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB}; +use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::{ event_id, events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, @@ -187,42 +187,6 @@ async fn test_remote_echo_new_position() { assert_pending!(stream); } -#[async_test] -async fn test_date_divider_duplication() { - let timeline = TestTimeline::new(); - - // Given two remote events from one day, and a local event from another day… - let f = EventFactory::new().sender(&BOB); - timeline.handle_live_event(f.text_msg("A")).await; - timeline.handle_live_event(f.text_msg("B")).await; - timeline - .handle_local_event(AnyMessageLikeEventContent::RoomMessage( - RoomMessageEventContent::text_plain("C"), - )) - .await; - - let items = timeline.controller.items().await; - assert_eq!(items.len(), 5); - assert!(items[0].is_date_divider()); - assert!(items[1].is_remote_event()); - assert!(items[2].is_remote_event()); - assert!(items[3].is_date_divider()); - assert!(items[4].is_local_echo()); - - // … when the second remote event is re-received (day still the same) - let event_id = items[2].as_event().unwrap().event_id().unwrap(); - timeline.handle_live_event(f.text_msg("B").event_id(event_id).server_ts(1)).await; - - // … it should not impact the date dividers. - let items = timeline.controller.items().await; - assert_eq!(items.len(), 5); - assert!(items[0].is_date_divider()); - assert!(items[1].is_remote_event()); - assert!(items[2].is_remote_event()); - assert!(items[3].is_date_divider()); - assert!(items[4].is_local_echo()); -} - #[async_test] async fn test_date_divider_removed_after_local_echo_disappeared() { let timeline = TestTimeline::new(); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs index 11fa3023721..196e61ed058 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/redaction.rs @@ -19,10 +19,7 @@ use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::events::{ reaction::RedactedReactionEventContent, - room::{ - message::{OriginalSyncRoomMessageEvent, RedactedRoomMessageEventContent}, - name::RoomNameEventContent, - }, + room::{message::OriginalSyncRoomMessageEvent, name::RoomNameEventContent}, FullStateEventContent, }; use stream_assert::assert_next_matches; @@ -177,56 +174,3 @@ async fn test_reaction_redaction_timeline_filter() { assert_eq!(item.reactions().len(), 0); assert_eq!(timeline.controller.items().await.len(), 2); } - -#[async_test] -async fn test_receive_unredacted() { - let timeline = TestTimeline::new(); - - let f = &timeline.factory; - - // send two events, second one redacted - timeline.handle_live_event(f.text_msg("about to be redacted").sender(&ALICE)).await; - timeline - .handle_live_redacted_message_event(&ALICE, RedactedRoomMessageEventContent::new()) - .await; - - // redact the first one as well - let items = timeline.controller.items().await; - assert!(items[0].is_date_divider()); - let fst = items[1].as_event().unwrap(); - timeline.handle_live_event(f.redaction(fst.event_id().unwrap()).sender(&ALICE)).await; - - let items = timeline.controller.items().await; - assert_eq!(items.len(), 3); - let fst = items[1].as_event().unwrap(); - let snd = items[2].as_event().unwrap(); - - // make sure we have two redacted events - assert!(fst.content.is_redacted()); - assert!(snd.content.is_redacted()); - - // send new events with the same event ID as the previous ones - timeline - .handle_live_event( - f.text_msg("unredacted #1") - .sender(*ALICE) - .event_id(fst.event_id().unwrap()) - .server_ts(fst.timestamp()), - ) - .await; - - timeline - .handle_live_event( - f.text_msg("unredacted #2") - .sender(*ALICE) - .event_id(snd.event_id().unwrap()) - .server_ts(snd.timestamp()), - ) - .await; - - // make sure we still have two redacted events - let items = timeline.controller.items().await; - assert_eq!(items.len(), 3); - assert!(items[1].as_event().unwrap().content.is_redacted()); - assert!(items[2].as_event().unwrap().content.is_redacted()); -} diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs index 3a2c0aeaa54..a796fc740da 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs @@ -37,7 +37,7 @@ use ruma::{ room_id, }; use serde_json::{json, Value as JsonValue}; -use stream_assert::{assert_next_eq, assert_next_matches}; +use stream_assert::{assert_next_eq, assert_pending}; use tokio::{ spawn, time::{sleep, timeout}, @@ -87,42 +87,52 @@ async fn test_back_pagination() { }; join(paginate, observe_paginating).await; - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "hello world"); - - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "the world is big"); - - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); - assert_eq!(state.state_key(), ""); - assert_let!( - AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { - content, - prev_content - }) = state.content() - ); - assert_eq!(content.name, "New room name"); - assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); + // `m.room.name` + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); + assert_eq!(state.state_key(), ""); + assert_let!( + AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { + content, + prev_content + }) = state.content() + ); + assert_eq!(content.name, "New room name"); + assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); + } + + // `m.room.name` receives an update + { + assert_let!(Some(VectorDiff::Set { index, .. }) = timeline_stream.next().await); + assert_eq!(index, 0); + } + + // `m.room.message`: “the world is big” + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "the world is big"); + } + + // `m.room.message`: “hello world” + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "hello world"); + } + + // Date divider is updated. + { + assert_let!( + Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await + ); + assert!(date_divider.is_date_divider()); + } - let date_divider = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert!(date_divider.is_date_divider()); + assert_pending!(timeline_stream); Mock::given(method("GET")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) @@ -144,6 +154,8 @@ async fn test_back_pagination() { back_pagination_status, LiveBackPaginationStatus::Idle { hit_start_of_timeline: true } ); + + assert_pending!(timeline_stream); } #[async_test] @@ -212,27 +224,31 @@ async fn test_back_pagination_highlighted() { timeline.live_paginate_backwards(10).await.unwrap(); server.reset().await; - let first = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - let remote_event = first.as_event().unwrap(); - // Own events don't trigger push rules. - assert!(!remote_event.is_highlighted()); - - let second = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - let remote_event = second.as_event().unwrap(); - // `m.room.tombstone` should be highlighted by default. - assert!(remote_event.is_highlighted()); + // `m.room.tombstone` + { + assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + let remote_event = second.as_event().unwrap(); + // `m.room.tombstone` should be highlighted by default. + assert!(remote_event.is_highlighted()); + } + + // `m.room.message` + { + assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + let remote_event = first.as_event().unwrap(); + // Own events don't trigger push rules. + assert!(!remote_event.is_highlighted()); + } + + // Date divider + { + assert_let!( + Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await + ); + assert!(date_divider.is_date_divider()); + } - let date_divider = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert!(date_divider.is_date_divider()); + assert_pending!(timeline_stream); } #[async_test] @@ -615,42 +631,52 @@ async fn test_empty_chunk() { }; join(paginate, observe_paginating).await; - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "hello world"); - - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "the world is big"); - - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); - assert_eq!(state.state_key(), ""); - assert_let!( - AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { - content, - prev_content - }) = state.content() - ); - assert_eq!(content.name, "New room name"); - assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); + // `m.room.name` + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); + assert_eq!(state.state_key(), ""); + assert_let!( + AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { + content, + prev_content + }) = state.content() + ); + assert_eq!(content.name, "New room name"); + assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); + } + + // `m.room.name` is updated + { + assert_let!(Some(VectorDiff::Set { index, .. }) = timeline_stream.next().await); + assert_eq!(index, 0); + } + + // `m.room.message`: “the world is big” + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "the world is big"); + } + + // `m.room.name`: “hello world” + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "hello world"); + } + + // Date divider + { + assert_let!( + Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await + ); + assert!(date_divider.is_date_divider()); + } - let date_divider = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert!(date_divider.is_date_divider()); + assert_pending!(timeline_stream); } #[async_test] @@ -715,59 +741,65 @@ async fn test_until_num_items_with_empty_chunk() { }; join(paginate, observe_paginating).await; - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "hello world"); - - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "the world is big"); - - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); - assert_eq!(state.state_key(), ""); - assert_let!( - AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { - content, - prev_content - }) = state.content() - ); - assert_eq!(content.name, "New room name"); - assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); - - let date_divider = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert!(date_divider.is_date_divider()); + // `m.room.name` + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); + assert_eq!(state.state_key(), ""); + assert_let!( + AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { + content, + prev_content + }) = state.content() + ); + assert_eq!(content.name, "New room name"); + assert_eq!(prev_content.as_ref().unwrap().name.as_ref().unwrap(), "Old room name"); + } + + // `m.room.name` is updated + { + assert_let!(Some(VectorDiff::Set { index, .. }) = timeline_stream.next().await); + assert_eq!(index, 0); + } + + // `m.room.message`: “the world is big” + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "the world is big"); + } + + // `m.room.name`: “hello world” + { + assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "hello world"); + } + + // Date divider + { + assert_let!( + Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await + ); + assert!(date_divider.is_date_divider()); + } timeline.live_paginate_backwards(10).await.unwrap(); - let message = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); - assert_let!(MessageType::Text(text) = msg.msgtype()); - assert_eq!(text.body, "hello room then"); + // `m.room.name`: “hello room then” + { + assert_let!( + Some(VectorDiff::Insert { index, value: message }) = timeline_stream.next().await + ); + assert_eq!(index, 1); + assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); + assert_let!(MessageType::Text(text) = msg.msgtype()); + assert_eq!(text.body, "hello room then"); + } - let date_divider = assert_next_matches!( - timeline_stream, - VectorDiff::PushFront { value } => value - ); - assert!(date_divider.is_date_divider()); - assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); + assert_pending!(timeline_stream); } #[async_test] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs index ae194ceecaf..9a1301acd90 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs @@ -409,9 +409,8 @@ async fn test_timeline_duplicated_events() -> Result<()> { assert_timeline_stream! { [timeline_stream] - update[3] "$x3:bar.org"; - update[1] "$x1:bar.org"; remove[1]; + update[2] "$x3:bar.org"; append "$x1:bar.org"; update[3] "$x1:bar.org"; append "$x4:bar.org"; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs index 249037faa1b..61b0c3efe44 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs @@ -289,6 +289,8 @@ async fn test_timeline_is_reset_when_a_user_is_ignored_or_unignored() { server.reset().await; // Timeline receives events as before. + assert_next_matches!(timeline_stream, VectorDiff::Clear); // TODO: Remove `RoomEventCacheUpdate::Clear` as it creates double + // `VectorDiff::Clear`. assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { assert_eq!(value.as_event().unwrap().event_id(), Some(fourth_event_id)); }); diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 04d4a9774e1..f845287f6b9 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -58,7 +58,7 @@ use tokio::{ sync::Mutex, time::{sleep, timeout}, }; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info, trace, warn}; use wiremock::{matchers::AnyMatcher, Mock, MockServer}; use crate::helpers::{wait_for_room, TestClientBuilder}; @@ -846,7 +846,7 @@ async fn test_delayed_invite_response_and_sent_message_decryption() -> Result<() let bob_sync_service = SyncService::builder(bob.clone()).build().await.unwrap(); bob_sync_service.start().await; - // alice creates a room and invites bob. + // Alice creates a room and will invite Bob. let alice_room = alice .create_room(assign!(CreateRoomRequest::new(), { invite: vec![], @@ -887,30 +887,38 @@ async fn test_delayed_invite_response_and_sent_message_decryption() -> Result<() assert_eq!(bob_room.state(), RoomState::Joined); assert!(bob_room.is_encrypted().await.unwrap()); - let bob_timeline = bob_room.timeline_builder().build().await?; + let bob_timeline = bob_room.timeline().await?; let (_, timeline_stream) = bob_timeline.subscribe().await; pin_mut!(timeline_stream); - // Get previous events, including the sent message + // Get previous events, including the sent messages bob_timeline.paginate_backwards(3).await?; // Look for the sent message, which should not be an UTD event loop { - let diff = timeout(Duration::from_millis(100), timeline_stream.next()) + let diff = timeout(Duration::from_millis(300), timeline_stream.next()) .await - .expect("Timed out. Neither an UTD nor the sent message were found") + .expect("Failed to receive the decrypted sent message") .unwrap(); - if let VectorDiff::PushFront { value } = diff { - if let Some(content) = value.as_event().map(|e| e.content()) { - if let Some(message) = content.as_message() { - if message.body() == "hello world" { - return Ok(()); + + trace!(?diff, "Received diff from Bob's room"); + + match diff { + VectorDiff::PushBack { value: event } + | VectorDiff::Insert { value: event, .. } + | VectorDiff::Set { value: event, .. } => { + if let Some(content) = event.as_event().map(|e| e.content()) { + if let Some(message) = content.as_message() { + if message.body() == "hello world" { + return Ok(()); + } + + panic!("Unexpected message event found"); } - panic!("Unexpected message event found"); - } else if content.as_unable_to_decrypt().is_some() { - panic!("UTD found!") } } + + _ => {} } } } diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 70d11ddc8af..40dbe1b8ed5 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -442,6 +442,11 @@ async fn test_enabling_backups_retries_decryption() { .await .expect("We should be able to paginate the timeline to fetch the history"); + // Wait for the event cache and the timeline to do their job. + // Timeline triggers a pagination, that inserts events in the event cache, that + // then broadcasts new events into the timeline. All this is async. + sleep(Duration::from_millis(300)).await; + let item = timeline.item_by_event_id(&event_id).await.expect("The event should be in the timeline"); @@ -622,6 +627,11 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { .await .expect("We should be able to paginate the timeline to fetch the history"); + // Wait for the event cache and the timeline to do their job. + // Timeline triggers a pagination, that inserts events in the event cache, that + // then broadcasts new events into the timeline. All this is async. + sleep(Duration::from_millis(300)).await; + if let Some(timeline_item) = timeline.item_by_event_id(&event_id).await { item = Some(timeline_item); break; From 891583b70ec204e49d5509ded79530b374596296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 8 Jan 2025 15:46:49 +0100 Subject: [PATCH 890/979] refactor: Add Mutex and RwLock wrappers that panic on poison --- crates/matrix-sdk-common/src/lib.rs | 1 + crates/matrix-sdk-common/src/locks.rs | 130 ++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 crates/matrix-sdk-common/src/locks.rs diff --git a/crates/matrix-sdk-common/src/lib.rs b/crates/matrix-sdk-common/src/lib.rs index 29c6709ec3d..78c2b2d43d2 100644 --- a/crates/matrix-sdk-common/src/lib.rs +++ b/crates/matrix-sdk-common/src/lib.rs @@ -26,6 +26,7 @@ pub mod deserialized_responses; pub mod executor; pub mod failures_cache; pub mod linked_chunk; +pub mod locks; pub mod ring_buffer; pub mod store_locks; pub mod timeout; diff --git a/crates/matrix-sdk-common/src/locks.rs b/crates/matrix-sdk-common/src/locks.rs new file mode 100644 index 00000000000..33a3eb9eac0 --- /dev/null +++ b/crates/matrix-sdk-common/src/locks.rs @@ -0,0 +1,130 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Simplified locks hat panic instead of returning a `Result` when the lock is +//! poisoned. + +use std::{ + fmt, + sync::{Mutex as StdMutex, MutexGuard, RwLock as StdRwLock, RwLockReadGuard, RwLockWriteGuard}, +}; + +use serde::{Deserialize, Serialize}; + +/// A wrapper around `std::sync::Mutex` that panics on poison. +/// +/// This `Mutex` works similarly to the standard library's `Mutex`, except its +/// `lock` method does not return a `Result`. Instead, if the mutex is poisoned, +/// it will panic. +/// +/// # Examples +/// +/// ``` +/// use matrix_sdk_common::locks::Mutex; +/// +/// let mutex = Mutex::new(42); +/// +/// { +/// let mut guard = mutex.lock(); +/// *guard = 100; +/// } +/// +/// assert_eq!(*mutex.lock(), 100); +/// ``` +#[derive(Default)] +pub struct Mutex(StdMutex); + +impl Mutex { + /// Creates a new `Mutex` wrapping the given value. + pub const fn new(t: T) -> Self { + Self(StdMutex::new(t)) + } +} + +impl Mutex { + /// Acquires the lock, panicking if the lock is poisoned. + /// + /// This method blocks the current thread until the lock is acquired. + pub fn lock(&self) -> MutexGuard<'_, T> { + self.0.lock().expect("The Mutex should never be poisoned") + } +} + +impl fmt::Debug for Mutex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// A wrapper around [`std::sync::RwLock`] that panics on poison. +/// +/// This `RwLock` works similarly to the standard library's `RwLock`, except its +/// `read` and `write` methods do not return a `Result`. Instead, if the lock is +/// poisoned, it will panic. +/// +/// # Examples +/// +/// ``` +/// use matrix_sdk_common::locks::RwLock; +/// +/// let lock = RwLock::new(42); +/// +/// { +/// let read_guard = lock.read(); +/// assert_eq!(*read_guard, 42); +/// } +/// { +/// let mut write_guard = lock.write(); +/// *write_guard = 100; +/// } +/// assert_eq!(*lock.read(), 100); +/// ``` +#[derive(Default, Serialize, Deserialize)] +#[serde(transparent)] +pub struct RwLock(StdRwLock); + +impl RwLock { + /// Creates a new `RwLock` wrapping the given value. + pub const fn new(t: T) -> Self { + Self(StdRwLock::new(t)) + } +} + +impl RwLock { + /// Acquires a mutable write lock, panicking if the lock is poisoned. + /// + /// This method blocks the current thread until the lock is acquired. + pub fn write(&self) -> RwLockWriteGuard<'_, T> { + self.0.write().expect("The RwLock should never be poisoned") + } + + /// Acquires a shared read lock, panicking if the lock is poisoned. + /// + /// This method blocks the current thread until the lock is acquired. + pub fn read(&self) -> RwLockReadGuard<'_, T> { + self.0.read().expect("The RwLock should never be poisoned") + } +} + +impl fmt::Debug for RwLock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl From for RwLock { + fn from(value: T) -> Self { + Self::new(value) + } +} From 46dc2a9c5e4176c105ba2b5596b0c2acb7d717a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 8 Jan 2025 15:47:23 +0100 Subject: [PATCH 891/979] refactor: Use the simplified locks in the failures cache --- .../matrix-sdk-common/src/failures_cache.rs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-common/src/failures_cache.rs b/crates/matrix-sdk-common/src/failures_cache.rs index cbed0930fda..84bb2666522 100644 --- a/crates/matrix-sdk-common/src/failures_cache.rs +++ b/crates/matrix-sdk-common/src/failures_cache.rs @@ -15,16 +15,12 @@ //! A TTL cache which can be used to time out repeated operations that might //! experience intermittent failures. -use std::{ - borrow::Borrow, - collections::HashMap, - hash::Hash, - sync::{Arc, RwLock}, - time::Duration, -}; +use std::{borrow::Borrow, collections::HashMap, hash::Hash, sync::Arc, time::Duration}; use ruma::time::Instant; +use super::locks::RwLock; + const MAX_DELAY: u64 = 15 * 60; const MULTIPLIER: u64 = 15; @@ -105,7 +101,7 @@ where T: Borrow, Q: Hash + Eq + ?Sized, { - let lock = self.inner.items.read().unwrap(); + let lock = self.inner.items.read(); let contains = if let Some(item) = lock.get(key) { !item.expired() } else { false }; @@ -127,7 +123,7 @@ where T: Borrow, Q: Hash + Eq + ?Sized, { - let lock = self.inner.items.read().unwrap(); + let lock = self.inner.items.read(); lock.get(key).map(|i| i.failure_count) } @@ -155,7 +151,7 @@ where /// not, will have their TTL extended using an exponential backoff /// algorithm. pub fn extend(&self, iterator: impl IntoIterator) { - let mut lock = self.inner.items.write().unwrap(); + let mut lock = self.inner.items.write(); let now = Instant::now(); @@ -181,7 +177,7 @@ where T: Borrow, Q: Hash + Eq + 'a + ?Sized, { - let mut lock = self.inner.items.write().unwrap(); + let mut lock = self.inner.items.write(); for item in iterator { lock.remove(item); @@ -194,7 +190,7 @@ where /// for immediate retry. #[doc(hidden)] pub fn expire(&self, item: &T) { - let mut lock = self.inner.items.write().unwrap(); + let mut lock = self.inner.items.write(); lock.get_mut(item).map(FailuresItem::expire); } } @@ -221,11 +217,11 @@ mod tests { cache.extend([1u8].iter()); assert!(cache.contains(&1)); - cache.inner.items.write().unwrap().get_mut(&1).unwrap().duration = Duration::from_secs(0); + cache.inner.items.write().get_mut(&1).unwrap().duration = Duration::from_secs(0); assert!(!cache.contains(&1)); cache.remove([1u8].iter()); - assert!(cache.inner.items.read().unwrap().get(&1).is_none()) + assert!(cache.inner.items.read().get(&1).is_none()) } #[test] From 62567ca6eb247c03bd2cfa0fa01042c987f74c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 8 Jan 2025 15:51:01 +0100 Subject: [PATCH 892/979] refactor(crypto): Use the simplified locks across the crypto crate --- .../src/backups/keys/backup.rs | 7 +- .../src/gossiping/machine.rs | 53 ++++++------ crates/matrix-sdk-crypto/src/gossiping/mod.rs | 10 ++- .../src/identities/device.rs | 10 +-- .../matrix-sdk-crypto/src/identities/user.rs | 41 ++++----- crates/matrix-sdk-crypto/src/machine/mod.rs | 3 +- .../src/olm/group_sessions/outbound.rs | 86 +++++++++---------- .../src/session_manager/group_sessions/mod.rs | 22 ++--- .../group_sessions/share_strategy.rs | 4 +- .../src/session_manager/sessions.rs | 50 ++++------- crates/matrix-sdk-crypto/src/store/caches.rs | 16 ++-- .../src/store/memorystore.rs | 79 ++++++++--------- crates/matrix-sdk-crypto/src/store/mod.rs | 15 ++-- .../src/verification/cache.rs | 29 +++---- .../src/verification/machine.rs | 13 ++- .../src/verification/sas/sas_state.rs | 33 ++++--- 16 files changed, 213 insertions(+), 258 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/backups/keys/backup.rs b/crates/matrix-sdk-crypto/src/backups/keys/backup.rs index 83409d950b6..eac48b52b63 100644 --- a/crates/matrix-sdk-crypto/src/backups/keys/backup.rs +++ b/crates/matrix-sdk-crypto/src/backups/keys/backup.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use matrix_sdk_common::locks::Mutex; use ruma::{ api::client::backup::{EncryptedSessionDataInit, KeyBackupData, KeyBackupDataInit}, serde::Base64, @@ -87,7 +88,7 @@ impl MegolmV1BackupKey { /// Get the backup version that this key is used with, if any. pub fn backup_version(&self) -> Option { - self.inner.version.lock().unwrap().clone() + self.inner.version.lock().clone() } /// Set the backup version that this `MegolmV1BackupKey` will be used with. @@ -95,7 +96,7 @@ impl MegolmV1BackupKey { /// The key won't be able to encrypt room keys unless a version has been /// set. pub fn set_version(&self, version: String) { - *self.inner.version.lock().unwrap() = Some(version); + *self.inner.version.lock() = Some(version); } /// Export the given inbound group session, and encrypt the data, ready for diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 5b7a7055ab9..31eb23f23d2 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -25,10 +25,11 @@ use std::{ mem, sync::{ atomic::{AtomicBool, Ordering}, - Arc, RwLock as StdRwLock, + Arc, }, }; +use matrix_sdk_common::locks::RwLock as StdRwLock; use ruma::{ api::client::keys::claim_keys::v3::Request as KeysClaimRequest, events::secret::request::{ @@ -168,14 +169,13 @@ impl GossipMachine { ) -> Result, CryptoStoreError> { let mut key_requests = self.load_outgoing_requests().await?; let key_forwards: Vec = - self.inner.outgoing_requests.read().unwrap().values().cloned().collect(); + self.inner.outgoing_requests.read().values().cloned().collect(); key_requests.extend(key_forwards); let users_for_key_claim: BTreeMap<_, _> = self .inner .users_for_key_claim .read() - .unwrap() .iter() .map(|(key, value)| { let device_map = value @@ -213,7 +213,7 @@ impl GossipMachine { trace!("Received a secret request event from ourselves, ignoring") } else { let request_info = event.to_request_info(); - self.inner.incoming_key_requests.write().unwrap().insert(request_info, event); + self.inner.incoming_key_requests.write().insert(request_info, event); } } @@ -229,8 +229,7 @@ impl GossipMachine { ) -> OlmResult> { let mut changed_sessions = Vec::new(); - let incoming_key_requests = - mem::take(&mut *self.inner.incoming_key_requests.write().unwrap()); + let incoming_key_requests = mem::take(&mut *self.inner.incoming_key_requests.write()); for event in incoming_key_requests.values() { if let Some(s) = match event { @@ -254,7 +253,6 @@ impl GossipMachine { self.inner .users_for_key_claim .write() - .unwrap() .entry(device.user_id().to_owned()) .or_default() .insert(device.device_id().into()); @@ -275,7 +273,7 @@ impl GossipMachine { /// * `device_id` - The device ID of the device that got the Olm session. pub fn retry_keyshare(&self, user_id: &UserId, device_id: &DeviceId) { if let Entry::Occupied(mut e) = - self.inner.users_for_key_claim.write().unwrap().entry(user_id.to_owned()) + self.inner.users_for_key_claim.write().entry(user_id.to_owned()) { e.get_mut().remove(device_id); @@ -284,7 +282,7 @@ impl GossipMachine { } } - let mut incoming_key_requests = self.inner.incoming_key_requests.write().unwrap(); + let mut incoming_key_requests = self.inner.incoming_key_requests.write(); for (key, event) in self.inner.wait_queue.remove(user_id, device_id) { incoming_key_requests.entry(key).or_insert(event); } @@ -555,7 +553,7 @@ impl GossipMachine { request_id: request.txn_id.clone(), request: Arc::new(request.into()), }; - self.inner.outgoing_requests.write().unwrap().insert(request.request_id.clone(), request); + self.inner.outgoing_requests.write().insert(request.request_id.clone(), request); Ok(used_session) } @@ -581,7 +579,7 @@ impl GossipMachine { request_id: request.txn_id.clone(), request: Arc::new(request.into()), }; - self.inner.outgoing_requests.write().unwrap().insert(request.request_id.clone(), request); + self.inner.outgoing_requests.write().insert(request.request_id.clone(), request); Ok(used_session) } @@ -824,7 +822,7 @@ impl GossipMachine { self.save_outgoing_key_info(info).await?; } - self.inner.outgoing_requests.write().unwrap().remove(id); + self.inner.outgoing_requests.write().remove(id); Ok(()) } @@ -840,13 +838,13 @@ impl GossipMachine { "Successfully received a secret, removing the request" ); - self.inner.outgoing_requests.write().unwrap().remove(&key_info.request_id); + self.inner.outgoing_requests.write().remove(&key_info.request_id); // TODO return the key info instead of deleting it so the sync handler // can delete it in one transaction. self.delete_key_info(key_info).await?; let request = key_info.to_cancellation(self.device_id()); - self.inner.outgoing_requests.write().unwrap().insert(request.request_id.clone(), request); + self.inner.outgoing_requests.write().insert(request.request_id.clone(), request); Ok(()) } @@ -1511,7 +1509,6 @@ mod tests { .inner .outgoing_requests .read() - .unwrap() .first_key_value() .map(|(_, r)| r.request_id.clone()) .unwrap(); @@ -1692,7 +1689,7 @@ mod tests { alice_machine.mark_outgoing_request_as_sent(&request.request_id).await.unwrap(); // Bob doesn't have any outgoing requests. - assert!(bob_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(bob_machine.inner.outgoing_requests.read().is_empty()); // Receive the room key request from alice. bob_machine.receive_incoming_key_request(&event); @@ -1702,7 +1699,7 @@ mod tests { bob_machine.collect_incoming_key_requests(&bob_cache).await.unwrap(); } // Now bob does have an outgoing request. - assert!(!bob_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(!bob_machine.inner.outgoing_requests.read().is_empty()); // Get the request and convert it to a encrypted to-device event. let requests = bob_machine.outgoing_to_device_requests().await.unwrap(); @@ -1774,7 +1771,7 @@ mod tests { alice_machine.mark_outgoing_request_as_sent(&request.request_id).await.unwrap(); // Bob doesn't have any outgoing requests. - assert!(bob_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(bob_machine.inner.outgoing_requests.read().is_empty()); // Receive the room key request from alice. bob_machine.receive_incoming_key_request(&event); @@ -1783,7 +1780,7 @@ mod tests { bob_machine.collect_incoming_key_requests(&bob_cache).await.unwrap(); } // Now bob does have an outgoing request. - assert!(!bob_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(!bob_machine.inner.outgoing_requests.read().is_empty()); // Get the request and convert it to a encrypted to-device event. let requests = bob_machine.outgoing_to_device_requests().await.unwrap(); @@ -1875,13 +1872,13 @@ mod tests { }; // No secret found - assert!(alice_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(alice_machine.inner.outgoing_requests.read().is_empty()); alice_machine.receive_incoming_secret_request(&event); { let alice_cache = alice_machine.inner.store.cache().await.unwrap(); alice_machine.collect_incoming_key_requests(&alice_cache).await.unwrap(); } - assert!(alice_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(alice_machine.inner.outgoing_requests.read().is_empty()); // No device found alice_machine.inner.store.reset_cross_signing_identity().await; @@ -1890,7 +1887,7 @@ mod tests { let alice_cache = alice_machine.inner.store.cache().await.unwrap(); alice_machine.collect_incoming_key_requests(&alice_cache).await.unwrap(); } - assert!(alice_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(alice_machine.inner.outgoing_requests.read().is_empty()); alice_machine.inner.store.save_device_data(&[bob_device]).await.unwrap(); @@ -1901,7 +1898,7 @@ mod tests { let alice_cache = alice_machine.inner.store.cache().await.unwrap(); alice_machine.collect_incoming_key_requests(&alice_cache).await.unwrap(); } - assert!(alice_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(alice_machine.inner.outgoing_requests.read().is_empty()); let event = RumaToDeviceEvent { sender: alice_id().to_owned(), @@ -1918,7 +1915,7 @@ mod tests { let alice_cache = alice_machine.inner.store.cache().await.unwrap(); alice_machine.collect_incoming_key_requests(&alice_cache).await.unwrap(); } - assert!(alice_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(alice_machine.inner.outgoing_requests.read().is_empty()); // We need a trusted device, otherwise we won't serve secrets alice_device.set_trust_state(LocalTrust::Verified); @@ -1929,7 +1926,7 @@ mod tests { let alice_cache = alice_machine.inner.store.cache().await.unwrap(); alice_machine.collect_incoming_key_requests(&alice_cache).await.unwrap(); } - assert!(!alice_machine.inner.outgoing_requests.read().unwrap().is_empty()); + assert!(!alice_machine.inner.outgoing_requests.read().is_empty()); } #[async_test] @@ -2053,7 +2050,7 @@ mod tests { // Bob doesn't have any outgoing requests. assert!(bob_machine.outgoing_to_device_requests().await.unwrap().is_empty()); - assert!(bob_machine.inner.users_for_key_claim.read().unwrap().is_empty()); + assert!(bob_machine.inner.users_for_key_claim.read().is_empty()); assert!(bob_machine.inner.wait_queue.is_empty()); // Receive the room key request from alice. @@ -2068,7 +2065,7 @@ mod tests { bob_machine.outgoing_to_device_requests().await.unwrap()[0].request(), AnyOutgoingRequest::KeysClaim(_) ); - assert!(!bob_machine.inner.users_for_key_claim.read().unwrap().is_empty()); + assert!(!bob_machine.inner.users_for_key_claim.read().is_empty()); assert!(!bob_machine.inner.wait_queue.is_empty()); let (alice_session, bob_session) = alice_machine @@ -2096,7 +2093,7 @@ mod tests { bob_machine.inner.store.save_sessions(&[bob_session]).await.unwrap(); bob_machine.retry_keyshare(alice_id(), alice_device_id()); - assert!(bob_machine.inner.users_for_key_claim.read().unwrap().is_empty()); + assert!(bob_machine.inner.users_for_key_claim.read().is_empty()); { let bob_cache = bob_machine.inner.store.cache().await.unwrap(); bob_machine.collect_incoming_key_requests(&bob_cache).await.unwrap(); diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index 2593091b2eb..f608e5fb36c 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -16,10 +16,11 @@ mod machine; use std::{ collections::{BTreeMap, BTreeSet}, - sync::{Arc, RwLock as StdRwLock}, + sync::Arc, }; pub(crate) use machine::GossipMachine; +use matrix_sdk_common::locks::RwLock as StdRwLock; use ruma::{ events::{ room_key_request::{Action, ToDeviceRoomKeyRequestEventContent}, @@ -323,7 +324,7 @@ impl WaitQueue { #[cfg(all(test, feature = "automatic-room-key-forwarding"))] fn is_empty(&self) -> bool { - let read_guard = self.inner.read().unwrap(); + let read_guard = self.inner.read(); read_guard.requests_ids_waiting.is_empty() && read_guard.requests_waiting_for_session.is_empty() } @@ -337,13 +338,14 @@ impl WaitQueue { ); let ids_waiting_key = (device.user_id().to_owned(), device.device_id().into()); - let mut write_guard = self.inner.write().unwrap(); + let mut write_guard = self.inner.write(); write_guard.requests_waiting_for_session.insert(requests_waiting_key, event); write_guard.requests_ids_waiting.entry(ids_waiting_key).or_default().insert(request_id); } fn remove(&self, user_id: &UserId, device_id: &DeviceId) -> Vec<(RequestInfo, RequestEvent)> { - let mut write_guard = self.inner.write().unwrap(); + let mut write_guard = self.inner.write(); + write_guard .requests_ids_waiting .remove(&(user_id.to_owned(), device_id.into())) diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index da84d068a41..cea61624e16 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -17,11 +17,11 @@ use std::{ ops::Deref, sync::{ atomic::{AtomicBool, Ordering}, - Arc, RwLock, + Arc, }, }; -use matrix_sdk_common::deserialized_responses::WithheldCode; +use matrix_sdk_common::{deserialized_responses::WithheldCode, locks::RwLock}; use ruma::{ api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest, events::{key::verification::VerificationMethod, AnyToDeviceEventContent}, @@ -470,7 +470,7 @@ impl Device { ) -> OlmResult> { let (used_session, raw_encrypted) = self.encrypt(event_type, content).await?; - // perist the used session + // Persist the used session self.verification_machine .store .save_changes(Changes { sessions: vec![used_session], ..Default::default() }) @@ -626,7 +626,7 @@ impl DeviceData { /// Get the trust state of the device. pub fn local_trust_state(&self) -> LocalTrust { - *self.trust_state.read().unwrap() + *self.trust_state.read() } /// Is the device locally marked as trusted. @@ -646,7 +646,7 @@ impl DeviceData { /// Note: This should only done in the crypto store where the trust state /// can be stored. pub(crate) fn set_trust_state(&self, state: LocalTrust) { - *self.trust_state.write().unwrap() = state; + *self.trust_state.write() = state; } pub(crate) fn mark_withheld_code_as_sent(&self) { diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index 75f927fd17f..4a4c788dc60 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -17,11 +17,12 @@ use std::{ ops::{Deref, DerefMut}, sync::{ atomic::{AtomicBool, Ordering}, - Arc, RwLock, + Arc, }, }; use as_variant::as_variant; +use matrix_sdk_common::locks::RwLock; use ruma::{ api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest, events::{ @@ -684,7 +685,7 @@ impl From for OtherUserIdentityDataSerializer { user_id: value.user_id.clone(), master_key: value.master_key().to_owned(), self_signing_key: value.self_signing_key().to_owned(), - pinned_master_key: value.pinned_master_key.read().unwrap().clone(), + pinned_master_key: value.pinned_master_key.read().clone(), previously_verified: value.previously_verified.load(Ordering::SeqCst), }; OtherUserIdentityDataSerializer { @@ -787,7 +788,7 @@ impl OtherUserIdentityData { /// which is not verified and is in pin violation. See /// [`OtherUserIdentity::identity_needs_user_approval`]. pub(crate) fn pin(&self) { - let mut m = self.pinned_master_key.write().unwrap(); + let mut m = self.pinned_master_key.write(); *m = self.master_key.as_ref().clone() } @@ -828,7 +829,7 @@ impl OtherUserIdentityData { /// accept and pin the new identity, perform a verification, or /// stop communications. pub(crate) fn has_pin_violation(&self) -> bool { - let pinned_master_key = self.pinned_master_key.read().unwrap(); + let pinned_master_key = self.pinned_master_key.read(); pinned_master_key.get_first_key() != self.master_key().get_first_key() } @@ -858,7 +859,7 @@ impl OtherUserIdentityData { // the previous pinned master key. // This identity will have a pin violation until the new master key is pinned // (see `has_pin_violation()`). - let pinned_master_key = self.pinned_master_key.read().unwrap().clone(); + let pinned_master_key = self.pinned_master_key.read().clone(); // Check if the new master_key is signed by our own **verified** // user_signing_key. If the identity was verified we remember it. @@ -947,7 +948,7 @@ impl PartialEq for OwnUserIdentityData { && self.master_key == other.master_key && self.self_signing_key == other.self_signing_key && self.user_signing_key == other.user_signing_key - && *self.verified.read().unwrap() == *other.verified.read().unwrap() + && *self.verified.read() == *other.verified.read() && self.master_key.signatures() == other.master_key.signatures() } } @@ -1067,12 +1068,12 @@ impl OwnUserIdentityData { /// Mark our identity as verified. pub fn mark_as_verified(&self) { - *self.verified.write().unwrap() = OwnUserIdentityVerifiedState::Verified; + *self.verified.write() = OwnUserIdentityVerifiedState::Verified; } /// Mark our identity as unverified. pub(crate) fn mark_as_unverified(&self) { - let mut guard = self.verified.write().unwrap(); + let mut guard = self.verified.write(); if *guard == OwnUserIdentityVerifiedState::Verified { *guard = OwnUserIdentityVerifiedState::VerificationViolation; } @@ -1080,7 +1081,7 @@ impl OwnUserIdentityData { /// Check if our identity is verified. pub fn is_verified(&self) -> bool { - *self.verified.read().unwrap() == OwnUserIdentityVerifiedState::Verified + *self.verified.read() == OwnUserIdentityVerifiedState::Verified } /// True if we verified our own identity at some point in the past. @@ -1089,7 +1090,7 @@ impl OwnUserIdentityData { /// [`OwnUserIdentityData::withdraw_verification()`]. pub fn was_previously_verified(&self) -> bool { matches!( - *self.verified.read().unwrap(), + *self.verified.read(), OwnUserIdentityVerifiedState::Verified | OwnUserIdentityVerifiedState::VerificationViolation ) @@ -1101,7 +1102,7 @@ impl OwnUserIdentityData { /// reported to the user. In order to remove this notice users have to /// verify again or to withdraw the verification requirement. pub fn withdraw_verification(&self) { - let mut guard = self.verified.write().unwrap(); + let mut guard = self.verified.write(); if *guard == OwnUserIdentityVerifiedState::VerificationViolation { *guard = OwnUserIdentityVerifiedState::NeverVerified; } @@ -1117,7 +1118,7 @@ impl OwnUserIdentityData { /// - Or by withdrawing the verification requirement /// [`OwnUserIdentity::withdraw_verification`]. pub fn has_verification_violation(&self) -> bool { - *self.verified.read().unwrap() == OwnUserIdentityVerifiedState::VerificationViolation + *self.verified.read() == OwnUserIdentityVerifiedState::VerificationViolation } /// Update the identity with a new master key and self signing key. @@ -1523,7 +1524,7 @@ pub(crate) mod tests { }); let migrated: OtherUserIdentityData = serde_json::from_value(serialized_value).unwrap(); - let pinned_master_key = migrated.pinned_master_key.read().unwrap(); + let pinned_master_key = migrated.pinned_master_key.read(); assert_eq!(*pinned_master_key, migrated.master_key().clone()); // Serialize back @@ -1547,12 +1548,12 @@ pub(crate) mod tests { // Set `"verified": false` *json.get_mut("verified").unwrap() = false.into(); let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap(); - assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::NeverVerified); + assert_eq!(*id.verified.read(), OwnUserIdentityVerifiedState::NeverVerified); // Tweak the json to have `"verified": true`, and repeat *json.get_mut("verified").unwrap() = true.into(); let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap(); - assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::Verified); + assert_eq!(*id.verified.read(), OwnUserIdentityVerifiedState::Verified); } #[test] @@ -1565,10 +1566,7 @@ pub(crate) mod tests { let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap(); // Then the value is correctly populated - assert_eq!( - *id.verified.read().unwrap(), - OwnUserIdentityVerifiedState::VerificationViolation - ); + assert_eq!(*id.verified.read(), OwnUserIdentityVerifiedState::VerificationViolation); } #[test] @@ -1581,10 +1579,7 @@ pub(crate) mod tests { let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap(); // Then the old value is re-interpreted as VerificationViolation - assert_eq!( - *id.verified.read().unwrap(), - OwnUserIdentityVerifiedState::VerificationViolation - ); + assert_eq!(*id.verified.read(), OwnUserIdentityVerifiedState::VerificationViolation); } #[test] diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index aa5bbc9a00e..fc79eea1631 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -14,7 +14,7 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, - sync::{Arc, RwLock as StdRwLock}, + sync::Arc, time::Duration, }; @@ -25,6 +25,7 @@ use matrix_sdk_common::{ UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, VerificationState, }, + locks::RwLock as StdRwLock, BoxFuture, }; use ruma::{ diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index 1c5539d7203..95a5949de20 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -18,12 +18,12 @@ use std::{ fmt, sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, RwLock as StdRwLock, + Arc, }, time::Duration, }; -use matrix_sdk_common::deserialized_responses::WithheldCode; +use matrix_sdk_common::{deserialized_responses::WithheldCode, locks::RwLock as StdRwLock}; use ruma::{ events::{ room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility}, @@ -274,7 +274,7 @@ impl OutboundGroupSession { request: Arc, share_infos: ShareInfoSet, ) { - self.to_share_with_set.write().unwrap().insert(request_id, (request, share_infos)); + self.to_share_with_set.write().insert(request_id, (request, share_infos)); } /// Create a new `m.room_key.withheld` event content with the given code for @@ -310,7 +310,7 @@ impl OutboundGroupSession { ) -> BTreeMap> { let mut no_olm_devices = BTreeMap::new(); - let removed = self.to_share_with_set.write().unwrap().remove(request_id); + let removed = self.to_share_with_set.write().remove(request_id); if let Some((to_device, request)) = removed { let recipients: BTreeMap<&UserId, BTreeSet<&DeviceId>> = request .iter() @@ -332,10 +332,10 @@ impl OutboundGroupSession { .collect(); no_olm_devices.insert(user_id.to_owned(), no_olms); - self.shared_with_set.write().unwrap().entry(user_id).or_default().extend(info); + self.shared_with_set.write().entry(user_id).or_default().extend(info); } - if self.to_share_with_set.read().unwrap().is_empty() { + if self.to_share_with_set.read().is_empty() { debug!( session_id = self.session_id(), room_id = ?self.room_id, @@ -347,7 +347,7 @@ impl OutboundGroupSession { } } else { let request_ids: Vec = - self.to_share_with_set.read().unwrap().keys().map(|k| k.to_string()).collect(); + self.to_share_with_set.read().keys().map(|k| k.to_string()).collect(); error!( all_request_ids = ?request_ids, @@ -540,22 +540,21 @@ impl OutboundGroupSession { /// Has or will the session be shared with the given user/device pair. pub(crate) fn is_shared_with(&self, device: &DeviceData) -> ShareState { // Check if we shared the session. - let shared_state = - self.shared_with_set.read().unwrap().get(device.user_id()).and_then(|d| { - d.get(device.device_id()).map(|s| match s { - ShareInfo::Shared(s) => { - if device.curve25519_key() == Some(s.sender_key) { - ShareState::Shared { - message_index: s.message_index, - olm_wedging_index: s.olm_wedging_index, - } - } else { - ShareState::SharedButChangedSenderKey + let shared_state = self.shared_with_set.read().get(device.user_id()).and_then(|d| { + d.get(device.device_id()).map(|s| match s { + ShareInfo::Shared(s) => { + if device.curve25519_key() == Some(s.sender_key) { + ShareState::Shared { + message_index: s.message_index, + olm_wedging_index: s.olm_wedging_index, } + } else { + ShareState::SharedButChangedSenderKey } - ShareInfo::Withheld(_) => ShareState::NotShared, - }) - }); + } + ShareInfo::Withheld(_) => ShareState::NotShared, + }) + }); if let Some(state) = shared_state { state @@ -565,24 +564,23 @@ impl OutboundGroupSession { // Find the first request that contains the given user id and // device ID. - let shared = - self.to_share_with_set.read().unwrap().values().find_map(|(_, share_info)| { - let d = share_info.get(device.user_id())?; - let info = d.get(device.device_id())?; - Some(match info { - ShareInfo::Shared(info) => { - if device.curve25519_key() == Some(info.sender_key) { - ShareState::Shared { - message_index: info.message_index, - olm_wedging_index: info.olm_wedging_index, - } - } else { - ShareState::SharedButChangedSenderKey + let shared = self.to_share_with_set.read().values().find_map(|(_, share_info)| { + let d = share_info.get(device.user_id())?; + let info = d.get(device.device_id())?; + Some(match info { + ShareInfo::Shared(info) => { + if device.curve25519_key() == Some(info.sender_key) { + ShareState::Shared { + message_index: info.message_index, + olm_wedging_index: info.olm_wedging_index, } + } else { + ShareState::SharedButChangedSenderKey } - ShareInfo::Withheld(_) => ShareState::NotShared, - }) - }); + } + ShareInfo::Withheld(_) => ShareState::NotShared, + }) + }); shared.unwrap_or(ShareState::NotShared) } @@ -591,7 +589,6 @@ impl OutboundGroupSession { pub(crate) fn is_withheld_to(&self, device: &DeviceData, code: &WithheldCode) -> bool { self.shared_with_set .read() - .unwrap() .get(device.user_id()) .and_then(|d| { let info = d.get(device.device_id())?; @@ -603,7 +600,7 @@ impl OutboundGroupSession { // Find the first request that contains the given user id and // device ID. - self.to_share_with_set.read().unwrap().values().any(|(_, share_info)| { + self.to_share_with_set.read().values().any(|(_, share_info)| { share_info .get(device.user_id()) .and_then(|d| d.get(device.device_id())) @@ -622,7 +619,7 @@ impl OutboundGroupSession { sender_key: Curve25519PublicKey, index: u32, ) { - self.shared_with_set.write().unwrap().entry(user_id.to_owned()).or_default().insert( + self.shared_with_set.write().entry(user_id.to_owned()).or_default().insert( device_id.to_owned(), ShareInfo::new_shared(sender_key, index, Default::default()), ); @@ -641,7 +638,6 @@ impl OutboundGroupSession { ShareInfo::new_shared(sender_key, self.message_index().await, Default::default()); self.shared_with_set .write() - .unwrap() .entry(user_id.to_owned()) .or_default() .insert(device_id.to_owned(), share_info); @@ -650,12 +646,12 @@ impl OutboundGroupSession { /// Get the list of requests that need to be sent out for this session to be /// marked as shared. pub(crate) fn pending_requests(&self) -> Vec> { - self.to_share_with_set.read().unwrap().values().map(|(req, _)| req.clone()).collect() + self.to_share_with_set.read().values().map(|(req, _)| req.clone()).collect() } /// Get the list of request ids this session is waiting for to be sent out. pub(crate) fn pending_request_ids(&self) -> Vec { - self.to_share_with_set.read().unwrap().keys().cloned().collect() + self.to_share_with_set.read().keys().cloned().collect() } /// Restore a Session from a previously pickled string. @@ -717,8 +713,8 @@ impl OutboundGroupSession { message_count: self.message_count.load(Ordering::SeqCst), shared: self.shared(), invalidated: self.invalidated(), - shared_with_set: self.shared_with_set.read().unwrap().clone(), - requests: self.to_share_with_set.read().unwrap().clone(), + shared_with_set: self.shared_with_set.read().clone(), + requests: self.to_share_with_set.read().clone(), } } } diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs index b6ddeaaa290..2e148471a6d 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs @@ -17,12 +17,14 @@ mod share_strategy; use std::{ collections::{BTreeMap, BTreeSet}, fmt::Debug, - sync::{Arc, RwLock as StdRwLock}, + sync::Arc, }; use futures_util::future::join_all; use itertools::Itertools; -use matrix_sdk_common::{deserialized_responses::WithheldCode, executor::spawn}; +use matrix_sdk_common::{ + deserialized_responses::WithheldCode, executor::spawn, locks::RwLock as StdRwLock, +}; use ruma::{ events::{AnyMessageLikeEventContent, ToDeviceEventType}, serde::Raw, @@ -60,7 +62,7 @@ impl GroupSessionCache { } pub(crate) fn insert(&self, session: OutboundGroupSession) { - self.sessions.write().unwrap().insert(session.room_id().to_owned(), session); + self.sessions.write().insert(session.room_id().to_owned(), session); } /// Either get a session for the given room from the cache or load it from @@ -72,20 +74,20 @@ impl GroupSessionCache { pub async fn get_or_load(&self, room_id: &RoomId) -> Option { // Get the cached session, if there isn't one load one from the store // and put it in the cache. - if let Some(s) = self.sessions.read().unwrap().get(room_id) { + if let Some(s) = self.sessions.read().get(room_id) { return Some(s.clone()); } match self.store.get_outbound_group_session(room_id).await { Ok(Some(s)) => { { - let mut sessions_being_shared = self.sessions_being_shared.write().unwrap(); + let mut sessions_being_shared = self.sessions_being_shared.write(); for request_id in s.pending_request_ids() { sessions_being_shared.insert(request_id, s.clone()); } } - self.sessions.write().unwrap().insert(room_id.to_owned(), s.clone()); + self.sessions.write().insert(room_id.to_owned(), s.clone()); Some(s) } @@ -104,20 +106,20 @@ impl GroupSessionCache { /// * `room_id` - The id of the room for which we should get the outbound /// group session. fn get(&self, room_id: &RoomId) -> Option { - self.sessions.read().unwrap().get(room_id).cloned() + self.sessions.read().get(room_id).cloned() } /// Returns whether any session is withheld with the given device and code. fn has_session_withheld_to(&self, device: &DeviceData, code: &WithheldCode) -> bool { - self.sessions.read().unwrap().values().any(|s| s.is_withheld_to(device, code)) + self.sessions.read().values().any(|s| s.is_withheld_to(device, code)) } fn remove_from_being_shared(&self, id: &TransactionId) -> Option { - self.sessions_being_shared.write().unwrap().remove(id) + self.sessions_being_shared.write().remove(id) } fn mark_as_being_shared(&self, id: OwnedTransactionId, session: OutboundGroupSession) { - self.sessions_being_shared.write().unwrap().insert(id, session); + self.sessions_being_shared.write().insert(id, session); } } diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index f3e5b074b00..f3d6efeaed7 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -125,7 +125,7 @@ pub(crate) async fn collect_session_recipients( trace!(?users, ?settings, "Calculating group session recipients"); let users_shared_with: BTreeSet = - outbound.shared_with_set.read().unwrap().keys().cloned().collect(); + outbound.shared_with_set.read().keys().cloned().collect(); let users_shared_with: BTreeSet<&UserId> = users_shared_with.iter().map(Deref::deref).collect(); @@ -339,7 +339,7 @@ fn is_session_overshared_for_user( let recipient_device_ids: BTreeSet<&DeviceId> = recipient_devices.iter().map(|d| d.device_id()).collect(); - let guard = outbound_session.shared_with_set.read().unwrap(); + let guard = outbound_session.shared_with_set.read(); let Some(shared) = guard.get(user_id) else { return false; diff --git a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs index e1e3aab1725..e7bdc5ba0ed 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs @@ -14,11 +14,11 @@ use std::{ collections::{BTreeMap, BTreeSet}, - sync::{Arc, RwLock as StdRwLock}, + sync::Arc, time::Duration, }; -use matrix_sdk_common::failures_cache::FailuresCache; +use matrix_sdk_common::{failures_cache::FailuresCache, locks::RwLock as StdRwLock}; use ruma::{ api::client::keys::claim_keys::v3::{ Request as KeysClaimRequest, Response as KeysClaimResponse, @@ -98,7 +98,7 @@ impl SessionManager { /// Mark the outgoing request as sent. pub fn mark_outgoing_request_as_sent(&self, id: &TransactionId) { - self.outgoing_to_device_requests.write().unwrap().remove(id); + self.outgoing_to_device_requests.write().remove(id); } pub async fn mark_device_as_wedged( @@ -121,13 +121,11 @@ impl SessionManager { if should_unwedge { self.users_for_key_claim .write() - .unwrap() .entry(device.user_id().to_owned()) .or_default() .insert(device.device_id().into()); self.wedged_devices .write() - .unwrap() .entry(device.user_id().to_owned()) .or_default() .insert(device.device_id().into()); @@ -142,7 +140,6 @@ impl SessionManager { pub fn is_device_wedged(&self, device: &DeviceData) -> bool { self.wedged_devices .read() - .unwrap() .get(device.user_id()) .is_some_and(|d| d.contains(device.device_id())) } @@ -151,13 +148,7 @@ impl SessionManager { /// /// If the device was wedged this will queue up a dummy to-device message. async fn check_if_unwedged(&self, user_id: &UserId, device_id: &DeviceId) -> OlmResult<()> { - if self - .wedged_devices - .write() - .unwrap() - .get_mut(user_id) - .is_some_and(|d| d.remove(device_id)) - { + if self.wedged_devices.write().get_mut(user_id).is_some_and(|d| d.remove(device_id)) { if let Some(device) = self.store.get_device(user_id, device_id).await? { let (_, content) = device.encrypt("m.dummy", ToDeviceDummyEventContent::new()).await?; @@ -176,7 +167,6 @@ impl SessionManager { self.outgoing_to_device_requests .write() - .unwrap() .insert(request.request_id.clone(), request); } } @@ -278,7 +268,7 @@ impl SessionManager { // Add the list of sessions that for some reason automatically need to // create an Olm session. - for (user, device_ids) in self.users_for_key_claim.read().unwrap().iter() { + for (user, device_ids) in self.users_for_key_claim.read().iter() { missing_session_devices_by_user.entry(user.to_owned()).or_default().extend( device_ids .iter() @@ -319,12 +309,12 @@ impl SessionManager { // stash the details of the request so that we can refer to it when handling the // response - *(self.current_key_claim_request.write().unwrap()) = result.clone(); + *(self.current_key_claim_request.write()) = result.clone(); Ok(result) } fn is_user_timed_out(&self, user_id: &UserId, device_id: &DeviceId) -> bool { - self.failed_devices.read().unwrap().get(user_id).is_some_and(|d| d.contains(device_id)) + self.failed_devices.read().get(user_id).is_some_and(|d| d.contains(device_id)) } /// This method will try to figure out for which devices a one-time key was @@ -354,7 +344,7 @@ impl SessionManager { ) { // First check that the response is for the request we were expecting. let request = { - let mut guard = self.current_key_claim_request.write().unwrap(); + let mut guard = self.current_key_claim_request.write(); let expected_request_id = guard.as_ref().map(|e| e.0.as_ref()); if Some(request_id) == expected_request_id { @@ -416,7 +406,7 @@ impl SessionManager { "Tried to create new Olm sessions, but the signed one-time key was missing for some devices", ); - let mut failed_devices_lock = self.failed_devices.write().unwrap(); + let mut failed_devices_lock = self.failed_devices.write(); for (user_id, device_set) in missing_devices_by_user { failed_devices_lock.entry(user_id.clone()).or_default().extend(device_set); @@ -542,7 +532,6 @@ impl SessionManager { self.failed_devices .write() - .unwrap() .entry(user_id.to_owned()) .or_default() .insert(device_id.to_owned()); @@ -573,7 +562,7 @@ impl SessionManager { info!(sessions = ?new_sessions, "Established new Olm sessions"); for (user, device_map) in new_sessions { - if let Some(user_cache) = self.failed_devices.read().unwrap().get(user) { + if let Some(user_cache) = self.failed_devices.read().get(user) { user_cache.remove(device_map.into_keys()); } } @@ -597,14 +586,9 @@ impl SessionManager { #[cfg(test)] mod tests { - use std::{ - collections::BTreeMap, - iter, - ops::Deref, - sync::{Arc, RwLock as StdRwLock}, - time::Duration, - }; + use std::{collections::BTreeMap, iter, ops::Deref, sync::Arc, time::Duration}; + use matrix_sdk_common::locks::RwLock as StdRwLock; use matrix_sdk_test::{async_test, ruma_response_from_json}; use ruma::{ api::client::keys::claim_keys::v3::Response as KeyClaimResponse, device_id, @@ -861,11 +845,11 @@ mod tests { let curve_key = bob_device.curve25519_key().unwrap(); - assert!(!manager.users_for_key_claim.read().unwrap().contains_key(bob.user_id())); + assert!(!manager.users_for_key_claim.read().contains_key(bob.user_id())); assert!(!manager.is_device_wedged(&bob_device)); manager.mark_device_as_wedged(bob_device.user_id(), curve_key).await.unwrap(); assert!(manager.is_device_wedged(&bob_device)); - assert!(manager.users_for_key_claim.read().unwrap().contains_key(bob.user_id())); + assert!(manager.users_for_key_claim.read().contains_key(bob.user_id())); let (txn_id, request) = manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().unwrap(); @@ -885,13 +869,13 @@ mod tests { let response = KeyClaimResponse::new(one_time_keys); - assert!(manager.outgoing_to_device_requests.read().unwrap().is_empty()); + assert!(manager.outgoing_to_device_requests.read().is_empty()); manager.receive_keys_claim_response(&txn_id, &response).await.unwrap(); assert!(!manager.is_device_wedged(&bob_device)); assert!(manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().is_none()); - assert!(!manager.outgoing_to_device_requests.read().unwrap().is_empty()) + assert!(!manager.outgoing_to_device_requests.read().is_empty()) } #[async_test] @@ -1012,7 +996,6 @@ mod tests { manager .failed_devices .write() - .unwrap() .get(alice) .unwrap() .expire(&alice_account.device_id().to_owned()); @@ -1027,7 +1010,6 @@ mod tests { assert!(manager .failed_devices .read() - .unwrap() .get(alice) .unwrap() .failure_count(alice_account.device_id()) diff --git a/crates/matrix-sdk-crypto/src/store/caches.rs b/crates/matrix-sdk-crypto/src/store/caches.rs index 3b45a62ad40..ca3954edf0c 100644 --- a/crates/matrix-sdk-crypto/src/store/caches.rs +++ b/crates/matrix-sdk-crypto/src/store/caches.rs @@ -22,10 +22,11 @@ use std::{ fmt::Display, sync::{ atomic::{AtomicBool, Ordering}, - Arc, RwLock as StdRwLock, Weak, + Arc, Weak, }, }; +use matrix_sdk_common::locks::RwLock as StdRwLock; use ruma::{DeviceId, OwnedDeviceId, OwnedRoomId, OwnedUserId, RoomId, UserId}; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, RwLock}; @@ -104,7 +105,6 @@ impl GroupSessionStore { pub fn add(&self, session: InboundGroupSession) -> bool { self.entries .write() - .unwrap() .entry(session.room_id().to_owned()) .or_default() .insert(session.session_id().to_owned(), session) @@ -113,12 +113,12 @@ impl GroupSessionStore { /// Get all the group sessions the store knows about. pub fn get_all(&self) -> Vec { - self.entries.read().unwrap().values().flat_map(HashMap::values).cloned().collect() + self.entries.read().values().flat_map(HashMap::values).cloned().collect() } /// Get the number of `InboundGroupSession`s we have. pub fn count(&self) -> usize { - self.entries.read().unwrap().values().map(HashMap::len).sum() + self.entries.read().values().map(HashMap::len).sum() } /// Get a inbound group session from our store. @@ -128,7 +128,7 @@ impl GroupSessionStore { /// /// * `session_id` - The unique id of the session. pub fn get(&self, room_id: &RoomId, session_id: &str) -> Option { - self.entries.read().unwrap().get(room_id)?.get(session_id).cloned() + self.entries.read().get(room_id)?.get(session_id).cloned() } } @@ -151,7 +151,6 @@ impl DeviceStore { let user_id = device.user_id(); self.entries .write() - .unwrap() .entry(user_id.to_owned()) .or_default() .insert(device.device_id().into(), device) @@ -160,7 +159,7 @@ impl DeviceStore { /// Get the device with the given device_id and belonging to the given user. pub fn get(&self, user_id: &UserId, device_id: &DeviceId) -> Option { - Some(self.entries.read().unwrap().get(user_id)?.get(device_id)?.clone()) + Some(self.entries.read().get(user_id)?.get(device_id)?.clone()) } /// Remove the device with the given device_id and belonging to the given @@ -168,14 +167,13 @@ impl DeviceStore { /// /// Returns the device if it was removed, None if it wasn't in the store. pub fn remove(&self, user_id: &UserId, device_id: &DeviceId) -> Option { - self.entries.write().unwrap().get_mut(user_id)?.remove(device_id) + self.entries.write().get_mut(user_id)?.remove(device_id) } /// Get a read-only view over all devices of the given user. pub fn user_devices(&self, user_id: &UserId) -> HashMap { self.entries .write() - .unwrap() .entry(user_id.to_owned()) .or_default() .iter() diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 4a4e16dc53c..1e6b4ae7fff 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -15,11 +15,12 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, convert::Infallible, - sync::RwLock as StdRwLock, }; use async_trait::async_trait; -use matrix_sdk_common::store_locks::memory_store_helper::try_take_leased_lock; +use matrix_sdk_common::{ + locks::RwLock as StdRwLock, store_locks::memory_store_helper::try_take_leased_lock, +}; use ruma::{ events::secret::request::SecretName, time::Instant, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, @@ -143,7 +144,7 @@ impl MemoryStore { } fn save_sessions(&self, sessions: Vec) { - let mut session_store = self.sessions.write().unwrap(); + let mut session_store = self.sessions.write(); for session in sessions { let entry = session_store.entry(session.sender_key().to_base64()).or_default(); @@ -159,12 +160,11 @@ impl MemoryStore { fn save_outbound_group_sessions(&self, sessions: Vec) { self.outbound_group_sessions .write() - .unwrap() .extend(sessions.into_iter().map(|s| (s.room_id().to_owned(), s))); } fn save_private_identity(&self, private_identity: Option) { - *self.private_identity.write().unwrap() = private_identity; + *self.private_identity.write() = private_identity; } /// Return all the [`InboundGroupSession`]s we have, paired with the @@ -176,7 +176,6 @@ impl MemoryStore { let lookup = |s: &InboundGroupSession| { self.inbound_group_sessions_backed_up_to .read() - .unwrap() .get(&s.room_id)? .get(s.session_id()) .cloned() @@ -202,11 +201,11 @@ impl CryptoStore for MemoryStore { type Error = Infallible; async fn load_account(&self) -> Result> { - Ok(self.account.read().unwrap().as_ref().map(|acc| acc.deep_clone())) + Ok(self.account.read().as_ref().map(|acc| acc.deep_clone())) } async fn load_identity(&self) -> Result> { - Ok(self.private_identity.read().unwrap().clone()) + Ok(self.private_identity.read().clone()) } async fn next_batch_token(&self) -> Result> { @@ -215,7 +214,7 @@ impl CryptoStore for MemoryStore { async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> { if let Some(account) = changes.account { - *self.account.write().unwrap() = Some(account); + *self.account.write() = Some(account); } Ok(()) @@ -232,7 +231,7 @@ impl CryptoStore for MemoryStore { self.delete_devices(changes.devices.deleted); { - let mut identities = self.identities.write().unwrap(); + let mut identities = self.identities.write(); for identity in changes.identities.new.into_iter().chain(changes.identities.changed) { identities.insert( identity.user_id().to_owned(), @@ -242,15 +241,15 @@ impl CryptoStore for MemoryStore { } { - let mut olm_hashes = self.olm_hashes.write().unwrap(); + let mut olm_hashes = self.olm_hashes.write(); for hash in changes.message_hashes { olm_hashes.entry(hash.sender_key.to_owned()).or_default().insert(hash.hash.clone()); } } { - let mut outgoing_key_requests = self.outgoing_key_requests.write().unwrap(); - let mut key_requests_by_info = self.key_requests_by_info.write().unwrap(); + let mut outgoing_key_requests = self.outgoing_key_requests.write(); + let mut key_requests_by_info = self.key_requests_by_info.write(); for key_request in changes.key_requests { let id = key_request.request_id.clone(); @@ -275,14 +274,14 @@ impl CryptoStore for MemoryStore { } { - let mut secret_inbox = self.secret_inbox.write().unwrap(); + let mut secret_inbox = self.secret_inbox.write(); for secret in changes.secrets { secret_inbox.entry(secret.secret_name.to_string()).or_default().push(secret); } } { - let mut direct_withheld_info = self.direct_withheld_info.write().unwrap(); + let mut direct_withheld_info = self.direct_withheld_info.write(); for (room_id, data) in changes.withheld_session_info { for (session_id, event) in data { direct_withheld_info @@ -298,7 +297,7 @@ impl CryptoStore for MemoryStore { } if !changes.room_settings.is_empty() { - let mut settings = self.room_settings.write().unwrap(); + let mut settings = self.room_settings.write(); settings.extend(changes.room_settings); } @@ -311,7 +310,7 @@ impl CryptoStore for MemoryStore { backed_up_to_version: Option<&str>, ) -> Result<()> { let mut inbound_group_sessions_backed_up_to = - self.inbound_group_sessions_backed_up_to.write().unwrap(); + self.inbound_group_sessions_backed_up_to.write(); for session in sessions { let room_id = session.room_id(); @@ -339,7 +338,7 @@ impl CryptoStore for MemoryStore { } async fn get_sessions(&self, sender_key: &str) -> Result>> { - Ok(self.sessions.read().unwrap().get(sender_key).cloned()) + Ok(self.sessions.read().get(sender_key).cloned()) } async fn get_inbound_group_session( @@ -358,7 +357,6 @@ impl CryptoStore for MemoryStore { Ok(self .direct_withheld_info .read() - .unwrap() .get(room_id) .and_then(|e| Some(e.get(session_id)?.to_owned()))) } @@ -458,7 +456,7 @@ impl CryptoStore for MemoryStore { room_and_session_ids: &[(&RoomId, &str)], ) -> Result<()> { let mut inbound_group_sessions_backed_up_to = - self.inbound_group_sessions_backed_up_to.write().unwrap(); + self.inbound_group_sessions_backed_up_to.write(); for &(room_id, session_id) in room_and_session_ids { let session = self.inbound_group_sessions.get(room_id, session_id); @@ -506,15 +504,15 @@ impl CryptoStore for MemoryStore { &self, room_id: &RoomId, ) -> Result> { - Ok(self.outbound_group_sessions.read().unwrap().get(room_id).cloned()) + Ok(self.outbound_group_sessions.read().get(room_id).cloned()) } async fn load_tracked_users(&self) -> Result> { - Ok(self.tracked_users.read().unwrap().values().cloned().collect()) + Ok(self.tracked_users.read().values().cloned().collect()) } async fn save_tracked_users(&self, tracked_users: &[(&UserId, bool)]) -> Result<()> { - self.tracked_users.write().unwrap().extend(tracked_users.iter().map(|(user_id, dirty)| { + self.tracked_users.write().extend(tracked_users.iter().map(|(user_id, dirty)| { let user_id: OwnedUserId = user_id.to_owned().into(); (user_id.clone(), TrackedUser { user_id, dirty: *dirty }) })); @@ -543,7 +541,7 @@ impl CryptoStore for MemoryStore { } async fn get_user_identity(&self, user_id: &UserId) -> Result> { - let serialized = self.identities.read().unwrap().get(user_id).cloned(); + let serialized = self.identities.read().get(user_id).cloned(); match serialized { None => Ok(None), Some(serialized) => { @@ -557,7 +555,6 @@ impl CryptoStore for MemoryStore { Ok(self .olm_hashes .write() - .unwrap() .entry(message_hash.sender_key.to_owned()) .or_default() .contains(&message_hash.hash)) @@ -567,7 +564,7 @@ impl CryptoStore for MemoryStore { &self, request_id: &TransactionId, ) -> Result> { - Ok(self.outgoing_key_requests.read().unwrap().get(request_id).cloned()) + Ok(self.outgoing_key_requests.read().get(request_id).cloned()) } async fn get_secret_request_by_info( @@ -579,16 +576,14 @@ impl CryptoStore for MemoryStore { Ok(self .key_requests_by_info .read() - .unwrap() .get(&key_info_string) - .and_then(|i| self.outgoing_key_requests.read().unwrap().get(i).cloned())) + .and_then(|i| self.outgoing_key_requests.read().get(i).cloned())) } async fn get_unsent_secret_requests(&self) -> Result> { Ok(self .outgoing_key_requests .read() - .unwrap() .values() .filter(|req| !req.sent_out) .cloned() @@ -596,10 +591,10 @@ impl CryptoStore for MemoryStore { } async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> { - let req = self.outgoing_key_requests.write().unwrap().remove(request_id); + let req = self.outgoing_key_requests.write().remove(request_id); if let Some(i) = req { let key_info_string = encode_key_info(&i.info); - self.key_requests_by_info.write().unwrap().remove(&key_info_string); + self.key_requests_by_info.write().remove(&key_info_string); } Ok(()) @@ -609,36 +604,30 @@ impl CryptoStore for MemoryStore { &self, secret_name: &SecretName, ) -> Result> { - Ok(self - .secret_inbox - .write() - .unwrap() - .entry(secret_name.to_string()) - .or_default() - .to_owned()) + Ok(self.secret_inbox.write().entry(secret_name.to_string()).or_default().to_owned()) } async fn delete_secrets_from_inbox(&self, secret_name: &SecretName) -> Result<()> { - self.secret_inbox.write().unwrap().remove(secret_name.as_str()); + self.secret_inbox.write().remove(secret_name.as_str()); Ok(()) } async fn get_room_settings(&self, room_id: &RoomId) -> Result> { - Ok(self.room_settings.read().unwrap().get(room_id).cloned()) + Ok(self.room_settings.read().get(room_id).cloned()) } async fn get_custom_value(&self, key: &str) -> Result>> { - Ok(self.custom_values.read().unwrap().get(key).cloned()) + Ok(self.custom_values.read().get(key).cloned()) } async fn set_custom_value(&self, key: &str, value: Vec) -> Result<()> { - self.custom_values.write().unwrap().insert(key.to_owned(), value); + self.custom_values.write().insert(key.to_owned(), value); Ok(()) } async fn remove_custom_value(&self, key: &str) -> Result<()> { - self.custom_values.write().unwrap().remove(key); + self.custom_values.write().remove(key); Ok(()) } @@ -648,7 +637,7 @@ impl CryptoStore for MemoryStore { key: &str, holder: &str, ) -> Result { - Ok(try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)) + Ok(try_take_leased_lock(&mut self.leases.write(), lease_duration_ms, key, holder)) } } @@ -1167,7 +1156,7 @@ mod integration_tests { impl MemoryStore { fn get_static_account(&self) -> Option { - self.account.read().unwrap().as_ref().map(|acc| acc.static_data().clone()) + self.account.read().as_ref().map(|acc| acc.static_data().clone()) } } diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 662829f8c35..8f990670d4c 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -43,13 +43,14 @@ use std::{ fmt::Debug, ops::Deref, pin::pin, - sync::{atomic::Ordering, Arc, RwLock as StdRwLock}, + sync::{atomic::Ordering, Arc}, time::Duration, }; use as_variant::as_variant; use futures_core::Stream; use futures_util::StreamExt; +use matrix_sdk_common::locks::RwLock as StdRwLock; use ruma::{ encryption::KeyUsage, events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedUserId, UserId, @@ -155,7 +156,7 @@ impl KeyQueryManager { let tracked_users = cache.store.load_tracked_users().await?; let mut query_users_lock = self.users_for_key_query.lock().await; - let mut tracked_users_cache = cache.tracked_users.write().unwrap(); + let mut tracked_users_cache = cache.tracked_users.write(); for user in tracked_users { tracked_users_cache.insert(user.user_id.to_owned()); @@ -245,7 +246,7 @@ impl SyncedKeyQueryManager<'_> { let mut key_query_lock = self.manager.users_for_key_query.lock().await; { - let mut tracked_users = self.cache.tracked_users.write().unwrap(); + let mut tracked_users = self.cache.tracked_users.write(); for user_id in users { if tracked_users.insert(user_id.to_owned()) { key_query_lock.insert_user(user_id); @@ -271,7 +272,7 @@ impl SyncedKeyQueryManager<'_> { let mut key_query_lock = self.manager.users_for_key_query.lock().await; { - let tracked_users = &self.cache.tracked_users.read().unwrap(); + let tracked_users = &self.cache.tracked_users.read(); for user_id in users { if tracked_users.contains(user_id) { key_query_lock.insert_user(user_id); @@ -297,7 +298,7 @@ impl SyncedKeyQueryManager<'_> { let mut key_query_lock = self.manager.users_for_key_query.lock().await; { - let tracked_users = self.cache.tracked_users.read().unwrap(); + let tracked_users = self.cache.tracked_users.read(); for user_id in users { if tracked_users.contains(user_id) { let clean = key_query_lock.maybe_remove_user(user_id, sequence_number); @@ -330,7 +331,7 @@ impl SyncedKeyQueryManager<'_> { /// See the docs for [`crate::OlmMachine::tracked_users()`]. pub fn tracked_users(&self) -> HashSet { - self.cache.tracked_users.read().unwrap().iter().cloned().collect() + self.cache.tracked_users.read().iter().cloned().collect() } /// Mark the given user as being tracked for device lists, and mark that it @@ -340,7 +341,7 @@ impl SyncedKeyQueryManager<'_> { /// next time [`Store::users_for_key_query()`] is called. pub async fn mark_user_as_changed(&self, user: &UserId) -> Result<()> { self.manager.users_for_key_query.lock().await.insert_user(user); - self.cache.tracked_users.write().unwrap().insert(user.to_owned()); + self.cache.tracked_users.write().insert(user.to_owned()); self.cache.store.save_tracked_users(&[(user, true)]).await } diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index f5e23344a5d..6601cb3d4d0 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -12,12 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::BTreeMap, - sync::{Arc, RwLock as StdRwLock}, -}; +use std::{collections::BTreeMap, sync::Arc}; use as_variant::as_variant; +use matrix_sdk_common::locks::RwLock as StdRwLock; use ruma::{DeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId}; #[cfg(feature = "qrcode")] use tracing::debug; @@ -69,7 +67,7 @@ impl VerificationCache { #[cfg(test)] #[allow(dead_code)] pub fn is_empty(&self) -> bool { - self.inner.verification.read().unwrap().values().all(|m| m.is_empty()) + self.inner.verification.read().values().all(|m| m.is_empty()) } /// Add a new `Verification` object to the cache, this will cancel any @@ -78,7 +76,7 @@ impl VerificationCache { pub fn insert(&self, verification: impl Into) { let verification = verification.into(); - let mut verification_write_guard = self.inner.verification.write().unwrap(); + let mut verification_write_guard = self.inner.verification.write(); let user_verifications = verification_write_guard.entry(verification.other_user().to_owned()).or_default(); @@ -150,22 +148,21 @@ impl VerificationCache { self.inner .verification .write() - .unwrap() .entry(verification.other_user().to_owned()) .or_default() .insert(verification.flow_id().to_owned(), verification.clone()); } pub fn get(&self, sender: &UserId, flow_id: &str) -> Option { - self.inner.verification.read().unwrap().get(sender)?.get(flow_id).cloned() + self.inner.verification.read().get(sender)?.get(flow_id).cloned() } pub fn outgoing_requests(&self) -> Vec { - self.inner.outgoing_requests.read().unwrap().values().cloned().collect() + self.inner.outgoing_requests.read().values().cloned().collect() } pub fn garbage_collect(&self) -> Vec { - let verification = &mut self.inner.verification.write().unwrap(); + let verification = &mut self.inner.verification.write(); for user_verification in verification.values_mut() { user_verification.retain(|_, s| !(s.is_done() || s.is_cancelled())); @@ -186,7 +183,7 @@ impl VerificationCache { pub fn add_request(&self, request: OutgoingRequest) { trace!("Adding an outgoing request {:?}", request); - self.inner.outgoing_requests.write().unwrap().insert(request.request_id.clone(), request); + self.inner.outgoing_requests.write().insert(request.request_id.clone(), request); } pub fn add_verification_request(&self, request: OutgoingVerificationRequest) { @@ -211,7 +208,7 @@ impl VerificationCache { "Storing the request info, waiting for the request to be marked as sent" ); - self.inner.flow_ids_waiting_for_response.write().unwrap().insert( + self.inner.flow_ids_waiting_for_response.write().insert( request_info.request_id.to_owned(), (recipient.to_owned(), request_info.flow_id), ); @@ -235,7 +232,7 @@ impl VerificationCache { request: Arc::new(request.into()), }; - self.inner.outgoing_requests.write().unwrap().insert(request_id, request); + self.inner.outgoing_requests.write().insert(request_id, request); } OutgoingContent::Room(r, c) => { @@ -247,18 +244,18 @@ impl VerificationCache { request_id: request_id.clone(), }; - self.inner.outgoing_requests.write().unwrap().insert(request_id, request); + self.inner.outgoing_requests.write().insert(request_id, request); } } } pub fn mark_request_as_sent(&self, request_id: &TransactionId) { - if let Some(request_id) = self.inner.outgoing_requests.write().unwrap().remove(request_id) { + if let Some(request_id) = self.inner.outgoing_requests.write().remove(request_id) { trace!(?request_id, "Marking a verification HTTP request as sent"); } if let Some((user_id, flow_id)) = - self.inner.flow_ids_waiting_for_response.read().unwrap().get(request_id) + self.inner.flow_ids_waiting_for_response.read().get(request_id) { if let Some(verification) = self.get(user_id, flow_id.as_str()) { match verification { diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index 5e7c0c15cf0..49d7b80ae14 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::HashMap, - sync::{Arc, RwLock as StdRwLock}, -}; +use std::{collections::HashMap, sync::Arc}; +use matrix_sdk_common::locks::RwLock as StdRwLock; use ruma::{ events::{ key::verification::VerificationMethod, AnyToDeviceEvent, AnyToDeviceEventContent, @@ -153,13 +151,12 @@ impl VerificationMachine { user_id: &UserId, flow_id: impl AsRef, ) -> Option { - self.requests.read().unwrap().get(user_id)?.get(flow_id.as_ref()).cloned() + self.requests.read().get(user_id)?.get(flow_id.as_ref()).cloned() } pub fn get_requests(&self, user_id: &UserId) -> Vec { self.requests .read() - .unwrap() .get(user_id) .map(|v| v.iter().map(|(_, value)| value.clone()).collect()) .unwrap_or_default() @@ -174,7 +171,7 @@ impl VerificationMachine { return; } - let mut requests = self.requests.write().unwrap(); + let mut requests = self.requests.write(); let user_requests = requests.entry(request.other_user().to_owned()).or_default(); // Cancel all the old verifications requests as well as the new one we @@ -247,7 +244,7 @@ impl VerificationMachine { let mut events = vec![]; let mut requests: Vec = { - let mut requests = self.requests.write().unwrap(); + let mut requests = self.requests.write(); for user_verification in requests.values_mut() { user_verification.retain(|_, v| !(v.is_done() || v.is_cancelled())); diff --git a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs index 295335bb7f2..958cf6a004d 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs @@ -12,12 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - matches, - sync::{Arc, Mutex}, - time::Duration, -}; +use std::{matches, sync::Arc, time::Duration}; +use matrix_sdk_common::locks::Mutex; use ruma::{ events::{ key::verification::{ @@ -356,7 +353,7 @@ impl SasState { let their_public_key = Curve25519PublicKey::from_slice(content.public_key().as_bytes()) .map_err(|_| CancelCode::from("Invalid public key"))?; - if let Some(sas) = self.inner.lock().unwrap().take() { + if let Some(sas) = self.inner.lock().take() { sas.diffie_hellman(their_public_key).map_err(|_| "Invalid public key".into()) } else { Err(CancelCode::UnexpectedMessage) @@ -1127,7 +1124,7 @@ impl SasState { /// second element the English description of the emoji. pub fn get_emoji(&self) -> [Emoji; 7] { get_emoji( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), self.state.we_started, @@ -1140,7 +1137,7 @@ impl SasState { /// numbers can be converted to a unique emoji defined by the spec. pub fn get_emoji_index(&self) -> [u8; 7] { get_emoji_index( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), self.state.we_started, @@ -1153,7 +1150,7 @@ impl SasState { /// the short auth string. pub fn get_decimal(&self) -> (u16, u16, u16) { get_decimal( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), self.state.we_started, @@ -1175,7 +1172,7 @@ impl SasState { self.check_event(sender, content.flow_id()).map_err(|c| self.clone().cancel(true, c))?; let (devices, master_keys) = receive_mac_event( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), sender, @@ -1239,7 +1236,7 @@ impl SasState { self.check_event(sender, content.flow_id()).map_err(|c| self.clone().cancel(true, c))?; let (devices, master_keys) = receive_mac_event( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), sender, @@ -1283,7 +1280,7 @@ impl SasState { self.check_event(sender, content.flow_id()).map_err(|c| self.clone().cancel(true, c))?; let (devices, master_keys) = receive_mac_event( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), sender, @@ -1315,7 +1312,7 @@ impl SasState { /// The content needs to be automatically sent to the other side. pub fn as_content(&self) -> OutgoingContent { get_mac_content( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, &self.verification_flow_id, self.state.accepted_protocols.message_auth_code, @@ -1376,7 +1373,7 @@ impl SasState { /// second element the English description of the emoji. pub fn get_emoji(&self) -> [Emoji; 7] { get_emoji( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), self.state.we_started, @@ -1389,7 +1386,7 @@ impl SasState { /// numbers can be converted to a unique emoji defined by the spec. pub fn get_emoji_index(&self) -> [u8; 7] { get_emoji_index( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), self.state.we_started, @@ -1402,7 +1399,7 @@ impl SasState { /// the short auth string. pub fn get_decimal(&self) -> (u16, u16, u16) { get_decimal( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, self.verification_flow_id.as_str(), self.state.we_started, @@ -1417,7 +1414,7 @@ impl SasState { /// wasn't already sent. pub fn as_content(&self) -> OutgoingContent { get_mac_content( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, &self.verification_flow_id, self.state.accepted_protocols.message_auth_code, @@ -1480,7 +1477,7 @@ impl SasState { /// wasn't already sent. pub fn as_content(&self) -> OutgoingContent { get_mac_content( - &self.state.sas.lock().unwrap(), + &self.state.sas.lock(), &self.ids, &self.verification_flow_id, self.state.accepted_protocols.message_auth_code, From 61dd560499127d79c2afce64938a6db9685d5ca9 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Jan 2025 13:12:00 +0100 Subject: [PATCH 893/979] feat: Remove the `experimental-sliding-sync` feature flag. Sliding sync is no longer experimental. It has a solid MSC4186, along with a solid implementation inside Synapse. It's time to consider it mature. The SDK continues to support the old MSC3575 in addition to MSC4186. This patch only removes the `experimental-sliding-sync` feature flag. --- bindings/matrix-sdk-ffi/Cargo.toml | 2 - crates/matrix-sdk-base/Cargo.toml | 13 +++-- crates/matrix-sdk-base/src/client.rs | 16 +++--- crates/matrix-sdk-base/src/latest_event.rs | 34 ++++++++---- crates/matrix-sdk-base/src/lib.rs | 1 - crates/matrix-sdk-base/src/rooms/normal.rs | 54 ++++++------------- .../matrix-sdk-base/src/sliding_sync/mod.rs | 38 ++++++++++--- .../src/store/migration_helpers.rs | 8 +-- crates/matrix-sdk-base/src/store/mod.rs | 1 - crates/matrix-sdk-ui/Cargo.toml | 2 +- crates/matrix-sdk/Cargo.toml | 8 +-- crates/matrix-sdk/src/client/builder/mod.rs | 35 +++--------- crates/matrix-sdk/src/client/futures.rs | 1 - crates/matrix-sdk/src/client/mod.rs | 11 ++-- crates/matrix-sdk/src/error.rs | 1 - crates/matrix-sdk/src/http_client/mod.rs | 9 ---- crates/matrix-sdk/src/lib.rs | 2 - crates/matrix-sdk/src/sliding_sync/cache.rs | 4 +- .../tests/integration/encryption/backups.rs | 1 - .../tests/integration/room_preview.rs | 14 +++-- xtask/src/ci.rs | 7 +-- 21 files changed, 112 insertions(+), 150 deletions(-) diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 4268a0220f0..b771e6f5759 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -56,7 +56,6 @@ features = [ "anyhow", "e2e-encryption", "experimental-oidc", - "experimental-sliding-sync", "experimental-widgets", "markdown", "rustls-tls", # note: differ from block below @@ -71,7 +70,6 @@ features = [ "anyhow", "e2e-encryption", "experimental-oidc", - "experimental-sliding-sync", "experimental-widgets", "markdown", "native-tls", # note: differ from block above diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 363b1c8036c..68daed73eb1 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -21,10 +21,6 @@ e2e-encryption = ["dep:matrix-sdk-crypto"] js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"] qrcode = ["matrix-sdk-crypto?/qrcode"] automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"] -experimental-sliding-sync = [ - "ruma/unstable-msc3575", - "ruma/unstable-msc4186", -] uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"] # Private feature, see @@ -65,7 +61,14 @@ matrix-sdk-store-encryption = { workspace = true } matrix-sdk-test = { workspace = true, optional = true } once_cell = { workspace = true } regex = "1.11.1" -ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] } +ruma = { workspace = true, features = [ + "canonical-json", + "unstable-msc2867", + "unstable-msc3381", + "unstable-msc3575", + "unstable-msc4186", + "rand", +] } unicode-normalization = { workspace = true } serde = { workspace = true, features = ["rc"] } serde_json = { workspace = true } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 790a6c00431..6456d4c6c90 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -63,7 +63,7 @@ use tokio::sync::{broadcast, Mutex}; use tokio::sync::{RwLock, RwLockReadGuard}; use tracing::{debug, error, info, instrument, trace, warn}; -#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] +#[cfg(feature = "e2e-encryption")] use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent}; #[cfg(feature = "e2e-encryption")] use crate::RoomMemberships; @@ -766,16 +766,12 @@ impl BaseClient { let (events, room_key_updates) = o.receive_sync_changes(encryption_sync_changes).await?; - #[cfg(feature = "experimental-sliding-sync")] for room_key_update in room_key_updates { if let Some(room) = self.get_room(&room_key_update.room_id) { self.decrypt_latest_events(&room, changes, room_info_notable_updates).await; } } - #[cfg(not(feature = "experimental-sliding-sync"))] // Silence unused variable warnings. - let _ = (room_key_updates, changes, room_info_notable_updates); - Ok(events) } else { // If we have no OlmMachine, just return the events that were passed in. @@ -789,7 +785,7 @@ impl BaseClient { /// 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"))] + #[cfg(feature = "e2e-encryption")] async fn decrypt_latest_events( &self, room: &Room, @@ -810,7 +806,7 @@ impl BaseClient { /// (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"))] + #[cfg(feature = "e2e-encryption")] async fn decrypt_latest_suitable_event( &self, room: &Room, @@ -1747,7 +1743,9 @@ mod tests { async_test, ruma_response_from_json, sync_timeline_event, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, }; - use ruma::{api::client as api, room_id, serde::Raw, user_id, UserId}; + #[cfg(feature = "e2e-encryption")] + use ruma::UserId; + use ruma::{api::client as api, room_id, serde::Raw, user_id}; use serde_json::{json, value::to_raw_value}; use super::BaseClient; @@ -1889,7 +1887,7 @@ mod tests { ); } - #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() { use std::collections::BTreeMap; diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 7884d585193..d610ec90ff5 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -1,8 +1,6 @@ //! Utilities for working with events to decide whether they are suitable for //! use as a [crate::Room::latest_event]. -#![cfg(any(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; #[cfg(feature = "e2e-encryption")] use ruma::{ @@ -10,9 +8,11 @@ use ruma::{ call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, poll::unstable_start::SyncUnstablePollStartEvent, relation::RelationType, - room::member::{MembershipState, SyncRoomMemberEvent}, - room::message::SyncRoomMessageEvent, - room::power_levels::RoomPowerLevels, + room::{ + member::{MembershipState, SyncRoomMemberEvent}, + message::SyncRoomMessageEvent, + power_levels::RoomPowerLevels, + }, sticker::SyncStickerEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, }, @@ -294,11 +294,16 @@ impl LatestEvent { #[cfg(test)] mod tests { + #[cfg(feature = "e2e-encryption")] use std::collections::BTreeMap; + #[cfg(feature = "e2e-encryption")] use assert_matches::assert_matches; + #[cfg(feature = "e2e-encryption")] use assert_matches2::assert_let; use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use ruma::serde::Raw; + #[cfg(feature = "e2e-encryption")] use ruma::{ events::{ call::{ @@ -336,14 +341,16 @@ mod tests { RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent, UnsignedRoomRedactionEvent, }, - owned_event_id, owned_mxc_uri, owned_user_id, - serde::Raw, - MilliSecondsSinceUnixEpoch, UInt, VoipVersionId, + owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt, + VoipVersionId, }; use serde_json::json; - use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent}; + use super::LatestEvent; + #[cfg(feature = "e2e-encryption")] + use super::{is_suitable_for_latest_event, PossibleLatestEvent}; + #[cfg(feature = "e2e-encryption")] #[test] fn test_room_messages_are_suitable() { let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( @@ -368,6 +375,7 @@ mod tests { assert_eq!(m.content.msgtype.msgtype(), "m.image"); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_polls_are_suitable() { let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart( @@ -391,6 +399,7 @@ mod tests { assert_eq!(m.content.poll_start().question.text, "do you like rust?"); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_call_invites_are_suitable() { let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite( @@ -413,6 +422,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_call_notifications_are_suitable() { let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify( @@ -435,6 +445,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_stickers_are_suitable() { let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker( @@ -457,6 +468,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_different_types_of_messagelike_are_unsuitable() { let event = @@ -479,6 +491,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_redacted_messages_are_suitable() { // Ruma does not allow constructing UnsignedRoomRedactionEvent instances. @@ -507,6 +520,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_encrypted_messages_are_unsuitable() { let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted( @@ -530,6 +544,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_state_events_are_unsuitable() { let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic( @@ -549,6 +564,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[test] fn test_replacement_events_are_unsuitable() { let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!"); diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 1c37a915f52..ba165ba8026 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -38,7 +38,6 @@ mod rooms; pub mod read_receipts; pub use read_receipts::PreviousEventsProvider; pub use rooms::RoomMembersUpdate; -#[cfg(feature = "experimental-sliding-sync")] pub mod sliding_sync; pub mod store; diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 204a64d512a..3a1b8a61d75 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] +#[cfg(feature = "e2e-encryption")] use std::sync::RwLock as SyncRwLock; use std::{ collections::{BTreeMap, BTreeSet, HashSet}, @@ -24,12 +24,9 @@ use as_variant::as_variant; use bitflags::bitflags; use eyeball::{AsyncLock, ObservableWriteGuard, SharedObservable, Subscriber}; use futures_util::{Stream, StreamExt}; -#[cfg(feature = "experimental-sliding-sync")] use matrix_sdk_common::deserialized_responses::TimelineEventKind; -#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] +#[cfg(feature = "e2e-encryption")] use matrix_sdk_common::ring_buffer::RingBuffer; -#[cfg(feature = "experimental-sliding-sync")] -use ruma::events::AnySyncTimelineEvent; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, events::{ @@ -51,7 +48,7 @@ use ruma::{ tombstone::RoomTombstoneEventContent, }, tag::{TagEventContent, Tags}, - AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, + AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, RoomAccountDataEventType, StateEventType, SyncStateEvent, }, room::RoomType, @@ -67,12 +64,11 @@ use super::{ members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomMember, RoomNotableTags, }; -#[cfg(feature = "experimental-sliding-sync")] -use crate::latest_event::LatestEvent; use crate::{ deserialized_responses::{ DisplayName, MemberEvent, RawMemberEvent, RawSyncOrStrippedState, SyncOrStrippedState, }, + latest_event::LatestEvent, notification_settings::RoomNotificationMode, read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, @@ -166,7 +162,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(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + #[cfg(feature = "e2e-encryption")] pub latest_encrypted_events: Arc>>>, /// A map for ids of room membership events in the knocking state linked to @@ -277,7 +273,7 @@ pub enum RoomMembersUpdate { impl Room { /// The size of the latest_encrypted_events RingBuffer // SAFETY: `new_unchecked` is safe because 10 is not zero. - #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + #[cfg(feature = "e2e-encryption")] const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize = unsafe { std::num::NonZeroUsize::new_unchecked(10) }; @@ -304,7 +300,7 @@ impl Room { room_id: room_info.room_id.clone(), store, inner: SharedObservable::new(room_info), - #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + #[cfg(feature = "e2e-encryption")] latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new( Self::MAX_ENCRYPTED_EVENTS, ))), @@ -925,7 +921,6 @@ impl Room { /// 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().latest_event.as_deref().cloned() } @@ -934,7 +929,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(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + #[cfg(feature = "e2e-encryption")] pub(crate) fn latest_encrypted_events(&self) -> Vec> { self.latest_encrypted_events.read().unwrap().iter().cloned().collect() } @@ -949,7 +944,7 @@ impl Room { /// /// It is the responsibility of the caller to apply the changes into the /// state store after calling this function. - #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + #[cfg(feature = "e2e-encryption")] pub(crate) fn on_latest_event_decrypted( &self, latest_event: Box, @@ -1195,7 +1190,6 @@ impl Room { /// Returns the recency stamp of the room. /// /// Please read `RoomInfo::recency_stamp` to learn more. - #[cfg(feature = "experimental-sliding-sync")] pub fn recency_stamp(&self) -> Option { self.inner.read().recency_stamp } @@ -1405,7 +1399,6 @@ pub struct RoomInfo { pub(crate) encryption_state_synced: bool, /// The last event send by sliding sync - #[cfg(feature = "experimental-sliding-sync")] pub(crate) latest_event: Option>, /// Information about read receipts for this room. @@ -1439,7 +1432,6 @@ pub struct RoomInfo { /// Sliding Sync might "ignore” some events when computing the recency /// stamp of the room. Thus, using this `recency_stamp` value is /// more accurate than relying on the latest event. - #[cfg(feature = "experimental-sliding-sync")] #[serde(default)] pub(crate) recency_stamp: Option, } @@ -1475,14 +1467,12 @@ impl RoomInfo { last_prev_batch: None, sync_info: SyncInfo::NoState, encryption_state_synced: false, - #[cfg(feature = "experimental-sliding-sync")] latest_event: None, read_receipts: Default::default(), base_info: Box::new(BaseRoomInfo::new()), warned_about_unknown_room_version: Arc::new(false.into()), cached_display_name: None, cached_user_defined_notification_mode: None, - #[cfg(feature = "experimental-sliding-sync")] recency_stamp: None, } } @@ -1627,7 +1617,6 @@ impl RoomInfo { }; tracing::Span::current().record("redacts", debug(redacts)); - #[cfg(feature = "experimental-sliding-sync")] if let Some(latest_event) = &mut self.latest_event { tracing::trace!("Checking if redaction applies to latest event"); if latest_event.event_id().as_deref() == Some(redacts) { @@ -1717,19 +1706,16 @@ impl RoomInfo { } /// Updates the joined member count. - #[cfg(feature = "experimental-sliding-sync")] pub(crate) fn update_joined_member_count(&mut self, count: u64) { self.summary.joined_member_count = count; } /// Updates the invited member count. - #[cfg(feature = "experimental-sliding-sync")] pub(crate) fn update_invited_member_count(&mut self, count: u64) { self.summary.invited_member_count = count; } /// Updates the room heroes. - #[cfg(feature = "experimental-sliding-sync")] pub(crate) fn update_heroes(&mut self, heroes: Vec) { self.summary.room_heroes = heroes; } @@ -1929,7 +1915,6 @@ impl RoomInfo { } /// Returns the latest (decrypted) event recorded for this room. - #[cfg(feature = "experimental-sliding-sync")] pub fn latest_event(&self) -> Option<&LatestEvent> { self.latest_event.as_deref() } @@ -1937,7 +1922,6 @@ impl RoomInfo { /// Updates the recency stamp of this room. /// /// Please read [`Self::recency_stamp`] to learn more. - #[cfg(feature = "experimental-sliding-sync")] pub(crate) fn update_recency_stamp(&mut self, stamp: u64) { self.recency_stamp = Some(stamp); } @@ -2023,7 +2007,6 @@ impl RoomInfo { } } -#[cfg(feature = "experimental-sliding-sync")] fn apply_redaction( event: &Raw, raw_redaction: &Raw, @@ -2168,7 +2151,6 @@ mod tests { }; use assign::assign; - #[cfg(feature = "experimental-sliding-sync")] use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use matrix_sdk_test::{ async_test, @@ -2205,9 +2187,8 @@ mod tests { use stream_assert::{assert_pending, assert_ready}; use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo}; - #[cfg(any(feature = "experimental-sliding-sync", feature = "e2e-encryption"))] - use crate::latest_event::LatestEvent; use crate::{ + latest_event::LatestEvent, rooms::RoomNotableTags, store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig}, test_utils::logged_in_base_client, @@ -2216,7 +2197,6 @@ mod tests { }; #[test] - #[cfg(feature = "experimental-sliding-sync")] fn test_room_info_serialization() { // This test exists to make sure we don't accidentally change the // serialized format for `RoomInfo`. @@ -2403,7 +2383,6 @@ mod tests { } #[test] - #[cfg(feature = "experimental-sliding-sync")] fn test_room_info_deserialization() { use ruma::{owned_mxc_uri, owned_user_id}; @@ -3153,8 +3132,8 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() { use std::collections::BTreeMap; @@ -3173,7 +3152,6 @@ mod tests { user_id: user_id!("@alice:example.org").into(), device_id: ruma::device_id!("AYEAYEAYE").into(), }, - #[cfg(feature = "e2e-encryption")] None, ) .await @@ -3221,8 +3199,8 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() { use std::collections::BTreeMap; @@ -3251,8 +3229,8 @@ mod tests { assert_eq!(room.latest_event().unwrap().event_id(), event.event_id()); } + #[cfg(feature = "e2e-encryption")] #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() { use std::collections::BTreeMap; @@ -3290,8 +3268,8 @@ mod tests { assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id()); } + #[cfg(feature = "e2e-encryption")] #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_replacing_the_newest_event_leaves_none_left() { use std::collections::BTreeMap; @@ -3323,7 +3301,7 @@ mod tests { assert_eq!(enc_evs.len(), 0); } - #[cfg(feature = "experimental-sliding-sync")] + #[cfg(feature = "e2e-encryption")] fn add_encrypted_event(room: &Room, event_id: &str) { room.latest_encrypted_events .write() @@ -3331,7 +3309,7 @@ mod tests { .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap()); } - #[cfg(feature = "experimental-sliding-sync")] + #[cfg(feature = "e2e-encryption")] fn make_latest_event(event_id: &str) -> Box { Box::new(LatestEvent::new(SyncTimelineEvent::new( Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(), diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index a5b7d726c3d..c9db56c8eb1 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -895,14 +895,15 @@ fn process_room_properties( #[cfg(test)] mod tests { - use std::{ - collections::{BTreeMap, HashSet}, - sync::{Arc, RwLock as SyncRwLock}, - }; + use std::collections::{BTreeMap, HashSet}; + #[cfg(feature = "e2e-encryption")] + use std::sync::{Arc, RwLock as SyncRwLock}; use assert_matches::assert_matches; + use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + #[cfg(feature = "e2e-encryption")] use matrix_sdk_common::{ - deserialized_responses::{SyncTimelineEvent, UnableToDecryptInfo, UnableToDecryptReason}, + deserialized_responses::{UnableToDecryptInfo, UnableToDecryptReason}, ring_buffer::RingBuffer, }; use matrix_sdk_test::async_test; @@ -928,13 +929,16 @@ mod tests { }; use serde_json::json; - use super::{cache_latest_events, http}; + #[cfg(feature = "e2e-encryption")] + use super::cache_latest_events; + use super::http; use crate::{ rooms::normal::{RoomHero, RoomInfoNotableUpdateReasons}, - store::MemoryStore, test_utils::logged_in_base_client, - BaseClient, Room, RoomInfoNotableUpdate, RoomState, + BaseClient, RoomInfoNotableUpdate, RoomState, }; + #[cfg(feature = "e2e-encryption")] + use crate::{store::MemoryStore, Room}; #[async_test] async fn test_notification_count_set() { @@ -1938,6 +1942,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_when_no_events_we_dont_cache_any() { let events = &[]; @@ -1945,6 +1950,7 @@ mod tests { assert!(chosen.is_none()); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_when_only_one_event_we_cache_it() { let event1 = make_event("m.room.message", "$1"); @@ -1953,6 +1959,7 @@ mod tests { assert_eq!(ev_id(chosen), rawev_id(event1)); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_with_multiple_events_we_cache_the_last_one() { let event1 = make_event("m.room.message", "$1"); @@ -1962,6 +1969,7 @@ mod tests { assert_eq!(ev_id(chosen), rawev_id(event2)); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() { let event1 = make_event("m.room.message", "$1"); @@ -1973,6 +1981,7 @@ mod tests { assert_eq!(ev_id(chosen), rawev_id(event2)); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_prefer_to_cache_nothing_rather_than_irrelevant_events() { let event1 = make_event("m.room.power_levels", "$1"); @@ -1981,6 +1990,7 @@ mod tests { assert!(chosen.is_none()); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_cache_encrypted_events_that_are_after_latest_message() { // Given two message events followed by two encrypted @@ -2011,6 +2021,7 @@ mod tests { assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event4])); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_dont_cache_encrypted_events_that_are_before_latest_message() { // Given an encrypted event before and after the message @@ -2035,6 +2046,7 @@ mod tests { assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3])); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_skip_irrelevant_events_eg_receipts_even_if_after_message() { // Given two message events followed by two encrypted, with a receipt in the @@ -2062,6 +2074,7 @@ mod tests { assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event5])); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_only_store_the_max_number_of_encrypted_events() { // Given two message events followed by lots of encrypted and other irrelevant @@ -2120,6 +2133,7 @@ mod tests { ); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_dont_overflow_capacity_if_previous_encrypted_events_exist() { // Given a RoomInfo with lots of encrypted events already inside it @@ -2162,6 +2176,7 @@ mod tests { assert_eq!(rawevs_ids(&room.latest_encrypted_events)[9], "$a"); } + #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_existing_encrypted_events_are_deleted_if_we_receive_unencrypted() { // Given a RoomInfo with some encrypted events already inside it @@ -2590,6 +2605,7 @@ mod tests { assert!(room_2.is_direct().await.unwrap()); } + #[cfg(feature = "e2e-encryption")] async fn choose_event_to_cache(events: &[SyncTimelineEvent]) -> Option { let room = make_room(); let mut room_info = room.clone_info(); @@ -2598,6 +2614,7 @@ mod tests { room.latest_event().map(|latest_event| latest_event.event().clone()) } + #[cfg(feature = "e2e-encryption")] fn rawev_id(event: SyncTimelineEvent) -> String { event.event_id().unwrap().to_string() } @@ -2606,14 +2623,17 @@ mod tests { event.unwrap().event_id().unwrap().to_string() } + #[cfg(feature = "e2e-encryption")] fn rawevs_ids(events: &Arc>>>) -> Vec { events.read().unwrap().iter().map(|e| e.get_field("event_id").unwrap().unwrap()).collect() } + #[cfg(feature = "e2e-encryption")] fn evs_ids(events: &[SyncTimelineEvent]) -> Vec { events.iter().map(|e| e.event_id().unwrap().to_string()).collect() } + #[cfg(feature = "e2e-encryption")] fn make_room() -> Room { let (sender, _receiver) = tokio::sync::broadcast::channel(1); @@ -2640,10 +2660,12 @@ mod tests { .unwrap() } + #[cfg(feature = "e2e-encryption")] fn make_event(typ: &str, id: &str) -> SyncTimelineEvent { SyncTimelineEvent::new(make_raw_event(typ, id)) } + #[cfg(feature = "e2e-encryption")] fn make_encrypted_event(id: &str) -> SyncTimelineEvent { SyncTimelineEvent::new_utd_event( Raw::from_json_string( diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index c8164096757..e38be077036 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -19,7 +19,6 @@ use std::{ sync::Arc, }; -#[cfg(feature = "experimental-sliding-sync")] use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use ruma::{ events::{ @@ -42,10 +41,9 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; -#[cfg(feature = "experimental-sliding-sync")] -use crate::latest_event::LatestEvent; use crate::{ deserialized_responses::SyncOrStrippedState, + latest_event::LatestEvent, rooms::{ normal::{RoomSummary, SyncInfo}, BaseRoomInfo, RoomNotableTags, @@ -78,7 +76,6 @@ pub struct RoomInfoV1 { sync_info: SyncInfo, #[serde(default = "encryption_state_default")] // see fn docs for why we use this default encryption_state_synced: bool, - #[cfg(feature = "experimental-sliding-sync")] latest_event: Option, base_info: BaseRoomInfoV1, } @@ -106,7 +103,6 @@ impl RoomInfoV1 { last_prev_batch, sync_info, encryption_state_synced, - #[cfg(feature = "experimental-sliding-sync")] latest_event, base_info, } = self; @@ -122,14 +118,12 @@ impl RoomInfoV1 { last_prev_batch, sync_info, encryption_state_synced, - #[cfg(feature = "experimental-sliding-sync")] latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))), read_receipts: Default::default(), base_info: base_info.migrate(create), warned_about_unknown_room_version: Arc::new(false.into()), cached_display_name: None, cached_user_defined_notification_mode: None, - #[cfg(feature = "experimental-sliding-sync")] recency_stamp: None, } } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index fd31b917734..57c04a35900 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -276,7 +276,6 @@ impl Store { } /// Check if a room exists. - #[cfg(feature = "experimental-sliding-sync")] pub(crate) fn room_exists(&self, room_id: &RoomId) -> bool { self.rooms.read().unwrap().get(room_id).is_some() } diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index 2ba3efa8db5..2006e0f6397 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -35,7 +35,7 @@ growable-bloom-filter = { workspace = true } imbl = { workspace = true, features = ["serde"] } indexmap = { workspace = true } itertools = { workspace = true } -matrix-sdk = { workspace = true, features = ["experimental-sliding-sync", "e2e-encryption"] } +matrix-sdk = { workspace = true, features = ["e2e-encryption"] } matrix-sdk-base = { workspace = true } mime = { workspace = true } once_cell = { workspace = true } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 450f008491a..e7e0b3d1139 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -55,10 +55,6 @@ experimental-oidc = [ "dep:tower", "dep:openidconnect", ] -experimental-sliding-sync = [ - "matrix-sdk-base/experimental-sliding-sync", - "reqwest/gzip", -] experimental-widgets = ["dep:language-tags", "dep:uuid"] docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode"] @@ -119,7 +115,7 @@ zeroize = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers = { workspace = true, features = ["futures"] } -reqwest = { workspace = true } +reqwest = { workspace = true, features = ["gzip"] } tokio = { workspace = true, features = ["macros"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -127,7 +123,7 @@ backoff = { version = "0.4.0", features = ["tokio"] } openidconnect = { version = "4.0.0-rc.1", optional = true } # only activate reqwest's stream feature on non-wasm, the wasm part seems to not # support *sending* streams, which makes it useless for us. -reqwest = { workspace = true, features = ["stream"] } +reqwest = { workspace = true, features = ["stream", "gzip"] } tokio = { workspace = true, features = ["fs", "rt", "macros"] } tokio-util = "0.7.12" wiremock = { workspace = true, optional = true } diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index f1606790517..8902f23608c 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -36,12 +36,10 @@ use crate::encryption::EncryptionSettings; use crate::http_client::HttpSettings; #[cfg(feature = "experimental-oidc")] use crate::oidc::OidcCtx; -#[cfg(feature = "experimental-sliding-sync")] -use crate::sliding_sync::VersionBuilder as SlidingSyncVersionBuilder; use crate::{ authentication::AuthCtx, client::ClientServerCapabilities, config::RequestConfig, - error::RumaApiError, http_client::HttpClient, send_queue::SendQueueData, HttpError, - IdParseError, + error::RumaApiError, http_client::HttpClient, send_queue::SendQueueData, + sliding_sync::VersionBuilder as SlidingSyncVersionBuilder, HttpError, IdParseError, }; /// Builder that allows creating and configuring various parts of a [`Client`]. @@ -86,7 +84,6 @@ use crate::{ #[derive(Clone, Debug)] pub struct ClientBuilder { homeserver_cfg: Option, - #[cfg(feature = "experimental-sliding-sync")] sliding_sync_version_builder: SlidingSyncVersionBuilder, http_cfg: Option, store_config: BuilderStoreConfig, @@ -110,7 +107,6 @@ impl ClientBuilder { pub(crate) fn new() -> Self { Self { homeserver_cfg: None, - #[cfg(feature = "experimental-sliding-sync")] sliding_sync_version_builder: SlidingSyncVersionBuilder::Native, http_cfg: None, store_config: BuilderStoreConfig::Custom(StoreConfig::new( @@ -195,7 +191,6 @@ impl ClientBuilder { } /// Set sliding sync to a specific version. - #[cfg(feature = "experimental-sliding-sync")] pub fn sliding_sync_version_builder( mut self, version_builder: SlidingSyncVersionBuilder, @@ -497,7 +492,6 @@ impl ClientBuilder { let HomeserverDiscoveryResult { server, homeserver, well_known, supported_versions } = homeserver_cfg.discover(&http_client).await?; - #[cfg(feature = "experimental-sliding-sync")] let sliding_sync_version = { let supported_versions = match supported_versions { Some(versions) => Some(versions), @@ -543,7 +537,6 @@ impl ClientBuilder { auth_ctx, server, homeserver, - #[cfg(feature = "experimental-sliding-sync")] sliding_sync_version, http_client, base_client, @@ -742,7 +735,6 @@ pub enum ClientBuildError { AutoDiscovery(FromHttpResponseError), /// Error when building the sliding sync version. - #[cfg(feature = "experimental-sliding-sync")] #[error(transparent)] SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError), @@ -771,7 +763,6 @@ pub(crate) mod tests { use assert_matches::assert_matches; use matrix_sdk_test::{async_test, test_json}; use serde_json::{json_internal, Value as JsonValue}; - #[cfg(feature = "experimental-sliding-sync")] use url::Url; use wiremock::{ matchers::{method, path}, @@ -779,7 +770,6 @@ pub(crate) mod tests { }; use super::*; - #[cfg(feature = "experimental-sliding-sync")] use crate::sliding_sync::Version as SlidingSyncVersion; #[test] @@ -861,7 +851,6 @@ pub(crate) mod tests { let _client = builder.build().await.unwrap(); // Then a client should be built with native support for sliding sync. - #[cfg(feature = "experimental-sliding-sync")] assert!(_client.sliding_sync_version().is_native()); } @@ -871,7 +860,6 @@ pub(crate) mod tests { // proxy injected. let homeserver = make_mock_homeserver().await; let mut builder = ClientBuilder::new(); - #[cfg(feature = "experimental-sliding-sync")] let url = { let url = Url::parse("https://localhost:1234").unwrap(); builder = builder.sliding_sync_version_builder(SlidingSyncVersionBuilder::Proxy { @@ -883,12 +871,11 @@ pub(crate) mod tests { // When building a client with the server's URL. builder = builder.server_name_or_homeserver_url(homeserver.uri()); - let _client = builder.build().await.unwrap(); + let client = builder.build().await.unwrap(); // Then a client should be built with support for sliding sync. - #[cfg(feature = "experimental-sliding-sync")] assert_matches!( - _client.sliding_sync_version(), + client.sliding_sync_version(), SlidingSyncVersion::Proxy { url: given_url } => { assert_eq!(given_url, url); } @@ -940,16 +927,14 @@ pub(crate) mod tests { // When building a client with the base server. builder = builder.server_name_or_homeserver_url(server.uri()); - let _client = builder.build().await.unwrap(); + let client = builder.build().await.unwrap(); // Then a client should be built with native support for sliding sync. // It's native support because it's the default. Nothing is checked here. - #[cfg(feature = "experimental-sliding-sync")] - assert!(_client.sliding_sync_version().is_native()); + assert!(client.sliding_sync_version().is_native()); } #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_discovery_well_known_with_sliding_sync() { // Given a base server with a well-known file that points to a homeserver with a // sliding sync proxy. @@ -971,12 +956,11 @@ pub(crate) mod tests { builder = builder .server_name_or_homeserver_url(server.uri()) .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverProxy); - let _client = builder.build().await.unwrap(); + let client = builder.build().await.unwrap(); // Then a client should be built with support for sliding sync. - #[cfg(feature = "experimental-sliding-sync")] assert_matches!( - _client.sliding_sync_version(), + client.sliding_sync_version(), SlidingSyncVersion::Proxy { url } => { assert_eq!(url, Url::parse("https://localhost:1234").unwrap()); } @@ -984,7 +968,6 @@ pub(crate) mod tests { } #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_discovery_well_known_with_sliding_sync_override() { // Given a base server with a well-known file that points to a homeserver with a // sliding sync proxy. @@ -1022,7 +1005,6 @@ pub(crate) mod tests { } #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_sliding_sync_discover_proxy() { // Given a homeserver with a `.well-known` file. let homeserver = make_mock_homeserver().await; @@ -1057,7 +1039,6 @@ pub(crate) mod tests { } #[async_test] - #[cfg(feature = "experimental-sliding-sync")] async fn test_sliding_sync_discover_native() { // Given a homeserver with a `/versions` file. let homeserver = make_mock_homeserver().await; diff --git a/crates/matrix-sdk/src/client/futures.rs b/crates/matrix-sdk/src/client/futures.rs index da5d8d8764b..7c828cc7b48 100644 --- a/crates/matrix-sdk/src/client/futures.rs +++ b/crates/matrix-sdk/src/client/futures.rs @@ -71,7 +71,6 @@ impl SendRequest { /// /// This is useful at the moment because the current sliding sync /// implementation uses a proxy server. - #[cfg(feature = "experimental-sliding-sync")] pub fn with_homeserver_override(mut self, homeserver_override: Option) -> Self { self.homeserver_override = homeserver_override; self diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index c0802de757e..22825062d60 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -79,8 +79,6 @@ use url::Url; use self::futures::SendRequest; #[cfg(feature = "experimental-oidc")] use crate::oidc::Oidc; -#[cfg(feature = "experimental-sliding-sync")] -use crate::sliding_sync::Version as SlidingSyncVersion; use crate::{ authentication::{AuthCtx, AuthData, ReloadSessionCallback, SaveSessionCallback}, config::RequestConfig, @@ -96,6 +94,7 @@ use crate::{ notification_settings::NotificationSettings, room_preview::RoomPreview, send_queue::SendQueueData, + sliding_sync::Version as SlidingSyncVersion, sync::{RoomUpdate, SyncResponse}, Account, AuthApi, AuthSession, Error, Media, Pusher, RefreshTokenError, Result, Room, TransmissionProgress, @@ -258,7 +257,7 @@ pub(crate) struct ClientInner { /// This is the URL for the client-server Matrix API. homeserver: StdRwLock, - #[cfg(feature = "experimental-sliding-sync")] + /// The sliding sync version. sliding_sync_version: StdRwLock, /// The underlying HTTP client. @@ -345,7 +344,7 @@ impl ClientInner { auth_ctx: Arc, server: Option, homeserver: Url, - #[cfg(feature = "experimental-sliding-sync")] sliding_sync_version: SlidingSyncVersion, + sliding_sync_version: SlidingSyncVersion, http_client: HttpClient, base_client: BaseClient, server_capabilities: ClientServerCapabilities, @@ -359,7 +358,6 @@ impl ClientInner { server, homeserver: StdRwLock::new(homeserver), auth_ctx, - #[cfg(feature = "experimental-sliding-sync")] sliding_sync_version: StdRwLock::new(sliding_sync_version), http_client, base_client, @@ -515,13 +513,11 @@ impl Client { } /// Get the sliding sync version. - #[cfg(feature = "experimental-sliding-sync")] pub fn sliding_sync_version(&self) -> SlidingSyncVersion { self.inner.sliding_sync_version.read().unwrap().clone() } /// Override the sliding sync version. - #[cfg(feature = "experimental-sliding-sync")] pub fn set_sliding_sync_version(&self, version: SlidingSyncVersion) { let mut lock = self.inner.sliding_sync_version.write().unwrap(); *lock = version; @@ -2390,7 +2386,6 @@ impl Client { self.inner.auth_ctx.clone(), self.server().cloned(), self.homeserver(), - #[cfg(feature = "experimental-sliding-sync")] self.sliding_sync_version(), self.inner.http_client.clone(), self.inner diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 82fe040b845..99658d2fc38 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -338,7 +338,6 @@ pub enum Error { UserTagName(#[from] InvalidUserTagName), /// An error occurred within sliding-sync - #[cfg(feature = "experimental-sliding-sync")] #[error(transparent)] SlidingSync(#[from] crate::sliding_sync::Error), diff --git a/crates/matrix-sdk/src/http_client/mod.rs b/crates/matrix-sdk/src/http_client/mod.rs index 6cc674bf64e..e0944d558e1 100644 --- a/crates/matrix-sdk/src/http_client/mod.rs +++ b/crates/matrix-sdk/src/http_client/mod.rs @@ -208,15 +208,6 @@ impl HttpClient { span.record("request_size", ByteSize(request_size).to_string_as(true)); } - // Since sliding sync is experimental, and the proxy might not do what we expect - // it to do given a specific request body, it's useful to log the - // request body here. This doesn't contain any personal information. - // TODO: Remove this once sliding sync isn't experimental anymore. - #[cfg(feature = "experimental-sliding-sync")] - if type_name::() == "ruma_client_api::sync::sync_events::v4::Request" { - span.record("request_body", debug(request.body())); - } - request }; diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 6d74698f71a..bfc7a9bc2af 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -61,7 +61,6 @@ pub mod futures { pub use super::client::futures::SendRequest; } -#[cfg(feature = "experimental-sliding-sync")] pub mod sliding_sync; pub mod sync; #[cfg(feature = "experimental-widgets")] @@ -85,7 +84,6 @@ pub use media::Media; pub use pusher::Pusher; pub use room::Room; pub use ruma::{IdParseError, OwnedServerName, ServerName}; -#[cfg(feature = "experimental-sliding-sync")] pub use sliding_sync::{ SlidingSync, SlidingSyncBuilder, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncListLoadingState, SlidingSyncMode, SlidingSyncRoom, UpdateSummary, diff --git a/crates/matrix-sdk/src/sliding_sync/cache.rs b/crates/matrix-sdk/src/sliding_sync/cache.rs index 27b906b153b..22f52f27b15 100644 --- a/crates/matrix-sdk/src/sliding_sync/cache.rs +++ b/crates/matrix-sdk/src/sliding_sync/cache.rs @@ -75,7 +75,7 @@ async fn clean_storage( /// Store the `SlidingSync`'s state in the storage. pub(super) async fn store_sliding_sync_state( sliding_sync: &SlidingSync, - position: &SlidingSyncPositionMarkers, + _position: &SlidingSyncPositionMarkers, ) -> Result<()> { let storage_key = &sliding_sync.inner.storage_key; let instance_storage_key = format_storage_key_for_sliding_sync(storage_key); @@ -94,6 +94,8 @@ pub(super) async fn store_sliding_sync_state( #[cfg(feature = "e2e-encryption")] { + let position = _position; + // FIXME (TERRIBLE HACK): we want to save `pos` in a cross-process safe manner, // with both processes sharing the same database backend; that needs to // go in the crypto process store at the moment, but should be fixed diff --git a/crates/matrix-sdk/tests/integration/encryption/backups.rs b/crates/matrix-sdk/tests/integration/encryption/backups.rs index 6a335763ac4..5f41f1e3929 100644 --- a/crates/matrix-sdk/tests/integration/encryption/backups.rs +++ b/crates/matrix-sdk/tests/integration/encryption/backups.rs @@ -714,7 +714,6 @@ async fn test_incremental_upload_of_keys() -> Result<()> { } #[async_test] -#[cfg(feature = "experimental-sliding-sync")] async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { use tokio::task::spawn_blocking; diff --git a/crates/matrix-sdk/tests/integration/room_preview.rs b/crates/matrix-sdk/tests/integration/room_preview.rs index 71d2dcb8d7a..39fd06a2f23 100644 --- a/crates/matrix-sdk/tests/integration/room_preview.rs +++ b/crates/matrix-sdk/tests/integration/room_preview.rs @@ -1,15 +1,14 @@ -#[cfg(feature = "experimental-sliding-sync")] use js_int::uint; use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; -#[cfg(feature = "experimental-sliding-sync")] -use matrix_sdk_base::sliding_sync; -use matrix_sdk_base::RoomState; +use matrix_sdk_base::{sliding_sync, RoomState}; use matrix_sdk_test::{ async_test, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, SyncResponseBuilder, }; -#[cfg(feature = "experimental-sliding-sync")] -use ruma::{api::client::sync::sync_events::v5::response::Hero, assign, owned_user_id}; -use ruma::{events::room::member::MembershipState, room_id, space::SpaceRoomJoinRule, RoomId}; +use ruma::{ + api::client::sync::sync_events::v5::response::Hero, assign, + events::room::member::MembershipState, owned_user_id, room_id, space::SpaceRoomJoinRule, + RoomId, +}; use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, @@ -115,7 +114,6 @@ async fn test_room_preview_leave_unknown_room_fails() { assert!(client.get_room(room_id).is_none()); } -#[cfg(feature = "experimental-sliding-sync")] #[async_test] async fn test_room_preview_computes_name_if_room_is_known() { let (client, _) = logged_in_client_with_server().await; diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 05ea8695996..f961d390071 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -193,7 +193,7 @@ fn check_clippy() -> Result<()> { "rustup run {NIGHTLY} cargo clippy --workspace --all-targets --exclude matrix-sdk-crypto --exclude xtask --no-default-features - --features native-tls,experimental-sliding-sync,sso-login,testing + --features native-tls,sso-login,testing -- -D warnings" ) .run()?; @@ -214,10 +214,7 @@ fn check_docs() -> Result<()> { fn run_feature_tests(cmd: Option) -> Result<()> { let args = BTreeMap::from([ - ( - FeatureSet::NoEncryption, - "--no-default-features --features sqlite,native-tls,experimental-sliding-sync,testing", - ), + (FeatureSet::NoEncryption, "--no-default-features --features sqlite,native-tls,testing"), ( FeatureSet::NoSqlite, "--no-default-features --features e2e-encryption,native-tls,testing", From 6b2233f8c4a89a310853cbeaba95fdacf566c5e8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Jan 2025 14:58:47 +0100 Subject: [PATCH 894/979] fix(sdk): Use `spawn` from `matrix_sdk_common` to make it compatible with `wasm32-u-u`. --- crates/matrix-sdk/src/sliding_sync/client.rs | 2 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 7 +++---- crates/matrix-sdk/src/sliding_sync/utils.rs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index d232e698f20..2cad1347733 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -361,7 +361,7 @@ async fn update_in_memory_caches(client: &Client, response: &SyncResponse) -> Re Ok(()) } -#[cfg(test)] +#[cfg(all(test, not(target_family = "wasm")))] mod tests { use std::collections::BTreeMap; diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 9e6494b155d..11a3d78589b 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -36,14 +36,14 @@ use async_stream::stream; pub use client::{Version, VersionBuilder}; use futures_core::stream::Stream; pub use matrix_sdk_base::sliding_sync::http; -use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timer}; +use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, executor::spawn, timer}; use ruma::{ api::{client::error::ErrorKind, OutgoingRequest}, assign, OwnedEventId, OwnedRoomId, RoomId, }; use serde::{Deserialize, Serialize}; use tokio::{ - select, spawn, + select, sync::{broadcast::Sender, Mutex as AsyncMutex, OwnedMutexGuard, RwLock as AsyncRwLock}, }; use tracing::{debug, error, info, instrument, trace, warn, Instrument, Span}; @@ -1090,7 +1090,7 @@ fn compute_limited( } } -#[cfg(test)] +#[cfg(all(test, not(target_family = "wasm")))] #[allow(clippy::dbg_macro)] mod tests { use std::{ @@ -2857,7 +2857,6 @@ mod tests { Ok(()) } - #[cfg(not(target_arch = "wasm32"))] // b/o tokio::time::sleep #[async_test] async fn test_aborted_request_doesnt_update_future_requests() -> Result<()> { let server = MockServer::start().await; diff --git a/crates/matrix-sdk/src/sliding_sync/utils.rs b/crates/matrix-sdk/src/sliding_sync/utils.rs index 0bc1e998d43..98d2619b635 100644 --- a/crates/matrix-sdk/src/sliding_sync/utils.rs +++ b/crates/matrix-sdk/src/sliding_sync/utils.rs @@ -6,7 +6,7 @@ use std::{ task::{Context, Poll}, }; -use tokio::task::{JoinError, JoinHandle}; +use matrix_sdk_common::executor::{JoinError, JoinHandle}; /// Private type to ensure a task is aborted on drop. pub(crate) struct AbortOnDrop(JoinHandle); From 5675ac7f46fee634ca98d285348ae5c9f2bc7535 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Jan 2025 15:10:33 +0100 Subject: [PATCH 895/979] refactor(sdk): Remove `SlidingSyncRoomInner::client`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch removes `SlidingSyncRoomInner::client` because, first off, it's not `Send`, and second, it's useless. Nobody uses it, it's basically dead code… annoying dead code… bad dead code! --- .../integration/timeline/sliding_sync.rs | 24 ++--- crates/matrix-sdk/src/sliding_sync/cache.rs | 9 +- crates/matrix-sdk/src/sliding_sync/error.rs | 2 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 12 +-- crates/matrix-sdk/src/sliding_sync/room.rs | 99 +++++++------------ crates/matrix-sdk/src/sliding_sync/utils.rs | 4 +- 6 files changed, 56 insertions(+), 94 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs index 9a1301acd90..75a11647970 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs @@ -20,8 +20,8 @@ use assert_matches2::assert_let; use eyeball_im::{Vector, VectorDiff}; use futures_util::{pin_mut, FutureExt, Stream, StreamExt}; use matrix_sdk::{ - test_utils::logged_in_client_with_server, SlidingSync, SlidingSyncList, SlidingSyncListBuilder, - SlidingSyncMode, UpdateSummary, + test_utils::{logged_in_client, logged_in_client_with_server}, + Client, SlidingSync, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode, UpdateSummary, }; use matrix_sdk_test::{async_test, mocks::mock_encryption_state}; use matrix_sdk_ui::{ @@ -223,7 +223,9 @@ macro_rules! assert_timeline_stream { pub(crate) use assert_timeline_stream; -async fn new_sliding_sync(lists: Vec) -> Result<(MockServer, SlidingSync)> { +async fn new_sliding_sync( + lists: Vec, +) -> Result<(Client, MockServer, SlidingSync)> { let (client, server) = logged_in_client_with_server().await; let mut sliding_sync_builder = client.sliding_sync("integration-test")?; @@ -234,7 +236,7 @@ async fn new_sliding_sync(lists: Vec) -> Result<(MockSer let sliding_sync = sliding_sync_builder.build().await?; - Ok((server, sliding_sync)) + Ok((client, server, sliding_sync)) } async fn create_one_room( @@ -268,13 +270,13 @@ async fn create_one_room( } async fn timeline_test_helper( + client: &Client, sliding_sync: &SlidingSync, room_id: &RoomId, ) -> Result<(Vector>, impl Stream>>)> { let sliding_sync_room = sliding_sync.get_room(room_id).await.unwrap(); let room_id = sliding_sync_room.room_id(); - let client = sliding_sync_room.client(); let sdk_room = client.get_room(room_id).ok_or_else(|| { anyhow::anyhow!("Room {room_id} not found in client. Can't provide a timeline for it") })?; @@ -295,7 +297,7 @@ impl Match for SlidingSyncMatcher { #[async_test] async fn test_timeline_basic() -> Result<()> { - let (server, sliding_sync) = new_sliding_sync(vec![SlidingSyncList::builder("foo") + let (client, server, sliding_sync) = new_sliding_sync(vec![SlidingSyncList::builder("foo") .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10))]) .await?; @@ -309,7 +311,7 @@ async fn test_timeline_basic() -> Result<()> { mock_encryption_state(&server, false).await; let (timeline_items, mut timeline_stream) = - timeline_test_helper(&sliding_sync, room_id).await?; + timeline_test_helper(&client, &sliding_sync, room_id).await?; assert!(timeline_items.is_empty()); // Receiving a bunch of events. @@ -344,7 +346,7 @@ async fn test_timeline_basic() -> Result<()> { #[async_test] async fn test_timeline_duplicated_events() -> Result<()> { - let (server, sliding_sync) = new_sliding_sync(vec![SlidingSyncList::builder("foo") + let (client, server, sliding_sync) = new_sliding_sync(vec![SlidingSyncList::builder("foo") .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10))]) .await?; @@ -357,7 +359,7 @@ async fn test_timeline_duplicated_events() -> Result<()> { mock_encryption_state(&server, false).await; - let (_, mut timeline_stream) = timeline_test_helper(&sliding_sync, room_id).await?; + let (_, mut timeline_stream) = timeline_test_helper(&client, &sliding_sync, room_id).await?; // Receiving events. { @@ -422,7 +424,7 @@ async fn test_timeline_duplicated_events() -> Result<()> { #[async_test] async fn test_timeline_read_receipts_are_updated_live() -> Result<()> { - let (server, sliding_sync) = new_sliding_sync(vec![SlidingSyncList::builder("foo") + let (client, server, sliding_sync) = new_sliding_sync(vec![SlidingSyncList::builder("foo") .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10))]) .await?; @@ -436,7 +438,7 @@ async fn test_timeline_read_receipts_are_updated_live() -> Result<()> { mock_encryption_state(&server, false).await; let (timeline_items, mut timeline_stream) = - timeline_test_helper(&sliding_sync, room_id).await?; + timeline_test_helper(&client, &sliding_sync, room_id).await?; assert!(timeline_items.is_empty()); // Receiving initial events. diff --git a/crates/matrix-sdk/src/sliding_sync/cache.rs b/crates/matrix-sdk/src/sliding_sync/cache.rs index 22f52f27b15..a882e0e6df2 100644 --- a/crates/matrix-sdk/src/sliding_sync/cache.rs +++ b/crates/matrix-sdk/src/sliding_sync/cache.rs @@ -248,10 +248,7 @@ pub(super) async fn restore_sliding_sync_state( restored_fields.rooms = frozen_rooms .into_iter() .map(|frozen_room| { - ( - frozen_room.room_id.clone(), - SlidingSyncRoom::from_frozen(frozen_room, client.clone()), - ) + (frozen_room.room_id.clone(), SlidingSyncRoom::from_frozen(frozen_room)) }) .collect(); } @@ -355,11 +352,11 @@ mod tests { rooms.insert( room_id1.clone(), - SlidingSyncRoom::new(client.clone(), room_id1.clone(), None, Vec::new()), + SlidingSyncRoom::new(room_id1.clone(), None, Vec::new()), ); rooms.insert( room_id2.clone(), - SlidingSyncRoom::new(client.clone(), room_id2.clone(), None, Vec::new()), + SlidingSyncRoom::new(room_id2.clone(), None, Vec::new()), ); } diff --git a/crates/matrix-sdk/src/sliding_sync/error.rs b/crates/matrix-sdk/src/sliding_sync/error.rs index c72d5333e03..774e31510e3 100644 --- a/crates/matrix-sdk/src/sliding_sync/error.rs +++ b/crates/matrix-sdk/src/sliding_sync/error.rs @@ -1,7 +1,7 @@ //! Sliding Sync errors. +use matrix_sdk_common::executor::JoinError; use thiserror::Error; -use tokio::task::JoinError; /// Internal representation of errors in Sliding Sync. #[derive(Error, Debug)] diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 11a3d78589b..cfdf45105b4 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -359,7 +359,6 @@ impl SlidingSync { rooms_map.insert( room_id.clone(), SlidingSyncRoom::new( - self.inner.client.clone(), room_id.clone(), room_data.prev_batch, timeline, @@ -2269,9 +2268,6 @@ mod tests { #[async_test] async fn test_limited_flag_computation() { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; - let make_event = |event_id: &str| -> SyncTimelineEvent { SyncTimelineEvent::new( Raw::from_json_string( @@ -2313,7 +2309,6 @@ mod tests { // it's not marked as initial in the response. not_initial.to_owned(), SlidingSyncRoom::new( - client.clone(), no_overlap.to_owned(), None, vec![event_a.clone(), event_b.clone()], @@ -2323,7 +2318,6 @@ mod tests { // This has no events overlapping with the response timeline, hence limited. no_overlap.to_owned(), SlidingSyncRoom::new( - client.clone(), no_overlap.to_owned(), None, vec![event_a.clone(), event_b.clone()], @@ -2333,7 +2327,6 @@ mod tests { // This has event_c in common with the response timeline. partial_overlap.to_owned(), SlidingSyncRoom::new( - client.clone(), partial_overlap.to_owned(), None, vec![event_a.clone(), event_b.clone(), event_c.clone()], @@ -2343,7 +2336,6 @@ mod tests { // This has all events in common with the response timeline. complete_overlap.to_owned(), SlidingSyncRoom::new( - client.clone(), partial_overlap.to_owned(), None, vec![event_c.clone(), event_d.clone()], @@ -2354,7 +2346,6 @@ mod tests { // limited. no_remote_events.to_owned(), SlidingSyncRoom::new( - client.clone(), no_remote_events.to_owned(), None, vec![event_c.clone(), event_d.clone()], @@ -2364,14 +2355,13 @@ mod tests { // We don't have events for this room locally, and even if the remote room contains // some events, it's not a limited sync. no_local_events.to_owned(), - SlidingSyncRoom::new(client.clone(), no_local_events.to_owned(), None, vec![]), + SlidingSyncRoom::new(no_local_events.to_owned(), None, vec![]), ), ( // Already limited, but would be marked limited if the flag wasn't ignored (same as // partial overlap). already_limited.to_owned(), SlidingSyncRoom::new( - client, already_limited.to_owned(), None, vec![event_a, event_b, event_c.clone()], diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 1f4b4dbec1f..b78cdc147f0 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -8,8 +8,6 @@ use matrix_sdk_base::{deserialized_responses::SyncTimelineEvent, sliding_sync::h use ruma::{OwnedRoomId, RoomId}; use serde::{Deserialize, Serialize}; -use crate::Client; - /// The state of a [`SlidingSyncRoom`]. #[derive(Copy, Clone, Debug, Default, PartialEq)] pub enum SlidingSyncRoomState { @@ -40,14 +38,12 @@ pub struct SlidingSyncRoom { impl SlidingSyncRoom { /// Create a new `SlidingSyncRoom`. pub fn new( - client: Client, room_id: OwnedRoomId, prev_batch: Option, timeline: Vec, ) -> Self { Self { inner: Arc::new(SlidingSyncRoomInner { - client, room_id, state: RwLock::new(SlidingSyncRoomState::NotLoaded), prev_batch: RwLock::new(prev_batch), @@ -74,11 +70,6 @@ impl SlidingSyncRoom { self.inner.timeline_queue.read().unwrap().clone() } - /// Get a clone of the associated client. - pub fn client(&self) -> Client { - self.inner.client.clone() - } - pub(super) fn update( &mut self, room_data: http::response::Room, @@ -129,12 +120,11 @@ impl SlidingSyncRoom { *state = SlidingSyncRoomState::Loaded; } - pub(super) fn from_frozen(frozen_room: FrozenSlidingSyncRoom, client: Client) -> Self { + pub(super) fn from_frozen(frozen_room: FrozenSlidingSyncRoom) -> Self { let FrozenSlidingSyncRoom { room_id, prev_batch, timeline_queue } = frozen_room; Self { inner: Arc::new(SlidingSyncRoomInner { - client, room_id, prev_batch: RwLock::new(prev_batch), state: RwLock::new(SlidingSyncRoomState::Preloaded), @@ -157,9 +147,6 @@ impl SlidingSyncRoom { #[derive(Debug)] struct SlidingSyncRoomInner { - /// The client, used to fetch [`Room`][crate::Room]. - client: Client, - /// The room ID. room_id: OwnedRoomId, @@ -232,13 +219,9 @@ mod tests { use matrix_sdk_test::async_test; use ruma::{events::room::message::RoomMessageEventContent, room_id, serde::Raw, RoomId}; use serde_json::json; - use wiremock::MockServer; use super::{http, NUMBER_OF_TIMELINE_EVENTS_TO_KEEP_FOR_THE_CACHE}; - use crate::{ - sliding_sync::{FrozenSlidingSyncRoom, SlidingSyncRoom, SlidingSyncRoomState}, - test_utils::logged_in_client, - }; + use crate::sliding_sync::{FrozenSlidingSyncRoom, SlidingSyncRoom, SlidingSyncRoomState}; macro_rules! room_response { ( $( $json:tt )+ ) => { @@ -248,24 +231,21 @@ mod tests { }; } - async fn new_room(room_id: &RoomId, inner: http::response::Room) -> SlidingSyncRoom { - new_room_with_timeline(room_id, inner, vec![]).await + fn new_room(room_id: &RoomId, inner: http::response::Room) -> SlidingSyncRoom { + new_room_with_timeline(room_id, inner, vec![]) } - async fn new_room_with_timeline( + fn new_room_with_timeline( room_id: &RoomId, inner: http::response::Room, timeline: Vec, ) -> SlidingSyncRoom { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; - - SlidingSyncRoom::new(client, room_id.to_owned(), inner.prev_batch, timeline) + SlidingSyncRoom::new(room_id.to_owned(), inner.prev_batch, timeline) } #[async_test] async fn test_state_from_not_loaded() { - let mut room = new_room(room_id!("!foo:bar.org"), room_response!({})).await; + let mut room = new_room(room_id!("!foo:bar.org"), room_response!({})); assert_eq!(room.state(), SlidingSyncRoomState::NotLoaded); @@ -277,7 +257,7 @@ mod tests { #[async_test] async fn test_state_from_preloaded() { - let mut room = new_room(room_id!("!foo:bar.org"), room_response!({})).await; + let mut room = new_room(room_id!("!foo:bar.org"), room_response!({})); room.set_state(SlidingSyncRoomState::Preloaded); @@ -290,7 +270,7 @@ mod tests { #[async_test] async fn test_room_room_id() { let room_id = room_id!("!foo:bar.org"); - let room = new_room(room_id, room_response!({})).await; + let room = new_room(room_id, room_response!({})); assert_eq!(room.room_id(), room_id); } @@ -299,7 +279,7 @@ mod tests { async fn test_prev_batch() { // Default value. { - let room = new_room(room_id!("!foo:bar.org"), room_response!({})).await; + let room = new_room(room_id!("!foo:bar.org"), room_response!({})); assert_eq!(room.prev_batch(), None); } @@ -307,15 +287,14 @@ mod tests { // Some value when initializing. { let room = - new_room(room_id!("!foo:bar.org"), room_response!({"prev_batch": "t111_222_333"})) - .await; + new_room(room_id!("!foo:bar.org"), room_response!({"prev_batch": "t111_222_333"})); assert_eq!(room.prev_batch(), Some("t111_222_333".to_owned())); } // Some value when updating. { - let mut room = new_room(room_id!("!foo:bar.org"), room_response!({})).await; + let mut room = new_room(room_id!("!foo:bar.org"), room_response!({})); assert_eq!(room.prev_batch(), None); @@ -329,7 +308,7 @@ mod tests { #[async_test] async fn test_timeline_queue_initially_empty() { - let room = new_room(room_id!("!foo:bar.org"), room_response!({})).await; + let room = new_room(room_id!("!foo:bar.org"), room_response!({})); assert!(room.timeline_queue().is_empty()); } @@ -368,8 +347,8 @@ mod tests { }; } - #[async_test] - async fn test_timeline_queue_initially_not_empty() { + #[test] + fn test_timeline_queue_initially_not_empty() { let room = new_room_with_timeline( room_id!("!foo:bar.org"), room_response!({}), @@ -377,8 +356,7 @@ mod tests { timeline_event!(from "@alice:baz.org" with id "$x0:baz.org" at 0: "message 0"), timeline_event!(from "@alice:baz.org" with id "$x1:baz.org" at 1: "message 1"), ], - ) - .await; + ); { let timeline_queue = room.timeline_queue(); @@ -394,8 +372,8 @@ mod tests { } } - #[async_test] - async fn test_timeline_queue_update_with_empty_timeline() { + #[test] + fn test_timeline_queue_update_with_empty_timeline() { let mut room = new_room_with_timeline( room_id!("!foo:bar.org"), room_response!({}), @@ -403,8 +381,7 @@ mod tests { timeline_event!(from "@alice:baz.org" with id "$x0:baz.org" at 0: "message 0"), timeline_event!(from "@alice:baz.org" with id "$x1:baz.org" at 1: "message 1"), ], - ) - .await; + ); { let timeline_queue = room.timeline_queue(); @@ -436,8 +413,8 @@ mod tests { } } - #[async_test] - async fn test_timeline_queue_update_with_empty_timeline_and_with_limited() { + #[test] + fn test_timeline_queue_update_with_empty_timeline_and_with_limited() { let mut room = new_room_with_timeline( room_id!("!foo:bar.org"), room_response!({}), @@ -445,8 +422,7 @@ mod tests { timeline_event!(from "@alice:baz.org" with id "$x0:baz.org" at 0: "message 0"), timeline_event!(from "@alice:baz.org" with id "$x1:baz.org" at 1: "message 1"), ], - ) - .await; + ); { let timeline_queue = room.timeline_queue(); @@ -477,8 +453,8 @@ mod tests { } } - #[async_test] - async fn test_timeline_queue_update_from_preloaded() { + #[test] + fn test_timeline_queue_update_from_preloaded() { let mut room = new_room_with_timeline( room_id!("!foo:bar.org"), room_response!({}), @@ -486,8 +462,7 @@ mod tests { timeline_event!(from "@alice:baz.org" with id "$x0:baz.org" at 0: "message 0"), timeline_event!(from "@alice:baz.org" with id "$x1:baz.org" at 1: "message 1"), ], - ) - .await; + ); room.set_state(SlidingSyncRoomState::Preloaded); @@ -527,8 +502,8 @@ mod tests { } } - #[async_test] - async fn test_timeline_queue_update_from_not_loaded() { + #[test] + fn test_timeline_queue_update_from_not_loaded() { let mut room = new_room_with_timeline( room_id!("!foo:bar.org"), room_response!({}), @@ -536,8 +511,7 @@ mod tests { timeline_event!(from "@alice:baz.org" with id "$x0:baz.org" at 0: "message 0"), timeline_event!(from "@alice:baz.org" with id "$x1:baz.org" at 1: "message 1"), ], - ) - .await; + ); { let timeline_queue = room.timeline_queue(); @@ -577,8 +551,8 @@ mod tests { } } - #[async_test] - async fn test_timeline_queue_update_from_not_loaded_with_limited() { + #[test] + fn test_timeline_queue_update_from_not_loaded_with_limited() { let mut room = new_room_with_timeline( room_id!("!foo:bar.org"), room_response!({}), @@ -586,8 +560,7 @@ mod tests { timeline_event!(from "@alice:baz.org" with id "$x0:baz.org" at 0: "message 0"), timeline_event!(from "@alice:baz.org" with id "$x1:baz.org" at 1: "message 1"), ], - ) - .await; + ); { let timeline_queue = room.timeline_queue(); @@ -678,8 +651,8 @@ mod tests { assert_eq!(deserialized.room_id, frozen_room.room_id); } - #[async_test] - async fn test_frozen_sliding_sync_room_has_a_capped_version_of_the_timeline() { + #[test] + fn test_frozen_sliding_sync_room_has_a_capped_version_of_the_timeline() { // Just below the limit. { let max = NUMBER_OF_TIMELINE_EVENTS_TO_KEEP_FOR_THE_CACHE - 1; @@ -705,8 +678,7 @@ mod tests { room_id!("!foo:bar.org"), room_response!({}), timeline_events, - ) - .await; + ); let frozen_room = FrozenSlidingSyncRoom::from(&room); assert_eq!(frozen_room.timeline_queue.len(), max + 1); @@ -743,8 +715,7 @@ mod tests { room_id!("!foo:bar.org"), room_response!({}), timeline_events, - ) - .await; + ); let frozen_room = FrozenSlidingSyncRoom::from(&room); assert_eq!( diff --git a/crates/matrix-sdk/src/sliding_sync/utils.rs b/crates/matrix-sdk/src/sliding_sync/utils.rs index 98d2619b635..6c699a16aa7 100644 --- a/crates/matrix-sdk/src/sliding_sync/utils.rs +++ b/crates/matrix-sdk/src/sliding_sync/utils.rs @@ -1,5 +1,7 @@ //! Moaaar features for Sliding Sync. +#![cfg(feature = "e2e-encryption")] + use std::{ future::Future, pin::Pin, @@ -23,7 +25,7 @@ impl Drop for AbortOnDrop { } } -impl Future for AbortOnDrop { +impl Future for AbortOnDrop { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { From df4b69666c146cf3de5d30ccf1cc8a8a48dcf7d4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Jan 2025 15:55:52 +0100 Subject: [PATCH 896/979] chore: Make Clippy and `wasm-pack` happy. --- crates/matrix-sdk-base/src/sliding_sync/mod.rs | 2 +- .../matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index c9db56c8eb1..29024555cab 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -893,7 +893,7 @@ fn process_room_properties( } } -#[cfg(test)] +#[cfg(all(test, not(target_family = "wasm")))] mod tests { use std::collections::{BTreeMap, HashSet}; #[cfg(feature = "e2e-encryption")] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs index 75a11647970..f2fdc800df7 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs @@ -20,8 +20,8 @@ use assert_matches2::assert_let; use eyeball_im::{Vector, VectorDiff}; use futures_util::{pin_mut, FutureExt, Stream, StreamExt}; use matrix_sdk::{ - test_utils::{logged_in_client, logged_in_client_with_server}, - Client, SlidingSync, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode, UpdateSummary, + test_utils::logged_in_client_with_server, Client, SlidingSync, SlidingSyncList, + SlidingSyncListBuilder, SlidingSyncMode, UpdateSummary, }; use matrix_sdk_test::{async_test, mocks::mock_encryption_state}; use matrix_sdk_ui::{ From d6c64027f66909f65be816d458a00d13fc14de56 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 8 Jan 2025 21:30:10 +0100 Subject: [PATCH 897/979] refactor(sdk): Un-cfg Client::rooms_stream It compiles on WASM too. --- crates/matrix-sdk/src/client/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 22825062d60..033bb2ac69f 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -23,13 +23,9 @@ use std::{ }; use eyeball::{SharedObservable, Subscriber}; -#[cfg(not(target_arch = "wasm32"))] -use eyeball_im::VectorDiff; +use eyeball_im::{Vector, VectorDiff}; use futures_core::Stream; -#[cfg(not(target_arch = "wasm32"))] use futures_util::StreamExt; -#[cfg(not(target_arch = "wasm32"))] -use imbl::Vector; #[cfg(feature = "e2e-encryption")] use matrix_sdk_base::crypto::store::LockableCryptoStore; use matrix_sdk_base::{ @@ -1098,7 +1094,6 @@ impl Client { } /// Get a stream of all the rooms, in addition to the existing rooms. - #[cfg(not(target_arch = "wasm32"))] pub fn rooms_stream(&self) -> (Vector, impl Stream>> + '_) { let (rooms, stream) = self.base_client().rooms_stream(); From eac5a5eb35ac1d763b38b2bd459b61cd188032f9 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 8 Jan 2025 21:20:21 +0100 Subject: [PATCH 898/979] refactor(ui): Fix unused import on wasm --- crates/matrix-sdk-ui/src/timeline/futures.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/futures.rs b/crates/matrix-sdk-ui/src/timeline/futures.rs index 8109e0cb8c9..bba9799a60a 100644 --- a/crates/matrix-sdk-ui/src/timeline/futures.rs +++ b/crates/matrix-sdk-ui/src/timeline/futures.rs @@ -1,6 +1,6 @@ use std::future::IntoFuture; -use eyeball::{SharedObservable, Subscriber}; +use eyeball::SharedObservable; use matrix_sdk::{attachment::AttachmentConfig, TransmissionProgress}; use matrix_sdk_base::boxed_into_future; use mime::Mime; @@ -48,10 +48,9 @@ impl<'a> SendAttachment<'a> { Self { use_send_queue: true, ..self } } - /// Get a subscriber to observe the progress of sending the request - /// body. + /// Get a subscriber to observe the progress of sending the request body. #[cfg(not(target_arch = "wasm32"))] - pub fn subscribe_to_send_progress(&self) -> Subscriber { + pub fn subscribe_to_send_progress(&self) -> eyeball::Subscriber { self.send_progress.subscribe() } } From b83786522685495dca1d975e761a17b5a5d5634e Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 8 Jan 2025 21:20:49 +0100 Subject: [PATCH 899/979] refactor(ui): Inherit Send / Sync bounds on RoomDataProvider from super traits --- crates/matrix-sdk-ui/src/timeline/traits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index d4ad9bdf288..ac6e3aa02b1 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -71,7 +71,7 @@ impl RoomExt for Room { } pub(super) trait RoomDataProvider: - Clone + Send + Sync + 'static + PaginableRoom + PinnedEventsRoom + Clone + PaginableRoom + PinnedEventsRoom + 'static { fn own_user_id(&self) -> &UserId; fn room_version(&self) -> RoomVersionId; From 2a954e3ce3e2bf7e5d9eaa4e05096526e67cccb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 9 Jan 2025 12:10:33 +0100 Subject: [PATCH 900/979] fix(base): Correctly name the LeftRoomUpdate in its debug implementation (#4487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Damir Jelić Co-authored-by: Benjamin Bouvier --- crates/matrix-sdk-base/src/sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index 92c87cf201e..bc2b4751375 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -215,7 +215,7 @@ impl LeftRoomUpdate { #[cfg(not(tarpaulin_include))] impl fmt::Debug for LeftRoomUpdate { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("JoinedRoom") + f.debug_struct("LeftRoomUpdate") .field("timeline", &self.timeline) .field("state", &DebugListOfRawEvents(&self.state)) .field("account_data", &DebugListOfRawEventsNoId(&self.account_data)) From ddf4d575b72e92463c81017a53ad5b4665c115c8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Jan 2025 19:21:35 +0100 Subject: [PATCH 901/979] fix(sdk): Ensure a gap has been inserted before removing it. This patch fixes a bug where the code assumes a gap has been inserted, and thus, is always present. But this isn't the case. If `prev_batch` is `None`, a gap is not inserted, and so we cannot remove it. This patch checks that `prev_batch` is `Some(_)`, which means the invariant is correct, and the code can remove the gap. --- crates/matrix-sdk/src/event_cache/room/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 7ad4a6afe2b..103d6863c44 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -431,7 +431,7 @@ impl RoomEventCacheInner { let added_unique_events = room_events.push_events(sync_timeline_events.clone()); - if !added_unique_events { + if !added_unique_events && prev_batch.is_some() { debug!( "not storing previous batch token, because we deduplicated all new sync events" ); From b926c4287a96877852115f89b1d04adaad95d0fa Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 11:22:56 +0100 Subject: [PATCH 902/979] refactor(event cache): use a more fine-grained check for the gap removal --- crates/matrix-sdk/src/event_cache/room/mod.rs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 103d6863c44..fca7f9b0f14 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -431,19 +431,30 @@ impl RoomEventCacheInner { let added_unique_events = room_events.push_events(sync_timeline_events.clone()); - if !added_unique_events && prev_batch.is_some() { + if !added_unique_events { debug!( "not storing previous batch token, because we deduplicated all new sync events" ); - // Remove the gap we just inserted. - let prev_gap_id = room_events - .rchunks() - .find_map(|c| c.is_gap().then_some(c.identifier())) - .expect("we just inserted the gap beforehand"); - room_events - .replace_gap_at([], prev_gap_id) - .expect("we obtained the valid position beforehand"); + if let Some(prev_token) = &prev_batch { + // Note: there can't be any race with another task touching the linked + // chunk at this point, because we're using `with_events_mut` which + // guards access to the data. + trace!("removing gap we just inserted"); + + // Find the gap that had the previous-batch token we inserted above. + let prev_gap_id = room_events + .rchunks() + .find_map(|c| { + let gap = as_variant::as_variant!(c.content(), ChunkContent::Gap)?; + (gap.prev_token == *prev_token).then_some(c.identifier()) + }) + .expect("we just inserted the gap beforehand"); + + room_events + .replace_gap_at([], prev_gap_id) + .expect("we obtained the valid position beforehand"); + } } }) .await?; From 9e97ed3134fdbad7721b574ffec18f1a9d4ef623 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 11:28:50 +0100 Subject: [PATCH 903/979] test(event cache): add a regression test for not deleting a gap that wasn't inserted --- .../tests/integration/event_cache.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 08189f34404..602c3732838 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1378,3 +1378,67 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { assert!(stream.is_empty()); } + +#[async_test] +async fn test_dont_delete_gap_that_wasnt_inserted() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + let room_id = room_id!("!omelette:fromage.fr"); + + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + + // Start with a room with a single event, limited timeline and prev-batch token. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("sup").event_id(event_id!("$3")).into_raw_sync()) + .set_timeline_limited() + .set_timeline_prev_batch("prev-batch".to_owned()), + ) + .await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); + if events.is_empty() { + assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = stream.recv()); + } + drop(events); + + // Back-paginate to consume the existing gap. + // Say the back-pagination doesn't return anything. + server + .mock_room_messages() + .from("prev-batch") + .ok("start-token-unused".to_owned(), None, Vec::>::new(), Vec::new()) + .mock_once() + .mount() + .await; + room_event_cache.pagination().run_backwards(20, once).await.unwrap(); + + // This doesn't cause an update, because nothing changed. + assert!(stream.is_empty()); + + // After a restart, a sync with the same sliding sync window may return the same + // events, but no prev-batch token this time. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_bulk(vec![f + .text_msg("sup") + .event_id(event_id!("$3")) + .into_raw_sync()]), + ) + .await; + + // This doesn't cause an update, because nothing changed. + assert!(stream.is_empty()); +} From fb54e869e9bf7f372ef6f48e4731c7bae8703bee Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 7 Jan 2025 17:32:05 +0100 Subject: [PATCH 904/979] chore(event cache): add more logs when the event cache tasks are shutting down --- crates/matrix-sdk/src/event_cache/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 6fbbbb7deaf..d41c54d88bd 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -248,11 +248,12 @@ impl EventCache { async move { while ignore_user_list_stream.next().await.is_some() { - info!("received an ignore user list change"); + info!("Received an ignore user list change"); if let Err(err) = inner.clear_all_rooms().await { - error!("error when clearing room storage: {err}"); + error!("when clearing room storage after ignore user list change: {err}"); } } + info!("Ignore user list stream has closed"); } .instrument(span) .await; @@ -271,6 +272,7 @@ impl EventCache { match err { EventCacheError::ClientDropped => { // The client has dropped, exit the listen task. + info!("Closing the event cache global listen task because client dropped"); break; } err => { @@ -286,12 +288,13 @@ impl EventCache { // TODO: implement Smart Matching™, warn!(num_skipped, "Lagged behind room updates, clearing all rooms"); if let Err(err) = inner.clear_all_rooms().await { - error!("error when clearing storage: {err}"); + error!("when clearing storage after lag in listen_task: {err}"); } } Err(RecvError::Closed) => { // The sender has shut down, exit. + info!("Closing the event cache global listen task because receiver closed"); break; } } From 0915eeed51b413a1924423d463a6d3731693653b Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 7 Jan 2025 17:35:17 +0100 Subject: [PATCH 905/979] chore(event cache): simplify and add logs to `RoomEventCacheState::propagate_changes` --- crates/matrix-sdk/src/event_cache/room/mod.rs | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index fca7f9b0f14..aa89c6ccd70 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -544,7 +544,7 @@ mod private { }; use once_cell::sync::OnceCell; use ruma::{serde::Raw, OwnedRoomId, RoomId}; - use tracing::{error, trace}; + use tracing::{error, instrument, trace}; use super::{chunk_debug_string, events::RoomEvents}; use crate::event_cache::EventCacheError; @@ -670,53 +670,58 @@ mod private { } /// Propagate changes to the underlying storage. + #[instrument(skip_all)] async fn propagate_changes(&mut self) -> Result<(), EventCacheError> { let mut updates = self.events.updates().take(); - if !updates.is_empty() { - if let Some(store) = self.store.get() { - // Strip relations from the `PushItems` updates. - for up in updates.iter_mut() { - match up { - Update::PushItems { items, .. } => { - Self::strip_relations_from_events(items) - } - // Other update kinds don't involve adding new events. - Update::NewItemsChunk { .. } - | Update::NewGapChunk { .. } - | Update::RemoveChunk(_) - | Update::RemoveItem { .. } - | Update::DetachLastItems { .. } - | Update::StartReattachItems - | Update::EndReattachItems - | Update::Clear => {} - } - } + if updates.is_empty() { + return Ok(()); + } - // Spawn a task to make sure that all the changes are effectively forwarded to - // the store, even if the call to this method gets aborted. - // - // The store cross-process locking involves an actual mutex, which ensures that - // storing updates happens in the expected order. + let Some(store) = self.store.get() else { + return Ok(()); + }; + + trace!("propagating {} updates", updates.len()); + + // Strip relations from the `PushItems` updates. + for up in updates.iter_mut() { + match up { + Update::PushItems { items, .. } => Self::strip_relations_from_events(items), + // Other update kinds don't involve adding new events. + Update::NewItemsChunk { .. } + | Update::NewGapChunk { .. } + | Update::RemoveChunk(_) + | Update::RemoveItem { .. } + | Update::DetachLastItems { .. } + | Update::StartReattachItems + | Update::EndReattachItems + | Update::Clear => {} + } + } - let store = store.clone(); - let room_id = self.room.clone(); + // Spawn a task to make sure that all the changes are effectively forwarded to + // the store, even if the call to this method gets aborted. + // + // The store cross-process locking involves an actual mutex, which ensures that + // storing updates happens in the expected order. - matrix_sdk_common::executor::spawn(async move { - let locked = store.lock().await?; + let store = store.clone(); + let room_id = self.room.clone(); - if let Err(err) = - locked.handle_linked_chunk_updates(&room_id, updates).await - { - error!("unable to handle linked chunk updates: {err}"); - } + matrix_sdk_common::executor::spawn(async move { + let locked = store.lock().await?; - super::Result::Ok(()) - }) - .await - .expect("joining failed")?; + if let Err(err) = locked.handle_linked_chunk_updates(&room_id, updates).await { + error!("unable to handle linked chunk updates: {err}"); } - } + + super::Result::Ok(()) + }) + .await + .expect("joining failed")?; + + trace!("done propagating store changes"); Ok(()) } From 8e0ee47637c2eb0a94f1e9132b640f2f007524eb Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 7 Jan 2025 17:49:42 +0100 Subject: [PATCH 906/979] refactor(event cache): eliminate intermediate function `append_events_locked` and replace it with an inlined call to `append_events_locked_impl`, that's then renamed `append_events_locked`. --- crates/matrix-sdk/src/event_cache/room/mod.rs | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index aa89c6ccd70..e7f95058090 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -325,7 +325,9 @@ impl RoomEventCacheInner { // so we wouldn't make use of the given previous-batch token. let prev_batch = if timeline.limited { timeline.prev_batch } else { None }; - self.append_new_events( + let mut state = self.state.write().await; + self.append_events_locked( + &mut state, timeline.events, prev_batch, ephemeral_events, @@ -366,7 +368,7 @@ impl RoomEventCacheInner { let _ = self.sender.send(RoomEventCacheUpdate::Clear); // Push the new events. - self.append_events_locked_impl( + self.append_events_locked( &mut state, sync_timeline_events, prev_batch.clone(), @@ -383,28 +385,9 @@ impl RoomEventCacheInner { /// Append a set of events to the room cache and storage, notifying /// observers. - async fn append_new_events( - &self, - sync_timeline_events: Vec, - prev_batch: Option, - ephemeral_events: Vec>, - ambiguity_changes: BTreeMap, - ) -> Result<()> { - let mut state = self.state.write().await; - self.append_events_locked_impl( - &mut state, - sync_timeline_events, - prev_batch, - ephemeral_events, - ambiguity_changes, - ) - .await - } - - /// Append a set of events and associated room data. /// /// This is a private implementation. It must not be exposed publicly. - async fn append_events_locked_impl( + async fn append_events_locked( &self, state: &mut RoomEventCacheState, sync_timeline_events: Vec, From c4bfbd0f4423a933cb24ea091746545a559aeb3a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 9 Jan 2025 18:08:44 +0200 Subject: [PATCH 907/979] feat(ffi): move tracing setup from the final client to the ffi layer (#4492) Having the final clients define the tracing filters / log levels proved to be tricky to keep in check resulting missing logs (e.g. recently introduced modules like the event cache) or unexpected behaviors (e.g. missing panics because we don't set a global filter). As such we decided to move the tracing setup and default definitions over to the rust side and let rust developers have full control over them. We will now take a general log level and optional extra targets and apply them on top of the rust side defined defaults. Targets that log more than the requested by default will remain unchanged while the others will increase their log levels to match. Certain targets like `hyper` will be ignored in this step as they're too verbose others like `matrix_sdk` because they're too generic. --- bindings/matrix-sdk-ffi/src/platform.rs | 198 +++++++++++++++++++----- bindings/matrix-sdk-ffi/src/tracing.rs | 10 ++ 2 files changed, 170 insertions(+), 38 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/platform.rs b/bindings/matrix-sdk-ffi/src/platform.rs index 009d3c501fb..95cccccdfa5 100644 --- a/bindings/matrix-sdk-ffi/src/platform.rs +++ b/bindings/matrix-sdk-ffi/src/platform.rs @@ -14,25 +14,11 @@ use tracing_subscriber::{ EnvFilter, Layer, }; -/// Add panic=error to a filter line, if it's missing from it. -/// -/// Doesn't do anything if the directive is already present. -fn add_panic_to_filter(filter: &mut String) { - if filter.split(',').all(|pair| pair.split('=').next().is_none_or(|lhs| lhs != "panic")) { - if !filter.is_empty() { - filter.push(','); - } - filter.push_str("panic=error"); - } -} +use crate::tracing::LogLevel; -pub fn log_panics(filter: &mut String) { +pub fn log_panics() { std::env::set_var("RUST_BACKTRACE", "1"); - // Make sure that panics will be properly logged. On 2025-01-08, `log_panics` - // uses the `panic` target, at the error log level. - add_panic_to_filter(filter); - log_panics::init(); } @@ -245,12 +231,75 @@ pub struct TracingFileConfiguration { max_files: Option, } +#[derive(PartialEq, PartialOrd)] +enum LogTarget { + Hyper, + MatrixSdkFfi, + MatrixSdk, + MatrixSdkClient, + MatrixSdkCrypto, + MatrixSdkCryptoAccount, + MatrixSdkOidc, + MatrixSdkHttpClient, + MatrixSdkSlidingSync, + MatrixSdkBaseSlidingSync, + MatrixSdkUiTimeline, + MatrixSdkEventCache, + MatrixSdkBaseEventCache, + MatrixSdkEventCacheStore, +} + +impl LogTarget { + fn as_str(&self) -> &'static str { + match self { + LogTarget::Hyper => "hyper", + LogTarget::MatrixSdkFfi => "matrix_sdk_ffi", + LogTarget::MatrixSdk => "matrix_sdk", + LogTarget::MatrixSdkClient => "matrix_sdk::client", + LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto", + LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account", + LogTarget::MatrixSdkOidc => "matrix_sdk::oidc", + LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client", + LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync", + LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync", + LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline", + LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache", + LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache", + LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store", + } + } +} + +const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[ + (LogTarget::Hyper, LogLevel::Warn), + (LogTarget::MatrixSdkFfi, LogLevel::Info), + (LogTarget::MatrixSdk, LogLevel::Info), + (LogTarget::MatrixSdkClient, LogLevel::Trace), + (LogTarget::MatrixSdkCrypto, LogLevel::Debug), + (LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace), + (LogTarget::MatrixSdkOidc, LogLevel::Trace), + (LogTarget::MatrixSdkHttpClient, LogLevel::Debug), + (LogTarget::MatrixSdkSlidingSync, LogLevel::Info), + (LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info), + (LogTarget::MatrixSdkUiTimeline, LogLevel::Info), + (LogTarget::MatrixSdkEventCache, LogLevel::Info), + (LogTarget::MatrixSdkBaseEventCache, LogLevel::Info), + (LogTarget::MatrixSdkEventCacheStore, LogLevel::Info), +]; + +const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[ + LogTarget::Hyper, // Too verbose + LogTarget::MatrixSdk, // Too generic +]; + #[derive(uniffi::Record)] pub struct TracingConfiguration { - /// A filter line following the [RUST_LOG format]. - /// - /// [RUST_LOG format]: https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/config_log.html - filter: String, + /// The desired log level + log_level: LogLevel, + + /// Additional targets that the FFI client would like to use e.g. + /// the target names for created [`crate::tracing::Span`] + extra_targets: Option>, /// Whether to log to stdout, or in the logcat on Android. write_to_stdout_or_system: bool, @@ -259,38 +308,111 @@ pub struct TracingConfiguration { write_to_files: Option, } +fn build_tracing_filter(config: &TracingConfiguration) -> String { + // We are intentionally not setting a global log level because we don't want to + // risk third party crates logging sensitive information. + // As such we need to make sure that panics will be properly logged. + // On 2025-01-08, `log_panics` uses the `panic` target, at the error log level. + let mut filters = vec!["panic=error".to_owned()]; + + DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| { + // Use the default if the log level shouldn't be changed for this target or + // if it's already logging more than requested + let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level { + level.as_str() + } else { + config.log_level.as_str() + }; + + filters.push(format!("{}={}", target.as_str(), level)); + }); + + // Finally append the extra targets requested by the client + if let Some(extra_targets) = &config.extra_targets { + for target in extra_targets { + filters.push(format!("{}={}", target, config.log_level.as_str())); + } + } + + filters.join(",") +} + #[matrix_sdk_ffi_macros::export] -pub fn setup_tracing(mut config: TracingConfiguration) { - log_panics(&mut config.filter); +pub fn setup_tracing(config: TracingConfiguration) { + log_panics(); tracing_subscriber::registry() - .with(EnvFilter::new(&config.filter)) + .with(EnvFilter::new(build_tracing_filter(&config))) .with(text_layers(config)) .init(); } #[cfg(test)] mod tests { - use super::add_panic_to_filter; + use super::build_tracing_filter; #[test] - fn test_add_panic_when_not_provided_empty() { - let mut filter = String::from(""); - add_panic_to_filter(&mut filter); - assert_eq!(filter, "panic=error"); - } + fn test_default_tracing_filter() { + let config = super::TracingConfiguration { + log_level: super::LogLevel::Error, + extra_targets: Some(vec!["super_duper_app".to_owned()]), + write_to_stdout_or_system: true, + write_to_files: None, + }; - #[test] - fn test_add_panic_when_not_provided_non_empty() { - let mut filter = String::from("a=b,c=d"); - add_panic_to_filter(&mut filter); - assert_eq!(filter, "a=b,c=d,panic=error"); + let filter = build_tracing_filter(&config); + + assert_eq!( + filter, + "panic=error,\ + hyper=warn,\ + matrix_sdk_ffi=info,\ + matrix_sdk=info,\ + matrix_sdk::client=trace,\ + matrix_sdk_crypto=debug,\ + matrix_sdk_crypto::olm::account=trace,\ + matrix_sdk::oidc=trace,\ + matrix_sdk::http_client=debug,\ + matrix_sdk::sliding_sync=info,\ + matrix_sdk_base::sliding_sync=info,\ + matrix_sdk_ui::timeline=info,\ + matrix_sdk::event_cache=info,\ + matrix_sdk_base::event_cache=info,\ + matrix_sdk_sqlite::event_cache_store=info,\ + super_duper_app=error" + ); } #[test] - fn test_do_nothing_when_provided() { - let mut filter = String::from("a=b,panic=info,c=d"); - add_panic_to_filter(&mut filter); - assert_eq!(filter, "a=b,panic=info,c=d"); + fn test_trace_tracing_filter() { + let config = super::TracingConfiguration { + log_level: super::LogLevel::Trace, + extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]), + write_to_stdout_or_system: true, + write_to_files: None, + }; + + let filter = build_tracing_filter(&config); + + assert_eq!( + filter, + "panic=error,\ + hyper=warn,\ + matrix_sdk_ffi=trace,\ + matrix_sdk=info,\ + matrix_sdk::client=trace,\ + matrix_sdk_crypto=trace,\ + matrix_sdk_crypto::olm::account=trace,\ + matrix_sdk::oidc=trace,\ + matrix_sdk::http_client=trace,\ + matrix_sdk::sliding_sync=trace,\ + matrix_sdk_base::sliding_sync=trace,\ + matrix_sdk_ui::timeline=trace,\ + matrix_sdk::event_cache=trace,\ + matrix_sdk_base::event_cache=trace,\ + matrix_sdk_sqlite::event_cache_store=trace,\ + super_duper_app=trace,\ + some_other_span=trace" + ); } } diff --git a/bindings/matrix-sdk-ffi/src/tracing.rs b/bindings/matrix-sdk-ffi/src/tracing.rs index cc50f14a1e3..ae7e7ba3b04 100644 --- a/bindings/matrix-sdk-ffi/src/tracing.rs +++ b/bindings/matrix-sdk-ffi/src/tracing.rs @@ -185,6 +185,16 @@ impl LogLevel { LogLevel::Trace => tracing::Level::TRACE, } } + + pub(crate) fn as_str(&self) -> &'static str { + match self { + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + } + } } #[derive(PartialEq, Eq, PartialOrd, Ord)] From 6e0f258a3963ab2d8bdce8c76a57d8dc84f4f4c1 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 16:01:02 +0100 Subject: [PATCH 908/979] chore(sdk): move send_queue.rs to send_queue/mod.rs --- crates/matrix-sdk/src/{send_queue.rs => send_queue/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/matrix-sdk/src/{send_queue.rs => send_queue/mod.rs} (100%) diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue/mod.rs similarity index 100% rename from crates/matrix-sdk/src/send_queue.rs rename to crates/matrix-sdk/src/send_queue/mod.rs From 5f5aa8117462504cd8f4e7a9a79e53770e6dab73 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 16:04:31 +0100 Subject: [PATCH 909/979] chore(ui): move date and timestamp functionality to `timeline/date_dividers.rs` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `util.rs` files are… not the best thing. These types and functions were only used by the date dividers file, so let's move them there. --- .../src/timeline/date_dividers.rs | 30 +++++++++++++++++-- crates/matrix-sdk-ui/src/timeline/util.rs | 29 +----------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs index 09f903b7b86..55ac269433c 100644 --- a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs @@ -17,15 +17,41 @@ use std::{fmt::Display, sync::Arc}; +use chrono::{Datelike, Local, TimeZone}; use ruma::MilliSecondsSinceUnixEpoch; use tracing::{error, event_enabled, instrument, trace, warn, Level}; use super::{ controller::{ObservableItemsTransaction, TimelineMetadata}, - util::timestamp_to_date, DateDividerMode, TimelineItem, TimelineItemKind, VirtualTimelineItem, }; +#[derive(Debug, PartialEq)] +struct Date { + year: i32, + month: u32, + day: u32, +} + +impl Date { + fn is_same_month_as(&self, date: Date) -> bool { + self.year == date.year && self.month == date.month + } +} + +/// Converts a timestamp since Unix Epoch to a year, month and day. +fn timestamp_to_date(ts: MilliSecondsSinceUnixEpoch) -> Date { + let datetime = Local + .timestamp_millis_opt(ts.0.into()) + // Only returns `None` if date is after Dec 31, 262143 BCE. + .single() + // Fallback to the current date to avoid issues with malicious + // homeservers. + .unwrap_or_else(Local::now); + + Date { year: datetime.year(), month: datetime.month(), day: datetime.day() } +} + /// Algorithm ensuring that date dividers are adjusted correctly, according to /// new items that have been inserted. pub(super) struct DateDividerAdjuster { @@ -618,8 +644,8 @@ mod tests { use super::{super::controller::ObservableItems, DateDividerAdjuster}; use crate::timeline::{ controller::TimelineMetadata, + date_dividers::timestamp_to_date, event_item::{EventTimelineItemKind, RemoteEventTimelineItem}, - util::timestamp_to_date, DateDividerMode, EventTimelineItem, TimelineItemContent, VirtualTimelineItem, }; diff --git a/crates/matrix-sdk-ui/src/timeline/util.rs b/crates/matrix-sdk-ui/src/timeline/util.rs index 2bb9c449456..47b2c21785c 100644 --- a/crates/matrix-sdk-ui/src/timeline/util.rs +++ b/crates/matrix-sdk-ui/src/timeline/util.rs @@ -14,9 +14,8 @@ use std::{ops::Deref, sync::Arc}; -use chrono::{Datelike, Local, TimeZone}; use imbl::Vector; -use ruma::{EventId, MilliSecondsSinceUnixEpoch}; +use ruma::EventId; #[cfg(doc)] use super::controller::TimelineMetadata; @@ -124,29 +123,3 @@ pub(super) enum RelativePosition { /// Event B is before (older than) event A. Before, } - -#[derive(Debug, PartialEq)] -pub(super) struct Date { - year: i32, - month: u32, - day: u32, -} - -impl Date { - pub fn is_same_month_as(&self, date: Date) -> bool { - self.year == date.year && self.month == date.month - } -} - -/// Converts a timestamp since Unix Epoch to a year, month and day. -pub(super) fn timestamp_to_date(ts: MilliSecondsSinceUnixEpoch) -> Date { - let datetime = Local - .timestamp_millis_opt(ts.0.into()) - // Only returns `None` if date is after Dec 31, 262143 BCE. - .single() - // Fallback to the current date to avoid issues with malicious - // homeservers. - .unwrap_or_else(Local::now); - - Date { year: datetime.year(), month: datetime.month(), day: datetime.day() } -} From c4a86a3d0a11692a937a36ff1cbdff06ce55c5e8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 16:18:34 +0100 Subject: [PATCH 910/979] chore(ui): move timeline/read_receipts to timeline/controller/read_receipts Read receipts only make sense in the context of the timeline controller. --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 1 + .../src/timeline/{ => controller}/read_receipts.rs | 10 +++------- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 4 ++-- crates/matrix-sdk-ui/src/timeline/mod.rs | 1 - 4 files changed, 6 insertions(+), 10 deletions(-) rename crates/matrix-sdk-ui/src/timeline/{ => controller}/read_receipts.rs (98%) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index faa4305ea7e..97f1b6df26e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -86,6 +86,7 @@ use crate::{ }; mod observable_items; +mod read_receipts; mod state; /// Data associated to the current timeline focus. diff --git a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs similarity index 98% rename from crates/matrix-sdk-ui/src/timeline/read_receipts.rs rename to crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs index 00964de20be..0444a111143 100644 --- a/crates/matrix-sdk-ui/src/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs @@ -25,14 +25,10 @@ use tokio_stream::wrappers::WatchStream; use tracing::{debug, error, warn}; use super::{ - controller::{ - AllRemoteEvents, FullEventMeta, ObservableItemsTransaction, TimelineMetadata, - TimelineState, TimelineStateTransaction, - }, - traits::RoomDataProvider, - util::{rfind_event_by_id, RelativePosition}, - TimelineItem, + rfind_event_by_id, AllRemoteEvents, FullEventMeta, ObservableItemsTransaction, + RoomDataProvider, TimelineMetadata, TimelineState, }; +use crate::timeline::{controller::TimelineStateTransaction, util::RelativePosition, TimelineItem}; /// In-memory caches for read receipts. #[derive(Clone, Debug, Default)] diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index dffda89f6ce..4fa209759fc 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -49,6 +49,7 @@ use super::{ AllRemoteEvents, ObservableItems, ObservableItemsTransaction, ObservableItemsTransactionEntry, }, + read_receipts::ReadReceipts, DateDividerMode, HandleManyEventsResult, TimelineFocusKind, TimelineSettings, }; use crate::{ @@ -62,7 +63,6 @@ use crate::{ event_item::{PollState, RemoteEventOrigin, ResponseData}, item::TimelineUniqueId, reactions::Reactions, - read_receipts::ReadReceipts, traits::RoomDataProvider, util::{rfind_event_by_id, RelativePosition}, Profile, TimelineItem, TimelineItemKind, @@ -1058,7 +1058,7 @@ pub(in crate::timeline) struct TimelineMetadata { /// Read receipts related state. /// /// TODO: move this over to the event cache (see also #3058). - pub read_receipts: ReadReceipts, + pub(in crate::timeline::controller) read_receipts: ReadReceipts, } /// Maximum number of stash pending edits. diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index df2ce390a79..e8545d3f9ea 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -69,7 +69,6 @@ mod item; mod pagination; mod pinned_events_loader; mod reactions; -mod read_receipts; #[cfg(test)] mod tests; mod to_device; From 692aceba5099b73e2d36155cf649c89d806a0254 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 16:20:37 +0100 Subject: [PATCH 911/979] chore(ui): move `RelativePosition` in timeline/controller --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 13 ++++++++++++- .../src/timeline/controller/read_receipts.rs | 4 ++-- .../matrix-sdk-ui/src/timeline/controller/state.rs | 6 +++--- crates/matrix-sdk-ui/src/timeline/util.rs | 11 ----------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 97f1b6df26e..9ecb2ded659 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -68,7 +68,7 @@ use super::{ event_item::{ReactionStatus, RemoteEventOrigin}, item::TimelineUniqueId, traits::{Decryptor, RoomDataProvider}, - util::{rfind_event_by_id, rfind_event_item, RelativePosition}, + util::{rfind_event_by_id, rfind_event_item}, DateDividerMode, Error, EventSendState, EventTimelineItem, InReplyToDetails, Message, PaginationError, Profile, ReactionInfo, RepliedToEvent, TimelineDetails, TimelineEventItemId, TimelineFocus, TimelineItem, TimelineItemContent, TimelineItemKind, @@ -1649,3 +1649,14 @@ async fn fetch_replied_to_event( }; Ok(res) } + +/// Result of comparing events position in the timeline. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::timeline) enum RelativePosition { + /// Event B is after (more recent than) event A. + After, + /// They are the same event. + Same, + /// Event B is before (older than) event A. + Before, +} diff --git a/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs b/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs index 0444a111143..6c3b232f562 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs @@ -26,9 +26,9 @@ use tracing::{debug, error, warn}; use super::{ rfind_event_by_id, AllRemoteEvents, FullEventMeta, ObservableItemsTransaction, - RoomDataProvider, TimelineMetadata, TimelineState, + RelativePosition, RoomDataProvider, TimelineMetadata, TimelineState, }; -use crate::timeline::{controller::TimelineStateTransaction, util::RelativePosition, TimelineItem}; +use crate::timeline::{controller::TimelineStateTransaction, TimelineItem}; /// In-memory caches for read receipts. #[derive(Clone, Debug, Default)] diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 4fa209759fc..b920ef8c347 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -50,7 +50,7 @@ use super::{ ObservableItemsTransactionEntry, }, read_receipts::ReadReceipts, - DateDividerMode, HandleManyEventsResult, TimelineFocusKind, TimelineSettings, + DateDividerMode, HandleManyEventsResult, RelativePosition, TimelineFocusKind, TimelineSettings, }; use crate::{ events::SyncTimelineEventWithoutContent, @@ -64,7 +64,7 @@ use crate::{ item::TimelineUniqueId, reactions::Reactions, traits::RoomDataProvider, - util::{rfind_event_by_id, RelativePosition}, + util::rfind_event_by_id, Profile, TimelineItem, TimelineItemKind, }, unable_to_decrypt_hook::UtdHookManager, @@ -1110,7 +1110,7 @@ impl TimelineMetadata { /// known. /// /// Returns `None` if none of the two events could be found in the timeline. - pub fn compare_events_positions( + pub(in crate::timeline) fn compare_events_positions( &self, event_a: &EventId, event_b: &EventId, diff --git a/crates/matrix-sdk-ui/src/timeline/util.rs b/crates/matrix-sdk-ui/src/timeline/util.rs index 47b2c21785c..edf3464bee5 100644 --- a/crates/matrix-sdk-ui/src/timeline/util.rs +++ b/crates/matrix-sdk-ui/src/timeline/util.rs @@ -112,14 +112,3 @@ pub(super) fn rfind_event_by_item_id<'a>( TimelineEventItemId::EventId(event_id) => rfind_event_by_id(items, event_id), } } - -/// Result of comparing events position in the timeline. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum RelativePosition { - /// Event B is after (more recent than) event A. - After, - /// They are the same event. - Same, - /// Event B is before (older than) event A. - Before, -} From 0cae54cc3f01c2c2daec73f02d40e6cdbd3c8cff Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 16:23:31 +0100 Subject: [PATCH 912/979] chore(ui): rename "utils" to "algorithms" It only contains functions used to search items in the timeline now \o/ --- .../matrix-sdk-ui/src/timeline/{util.rs => algorithms.rs} | 0 crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 4 ++-- crates/matrix-sdk-ui/src/timeline/controller/state.rs | 2 +- crates/matrix-sdk-ui/src/timeline/event_handler.rs | 2 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 8 +++++--- crates/matrix-sdk-ui/src/timeline/tests/mod.rs | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) rename crates/matrix-sdk-ui/src/timeline/{util.rs => algorithms.rs} (100%) diff --git a/crates/matrix-sdk-ui/src/timeline/util.rs b/crates/matrix-sdk-ui/src/timeline/algorithms.rs similarity index 100% rename from crates/matrix-sdk-ui/src/timeline/util.rs rename to crates/matrix-sdk-ui/src/timeline/algorithms.rs diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 9ecb2ded659..066aecab3c9 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -64,22 +64,22 @@ pub(super) use self::{ }, }; use super::{ + algorithms::{rfind_event_by_id, rfind_event_item}, event_handler::TimelineEventKind, event_item::{ReactionStatus, RemoteEventOrigin}, item::TimelineUniqueId, traits::{Decryptor, RoomDataProvider}, - util::{rfind_event_by_id, rfind_event_item}, DateDividerMode, Error, EventSendState, EventTimelineItem, InReplyToDetails, Message, PaginationError, Profile, ReactionInfo, RepliedToEvent, TimelineDetails, TimelineEventItemId, TimelineFocus, TimelineItem, TimelineItemContent, TimelineItemKind, }; use crate::{ timeline::{ + algorithms::rfind_event_by_item_id, date_dividers::DateDividerAdjuster, event_item::EventTimelineItemKind, pinned_events_loader::{PinnedEventsLoader, PinnedEventsLoaderError}, reactions::FullReactionKey, - util::rfind_event_by_item_id, TimelineEventFilterFn, }, unable_to_decrypt_hook::UtdHookManager, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index b920ef8c347..2f91777777c 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -55,6 +55,7 @@ use super::{ use crate::{ events::SyncTimelineEventWithoutContent, timeline::{ + algorithms::rfind_event_by_id, date_dividers::DateDividerAdjuster, event_handler::{ Flow, HandleEventResult, TimelineEventContext, TimelineEventHandler, TimelineEventKind, @@ -64,7 +65,6 @@ use crate::{ item::TimelineUniqueId, reactions::Reactions, traits::RoomDataProvider, - util::rfind_event_by_id, Profile, TimelineItem, TimelineItemKind, }, unable_to_decrypt_hook::UtdHookManager, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index a1262a78a2c..8cdcf1256ee 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -51,6 +51,7 @@ use ruma::{ use tracing::{debug, error, field::debug, info, instrument, trace, warn}; use super::{ + algorithms::{rfind_event_by_id, rfind_event_item}, controller::{ ObservableItemsTransaction, ObservableItemsTransactionEntry, PendingEdit, PendingEditKind, TimelineMetadata, TimelineStateTransaction, @@ -64,7 +65,6 @@ use super::{ }, reactions::{FullReactionKey, PendingReaction}, traits::RoomDataProvider, - util::{rfind_event_by_id, rfind_event_item}, EventTimelineItem, InReplyToDetails, OtherState, RepliedToEvent, Sticker, TimelineDetails, TimelineItem, TimelineItemContent, }; diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index e8545d3f9ea..7219925278a 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -18,6 +18,7 @@ use std::{fs, path::PathBuf, pin::Pin, sync::Arc, task::Poll}; +use algorithms::rfind_event_by_item_id; use event_item::{extract_room_msg_edit_content, TimelineItemHandle}; use eyeball_im::VectorDiff; use futures_core::Stream; @@ -53,10 +54,13 @@ use ruma::{ }; use thiserror::Error; use tracing::{error, instrument, trace, warn}; -use util::rfind_event_by_item_id; +use self::{ + algorithms::rfind_event_by_id, controller::TimelineController, futures::SendAttachment, +}; use crate::timeline::pinned_events_loader::PinnedEventsRoom; +mod algorithms; mod builder; mod controller; mod date_dividers; @@ -73,7 +77,6 @@ mod reactions; mod tests; mod to_device; mod traits; -mod util; mod virtual_item; pub use self::{ @@ -93,7 +96,6 @@ pub use self::{ traits::RoomExt, virtual_item::VirtualTimelineItem, }; -use self::{controller::TimelineController, futures::SendAttachment, util::rfind_event_by_id}; /// Information needed to reply to an event. #[derive(Debug, Clone)] diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index eb6601c167d..13f974f1621 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -61,11 +61,11 @@ use ruma::{ use tokio::sync::RwLock; use super::{ + algorithms::rfind_event_by_item_id, controller::{TimelineNewItemPosition, TimelineSettings}, event_handler::TimelineEventKind, event_item::RemoteEventOrigin, traits::RoomDataProvider, - util::rfind_event_by_item_id, EventTimelineItem, Profile, TimelineController, TimelineEventItemId, TimelineFocus, TimelineItem, }; From 6c053a86bf3a6335e27fff998d9e15dd16a11599 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 9 Jan 2025 20:06:02 +0100 Subject: [PATCH 913/979] chore: Fix new nightly warnings --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 2 +- crates/matrix-sdk-common/src/linked_chunk/as_vector.rs | 2 +- crates/matrix-sdk/src/oidc/mod.rs | 6 ++---- examples/persist_session/src/main.rs | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 5773717714b..7a4df082f0d 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -123,7 +123,7 @@ impl Timeline { .thumbnail(thumbnail) .info(attachment_info) .caption(params.caption) - .formatted_caption(formatted_caption.map(Into::into)) + .formatted_caption(formatted_caption) .mentions(params.mentions.map(Into::into)); let handle = SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { diff --git a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs index e9bfe0c6008..c0d65af1c15 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs @@ -803,7 +803,7 @@ mod tests { assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']); // Empty updates first. - let _ = linked_chunk.updates().take(); + let _ = linked_chunk.updates().unwrap().take(); // Start observing future updates. let mut as_vector = linked_chunk.as_vector().unwrap(); diff --git a/crates/matrix-sdk/src/oidc/mod.rs b/crates/matrix-sdk/src/oidc/mod.rs index 4c20e085153..86f3e7f5295 100644 --- a/crates/matrix-sdk/src/oidc/mod.rs +++ b/crates/matrix-sdk/src/oidc/mod.rs @@ -1160,8 +1160,7 @@ impl Oidc { #[cfg(feature = "e2e-encryption")] None, ) - .await - .map_err(crate::Error::from)?; + .await?; // At this point the Olm machine has been set up. // Enable the cross-process lock for refreshes, if needs be. @@ -1313,8 +1312,7 @@ impl Oidc { refresh_token.clone(), latest_id_token.clone(), ) - .await - .map_err(OidcError::from)?; + .await?; trace!( "Token refresh: new refresh_token: {} / access_token: {:x}", diff --git a/examples/persist_session/src/main.rs b/examples/persist_session/src/main.rs index 81b40545da1..b3b9a9e1cd2 100644 --- a/examples/persist_session/src/main.rs +++ b/examples/persist_session/src/main.rs @@ -77,7 +77,7 @@ async fn main() -> anyhow::Result<()> { (login(&data_dir, &session_file).await?, None) }; - sync(client, sync_token, &session_file).await.map_err(Into::into) + sync(client, sync_token, &session_file).await } /// Restore a previous session. From ff5dcbf631dd988c223299b5eb5a327f0bb131b0 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 9 Jan 2025 20:59:31 +0100 Subject: [PATCH 914/979] refactor(common): Warn if LinkedChunk::updates() return value is not used --- crates/matrix-sdk-common/src/linked_chunk/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index b8f14589ad4..113edbdc8c4 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -897,6 +897,7 @@ impl LinkedChunk { /// It returns `None` if updates are disabled, i.e. if this linked chunk has /// been constructed with [`Self::new`], otherwise, if it's been constructed /// with [`Self::new_with_update_history`], it returns `Some(…)`. + #[must_use] pub fn updates(&mut self) -> Option<&mut ObservableUpdates> { self.updates.as_mut() } From 4043f9bf5dd8dcddd27717536ea54f1449c49212 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 9 Jan 2025 20:47:07 +0100 Subject: [PATCH 915/979] refactor(sdk): Un-cfg SendAttachment::with_send_progress_observable It (now) compiles on WASM just fine. --- crates/matrix-sdk/src/room/futures.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index 2aaf6c38b56..962a9e90970 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -271,7 +271,6 @@ impl<'a> SendAttachment<'a> { /// Replace the default `SharedObservable` used for tracking upload /// progress. - #[cfg(not(target_arch = "wasm32"))] pub fn with_send_progress_observable( mut self, send_progress: SharedObservable, From 526b5c46302d3cc5d5d49948aa545fbf4a2c6212 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 9 Jan 2025 20:45:38 +0100 Subject: [PATCH 916/979] refactor(ui): Relax some Send constraints on WASM --- crates/matrix-sdk-ui/src/timeline/controller/mod.rs | 7 +++++-- .../matrix-sdk-ui/src/timeline/pinned_events_loader.rs | 7 +++---- crates/matrix-sdk-ui/src/timeline/traits.rs | 10 ++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 066aecab3c9..64048aeda31 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -27,7 +27,7 @@ use matrix_sdk::{ send_queue::{ LocalEcho, LocalEchoContent, RoomSendQueueUpdate, SendHandle, SendReactionHandle, }, - Result, Room, + Result, Room, SendOutsideWasm, }; use ruma::{ api::client::receipt::create_receipt::v3::ReceiptType as SendReceiptType, @@ -479,7 +479,10 @@ impl TimelineController

{ pub(super) async fn subscribe( &self, - ) -> (Vector>, impl Stream>> + Send) { + ) -> ( + Vector>, + impl Stream>> + SendOutsideWasm, + ) { trace!("Creating timeline items signal"); let state = self.state.read().await; (state.items.clone_items(), state.items.subscribe().into_stream()) diff --git a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs index 1deb853607d..2fa07fd6787 100644 --- a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs +++ b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs @@ -14,7 +14,7 @@ use std::{fmt::Formatter, sync::Arc}; -use futures_util::{stream, FutureExt as _, StreamExt}; +use futures_util::{stream, StreamExt}; use matrix_sdk::{ config::RequestConfig, event_cache::paginator::PaginatorError, BoxFuture, Room, SendOutsideWasm, SyncOutsideWasm, @@ -151,7 +151,7 @@ impl PinnedEventsRoom for Room { request_config: Option, related_event_filters: Option>, ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>> { - async move { + Box::pin(async move { if let Ok((cache, _handles)) = self.event_cache().await { if let Some(ret) = cache.event_with_relations(event_id, related_event_filters).await { @@ -165,8 +165,7 @@ impl PinnedEventsRoom for Room { .await .map(|e| (e.into(), Vec::new())) .map_err(|err| PaginatorError::SdkError(Box::new(err))) - } - .boxed() + }) } fn pinned_event_ids(&self) -> Option> { diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index ac6e3aa02b1..b865ff8e33f 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -21,7 +21,8 @@ use indexmap::IndexMap; use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement}; use matrix_sdk::{ crypto::types::events::CryptoContextInfo, deserialized_responses::TimelineEvent, - event_cache::paginator::PaginableRoom, BoxFuture, Result, Room, + event_cache::paginator::PaginableRoom, AsyncTraitDeps, BoxFuture, Result, Room, + SendOutsideWasm, }; use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo}; use ruma::{ @@ -47,7 +48,8 @@ pub trait RoomExt { /// independent events. /// /// This is the same as using `room.timeline_builder().build()`. - fn timeline(&self) -> impl Future> + Send; + fn timeline(&self) + -> impl Future> + SendOutsideWasm; /// Get a [`TimelineBuilder`] for this room. /// @@ -289,11 +291,11 @@ impl RoomDataProvider for Room { // Internal helper to make most of retry_event_decryption independent of a room // object, which is annoying to create for testing and not really needed -pub(super) trait Decryptor: Clone + Send + Sync + 'static { +pub(super) trait Decryptor: AsyncTraitDeps + Clone + 'static { fn decrypt_event_impl( &self, raw: &Raw, - ) -> impl Future> + Send; + ) -> impl Future> + SendOutsideWasm; } impl Decryptor for Room { From 7466f77eaea38c635a1ed06f5c486b46df9e0d5f Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 9 Jan 2025 20:48:51 +0100 Subject: [PATCH 917/979] refactor(ui): Replace tokio::spawn with matrix_sdk::executor::spawn --- crates/matrix-sdk-ui/src/sync_service.rs | 14 +++++++------- crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs | 8 +++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk-ui/src/sync_service.rs b/crates/matrix-sdk-ui/src/sync_service.rs index 7ff37bfbfed..752d3eb6fbf 100644 --- a/crates/matrix-sdk-ui/src/sync_service.rs +++ b/crates/matrix-sdk-ui/src/sync_service.rs @@ -28,14 +28,14 @@ use std::sync::{Arc, Mutex}; use eyeball::{SharedObservable, Subscriber}; use futures_core::Future; use futures_util::{pin_mut, StreamExt as _}; -use matrix_sdk::Client; +use matrix_sdk::{ + executor::{spawn, JoinHandle}, + Client, +}; use thiserror::Error; -use tokio::{ - sync::{ - mpsc::{Receiver, Sender}, - Mutex as AsyncMutex, OwnedMutexGuard, - }, - task::{spawn, JoinHandle}, +use tokio::sync::{ + mpsc::{Receiver, Sender}, + Mutex as AsyncMutex, OwnedMutexGuard, }; use tracing::{error, info, instrument, trace, warn, Instrument, Level}; diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index ce4c66c51db..9cb4b3fdb41 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -24,16 +24,18 @@ use std::{ }; use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; -use matrix_sdk::{crypto::types::events::UtdCause, Client}; +use matrix_sdk::{ + crypto::types::events::UtdCause, + executor::{spawn, JoinHandle}, + Client, +}; use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError}; use ruma::{ time::{Duration, Instant}, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedServerName, UserId, }; use tokio::{ - spawn, sync::{Mutex as AsyncMutex, MutexGuard}, - task::JoinHandle, time::sleep, }; use tracing::error; From 7ec384c61adba9dd51b61c1c0518a313ae646bec Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 8 Jan 2025 15:10:11 +0000 Subject: [PATCH 918/979] fix: Fix incorrect debug_struct calls in several places --- crates/matrix-sdk-base/src/client.rs | 2 +- crates/matrix-sdk-base/src/sync.rs | 4 ++-- .../matrix-sdk-crypto/src/types/events/room_key_withheld.rs | 2 +- crates/matrix-sdk/src/oidc/mod.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 6456d4c6c90..7c66f271f01 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -129,7 +129,7 @@ pub struct BaseClient { #[cfg(not(tarpaulin_include))] impl fmt::Debug for BaseClient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Client") + f.debug_struct("BaseClient") .field("session_meta", &self.store.session_meta()) .field("sync_token", &self.store.sync_token) .finish_non_exhaustive() diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index bc2b4751375..824becb54b2 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -102,7 +102,7 @@ impl RoomUpdates { #[cfg(not(tarpaulin_include))] impl fmt::Debug for RoomUpdates { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Rooms") + f.debug_struct("RoomUpdates") .field("leave", &self.leave) .field("join", &self.join) .field("invite", &DebugInvitedRoomUpdates(&self.invite)) @@ -138,7 +138,7 @@ pub struct JoinedRoomUpdate { #[cfg(not(tarpaulin_include))] impl fmt::Debug for JoinedRoomUpdate { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("JoinedRoom") + f.debug_struct("JoinedRoomUpdate") .field("unread_notifications", &self.unread_notifications) .field("timeline", &self.timeline) .field("state", &DebugListOfRawEvents(&self.state)) diff --git a/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs b/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs index 61b1bd91d5c..daef8acdac2 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs @@ -264,7 +264,7 @@ pub struct NoOlmWithheldContent { #[cfg(not(tarpaulin_include))] impl std::fmt::Debug for CommonWithheldCodeContent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AnyWithheldContent") + f.debug_struct("CommonWithheldCodeContent") .field("room_id", &self.room_id) .field("session_id", &self.session_id) .field("sender_key", &self.sender_key) diff --git a/crates/matrix-sdk/src/oidc/mod.rs b/crates/matrix-sdk/src/oidc/mod.rs index 86f3e7f5295..5fb525d75a1 100644 --- a/crates/matrix-sdk/src/oidc/mod.rs +++ b/crates/matrix-sdk/src/oidc/mod.rs @@ -1585,7 +1585,7 @@ pub struct OidcSessionTokens { #[cfg(not(tarpaulin_include))] impl fmt::Debug for OidcSessionTokens { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SessionTokens").finish_non_exhaustive() + f.debug_struct("OidcSessionTokens").finish_non_exhaustive() } } From cb72d4375f6d735c562598db92e928dd300fd9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 10 Jan 2025 11:16:28 +0100 Subject: [PATCH 919/979] chore: Upgrade Ruma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c7e1a41a50..36fb56dec1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4786,7 +4786,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.0" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "assign", "js_int", @@ -4802,7 +4802,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.0" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "as_variant", "assign", @@ -4825,7 +4825,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.0" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "as_variant", "base64 0.22.1", @@ -4857,7 +4857,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.0" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4882,7 +4882,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.0" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "http", "js_int", @@ -4896,7 +4896,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.4.0" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "as_variant", "html5ever", @@ -4908,7 +4908,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "js_int", "thiserror 2.0.3", @@ -4917,7 +4917,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.15.0" -source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" +source = "git+https://github.com/ruma/ruma?rev=b266343136e8470a7d040efc207e16af0c20d374#b266343136e8470a7d040efc207e16af0c20d374" dependencies = [ "cfg-if", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index a80e0cc06fc..78ec2e40895 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ proptest = { version = "1.5.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "71be4a316198d6db91f512b2ceb8eb91238581f1", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "b266343136e8470a7d040efc207e16af0c20d374", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -72,7 +72,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "71be4a316198d6db91f512b2ce "unstable-msc4140", "unstable-msc4171", ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "71be4a316198d6db91f512b2ceb8eb91238581f1" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "b266343136e8470a7d040efc207e16af0c20d374" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" From e4b269e0de27e354caaaea86ec330ef9742c09ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 10 Jan 2025 09:13:13 +0100 Subject: [PATCH 920/979] fix: Implement visit_bytes for the Ed25519PublicKey deserialization This fixes the deserialization of the SenderData since it switched to the base64 encoding for serialization of the master key in one of its variants. The issue was introduced in 5ff556f6c3026ab53b21ba0b0a24749ffa715e17. --- .../src/olm/group_sessions/sender_data.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index fa79b6b6ab3..697507647f1 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -70,13 +70,31 @@ where where A: de::SeqAccess<'de>, { - let mut buf = [0u8; 32]; + let mut buf = [0u8; Ed25519PublicKey::LENGTH]; + for (i, item) in buf.iter_mut().enumerate() { *item = seq.next_element()?.ok_or_else(|| de::Error::invalid_length(i, &self))?; } + let key = Ed25519PublicKey::from_slice(&buf).map_err(|e| de::Error::custom(&e))?; + Ok(Box::new(key)) } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: de::Error, + { + if v.len() == Ed25519PublicKey::LENGTH { + let mut buf = [0u8; Ed25519PublicKey::LENGTH]; + buf.copy_from_slice(v); + + let key = Ed25519PublicKey::from_slice(&buf).map_err(|e| de::Error::custom(&e))?; + Ok(Box::new(key)) + } else { + Err(de::Error::invalid_length(v.len(), &self)) + } + } } de.deserialize_any(KeyVisitor) From 1dd2b2c9e8353f9feef9cb5e4c1f7758d0f7be90 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 10 Jan 2025 13:21:33 +0100 Subject: [PATCH 921/979] test: Test the KnownSenderData migration with optimised [u8] serialization --- crates/matrix-sdk-crypto/Cargo.toml | 1 + .../src/olm/group_sessions/sender_data.rs | 36 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 551c89a9bac..17c7c1c746b 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -87,6 +87,7 @@ http = { workspace = true } indoc = "2.0.5" insta = { workspace = true } matrix-sdk-test = { workspace = true } +rmp-serde = { workspace = true } proptest = { workspace = true } similar-asserts = { workspace = true } # required for async_test macro diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index 697507647f1..9ac3b4a5463 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -352,11 +352,11 @@ mod tests { device_id, owned_device_id, owned_user_id, user_id, DeviceKeyAlgorithm, DeviceKeyId, }; use serde_json::json; - use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; + use vodozemac::{base64_decode, Curve25519PublicKey, Ed25519PublicKey}; use super::SenderData; use crate::{ - olm::KnownSenderData, + olm::{KnownSenderData, PickledInboundGroupSession}, types::{DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures}, }; @@ -616,4 +616,36 @@ mod tests { Ed25519PublicKey::from_slice(&[0u8; 32]).unwrap().to_base64() ); } + + #[test] + fn test_sender_known_data_migration_with_efficient_bytes_array() { + // This is an serialized PickledInboundGroupSession as rmp_serde will generate. + // + // This export usse a more efficient serialization format for bytes. This was + // exported when the `KnownSenderData` master_key was serialized as an byte + // array instead of a base64 encoded string. + const SERIALIZED_B64: &str = + "iaZwaWNrbGWEr2luaXRpYWxfcmF0Y2hldIKlaW5uZXLcAIABYMzfSnBRzMlPKF1uKjYbzLtkzNJ4RcylzN0HzP\ + 9DzON1Tm05zO7M2MzFQsy9Acz9zPnMqDvM4syQzNrMzxF5KzbM4sy9zPUbBWfM7m4/zJzM18zDzMESKgfMkE7M\ + yszIHszqWjYyQURbzKTMkx7M58zANsy+AGPM2A8tbcyFYczge8ykzMFdbVxJMMyAzN8azJEXGsy8zPJazMMaP8\ + ziDszmWwfM+My2ajLMr8y+eczTRm9TFadjb3VudGVyAKtzaWduaW5nX2tlecQgefpCr6Duu7QUWzKIeMOFmxv/\ + NjfcsYwZz8IN2ZOhdaS0c2lnbmluZ19rZXlfdmVyaWZpZWTDpmNvbmZpZ4GndmVyc2lvbqJWMapzZW5kZXJfa2\ + V52StoMkIySDg2ajFpYmk2SW13ak9UUkhzbTVMamtyT2kyUGtiSXVUb0w0TWtFq3NpZ25pbmdfa2V5gadlZDI1\ + NTE52StUWHJqNS9UYXpia3Yram1CZDl4UlB4NWNVaFFzNUNnblc1Q1pNRjgvNjZzq3NlbmRlcl9kYXRhgbBTZW\ + 5kZXJVbnZlcmlmaWVkg6d1c2VyX2lks0B2YWxvdTM1Om1hdHJpeC5vcmepZGV2aWNlX2lkqkZJQlNaRlJLUE2q\ + bWFzdGVyX2tlecQgkOp9s4ClyQujYD7rRZA8xgE6kvYlqKSNnMrQNmSrcuGncm9vbV9pZL4hRWt5VEtGdkViYl\ + B6SmxhaUhFOm1hdHJpeC5vcmeoaW1wb3J0ZWTCqWJhY2tlZF91cMKyaGlzdG9yeV92aXNpYmlsaXR5wKlhbGdv\ + cml0aG20bS5tZWdvbG0udjEuYWVzLXNoYTI"; + + let input = base64_decode(SERIALIZED_B64).unwrap(); + let sender_data: PickledInboundGroupSession = rmp_serde::from_slice(&input) + .expect("Should be able to deserialize serialized inbound group session"); + + assert_let!( + SenderData::SenderUnverified(KnownSenderData { master_key, .. }) = + sender_data.sender_data + ); + + assert_eq!(master_key.to_base64(), "kOp9s4ClyQujYD7rRZA8xgE6kvYlqKSNnMrQNmSrcuE"); + } } From def4bbbed2e59103aec0f1ac57d18c47e6738c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 10 Jan 2025 14:13:10 +0100 Subject: [PATCH 922/979] fix(store-encryption): Remove an unwrap that snuck in (#4506) --- crates/matrix-sdk-store-encryption/CHANGELOG.md | 7 +++++++ crates/matrix-sdk-store-encryption/src/lib.rs | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-store-encryption/CHANGELOG.md b/crates/matrix-sdk-store-encryption/CHANGELOG.md index d3fae753d53..ac64e342f3b 100644 --- a/crates/matrix-sdk-store-encryption/CHANGELOG.md +++ b/crates/matrix-sdk-store-encryption/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Bug Fixes + +- Remove the usage of an unwrap in the `StoreCipher::import_with_key` method. + This could have lead to panics if the second argument was an invalid + `StoreCipher` export. + ([#4506](https://github.com/matrix-org/matrix-rust-sdk/pull/4506)) + ## [0.9.0] - 2024-12-18 No notable changes in this release. diff --git a/crates/matrix-sdk-store-encryption/src/lib.rs b/crates/matrix-sdk-store-encryption/src/lib.rs index 78947d96af2..dea7e7e440c 100644 --- a/crates/matrix-sdk-store-encryption/src/lib.rs +++ b/crates/matrix-sdk-store-encryption/src/lib.rs @@ -334,7 +334,7 @@ impl StoreCipher { /// # anyhow::Ok(()) }; /// ``` pub fn import_with_key(key: &[u8; 32], encrypted: &[u8]) -> Result { - let encrypted: EncryptedStoreCipher = rmp_serde::from_slice(encrypted).unwrap(); + let encrypted: EncryptedStoreCipher = rmp_serde::from_slice(encrypted)?; if let KdfInfo::Pbkdf2ToChaCha20Poly1305 { .. } = encrypted.kdf_info { return Err(Error::KdfMismatch); @@ -903,6 +903,12 @@ mod tests { Ok(()) } + #[test] + fn test_importing_invalid_store_cipher_does_not_panic() { + // This used to panic, we're testing that we're getting a real error. + assert!(StoreCipher::import_with_key(&[0; 32], &[0; 64]).is_err()) + } + #[test] fn encrypting_values() -> Result<(), Error> { let event = json!({ From b3491582d09cadbd71f6751f4637ee76f09f8038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 10 Jan 2025 12:02:54 +0100 Subject: [PATCH 923/979] feat(sdk): Allow to set and check whether an image is animated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using MSC4230. Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/src/ruma.rs | 4 ++ crates/matrix-sdk/CHANGELOG.md | 5 ++ crates/matrix-sdk/Cargo.toml | 10 +++- crates/matrix-sdk/src/attachment.rs | 3 ++ .../tests/integration/room/attachment/mod.rs | 50 +++++++++++++++++++ .../tests/integration/send_queue.rs | 4 ++ 6 files changed, 75 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 89f7cc920cf..2bc395be8ff 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -570,6 +570,7 @@ pub struct ImageInfo { pub thumbnail_info: Option, pub thumbnail_source: Option>, pub blurhash: Option, + pub is_animated: Option, } impl From for RumaImageInfo { @@ -582,6 +583,7 @@ impl From for RumaImageInfo { thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new), thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()), blurhash: value.blurhash, + is_animated: value.is_animated, }) } } @@ -603,6 +605,7 @@ impl TryFrom<&ImageInfo> for BaseImageInfo { width: Some(width), size: Some(size), blurhash: Some(blurhash), + is_animated: value.is_animated, }) } } @@ -859,6 +862,7 @@ impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo { .transpose()? .map(Arc::new), blurhash: info.blurhash.clone(), + is_animated: info.is_animated, }) } } diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index c9cbe917196..7f8806bc30c 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Features + +- Allow to set and check whether an image is animated via its `ImageInfo`. + ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) + ### Refactor - [**breaking**] Move the optional `RequestConfig` argument of the diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index e7e0b3d1139..9a1b62dbb1e 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -96,7 +96,15 @@ mime2ext = "0.1.53" once_cell = { workspace = true } pin-project-lite = { workspace = true } rand = { workspace = true , optional = true } -ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965", "unstable-msc3930", "unstable-msc3245-v1-compat", "unstable-msc2867"] } +ruma = { workspace = true, features = [ + "rand", + "unstable-msc2448", + "unstable-msc2965", + "unstable-msc3930", + "unstable-msc3245-v1-compat", + "unstable-msc2867", + "unstable-msc4230", +] } serde = { workspace = true } serde_html_form = { workspace = true } serde_json = { workspace = true } diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index f5defb05ba4..84e43c499fb 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -39,6 +39,8 @@ pub struct BaseImageInfo { pub size: Option, /// The [BlurHash](https://blurha.sh/) for this image. pub blurhash: Option, + /// Whether this image is animated. + pub is_animated: Option, } /// Base metadata about a video. @@ -100,6 +102,7 @@ impl From for ImageInfo { width: info.width, size: info.size, blurhash: info.blurhash, + is_animated: info.is_animated, }), _ => ImageInfo::new(), } diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 8fc38451d6a..2a94c991661 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -89,6 +89,7 @@ async fn test_room_attachment_send_info() { width: Some(uint!(800)), size: None, blurhash: None, + is_animated: None, })) .caption(Some("image caption".to_owned())); @@ -217,6 +218,7 @@ async fn test_room_attachment_send_info_thumbnail() { width: Some(uint!(800)), size: None, blurhash: None, + is_animated: None, })); let response = room @@ -299,3 +301,51 @@ async fn test_room_attachment_send_mentions() { assert_eq!(expected_event_id, response.event_id); } + +#[async_test] +async fn test_room_attachment_send_is_animated() { + let mock = MatrixMockServer::new().await; + + let expected_event_id = event_id!("$h29iv0s8:example.com"); + mock.mock_room_send() + .body_matches_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + "org.matrix.msc4230.is_animated": false, + } + })) + .ok(expected_event_id) + .mock_once() + .mount() + .await; + + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() + .await; + + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; + + let config = AttachmentConfig::new() + .info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + is_animated: Some(false), + })) + .caption(Some("image caption".to_owned())); + + let response = room + .send_attachment("image.jpg", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config) + .await + .unwrap(); + + assert_eq!(expected_event_id, response.event_id) +} diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 8b16194dba2..2c965062090 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -54,6 +54,7 @@ async fn queue_attachment_no_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'stat width: Some(uint!(37)), size: Some(uint!(42)), blurhash: None, + is_animated: None, })); let handle = q .send_attachment(filename, content_type, data, config) @@ -85,6 +86,7 @@ async fn queue_attachment_with_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'st width: Some(uint!(37)), size: Some(uint!(42)), blurhash: None, + is_animated: None, }, )); @@ -1811,6 +1813,7 @@ async fn test_media_uploads() { width: Some(uint!(38)), size: Some(uint!(43)), blurhash: None, + is_animated: Some(false), }); let transaction_id = TransactionId::new(); @@ -1871,6 +1874,7 @@ async fn test_media_uploads() { assert_eq!(info.size, Some(uint!(43))); assert_eq!(info.mimetype.as_deref(), Some("image/jpeg")); assert!(info.blurhash.is_none()); + assert_eq!(info.is_animated, Some(false)); // Check the data source: it should reference the send queue local storage. let local_source = img_content.source; From 5941495e68a84961305ca2f6d72c7aa806ef78f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 10 Jan 2025 12:07:26 +0100 Subject: [PATCH 924/979] feat(sdk): Implement Default for AttachmentInfo types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since all of their fields are optional, it simplifies their construction. Signed-off-by: Kévin Commaille --- crates/matrix-sdk/CHANGELOG.md | 2 ++ crates/matrix-sdk/src/attachment.rs | 8 ++++---- .../tests/integration/room/attachment/mod.rs | 14 ++++---------- crates/matrix-sdk/tests/integration/send_queue.rs | 8 +++----- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 7f8806bc30c..c8f627b8b52 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. - Allow to set and check whether an image is animated via its `ImageInfo`. ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) +- Implement `Default` for `BaseImageInfo`, `BaseVideoInfo`, `BaseAudioInfo` and + `BaseFileInfo`. ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) ### Refactor diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 84e43c499fb..c39f00e30a7 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -29,7 +29,7 @@ use ruma::{ }; /// Base metadata about an image. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct BaseImageInfo { /// The height of the image in pixels. pub height: Option, @@ -44,7 +44,7 @@ pub struct BaseImageInfo { } /// Base metadata about a video. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct BaseVideoInfo { /// The duration of the video. pub duration: Option, @@ -59,7 +59,7 @@ pub struct BaseVideoInfo { } /// Base metadata about an audio clip. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct BaseAudioInfo { /// The duration of the audio clip. pub duration: Option, @@ -68,7 +68,7 @@ pub struct BaseAudioInfo { } /// Base metadata about a file. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct BaseFileInfo { /// The size of the file in bytes. pub size: Option, diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 2a94c991661..7ef75d97cb6 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -87,9 +87,7 @@ async fn test_room_attachment_send_info() { .info(AttachmentInfo::Image(BaseImageInfo { height: Some(uint!(600)), width: Some(uint!(800)), - size: None, - blurhash: None, - is_animated: None, + ..Default::default() })) .caption(Some("image caption".to_owned())); @@ -140,8 +138,7 @@ async fn test_room_attachment_send_wrong_info() { height: Some(uint!(600)), width: Some(uint!(800)), duration: Some(Duration::from_millis(3600)), - size: None, - blurhash: None, + ..Default::default() })) .caption(Some("image caption".to_owned())); @@ -216,9 +213,7 @@ async fn test_room_attachment_send_info_thumbnail() { .info(AttachmentInfo::Image(BaseImageInfo { height: Some(uint!(600)), width: Some(uint!(800)), - size: None, - blurhash: None, - is_animated: None, + ..Default::default() })); let response = room @@ -336,9 +331,8 @@ async fn test_room_attachment_send_is_animated() { .info(AttachmentInfo::Image(BaseImageInfo { height: Some(uint!(600)), width: Some(uint!(800)), - size: None, - blurhash: None, is_animated: Some(false), + ..Default::default() })) .caption(Some("image caption".to_owned())); diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 2c965062090..a35fcad9de2 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -53,8 +53,7 @@ async fn queue_attachment_no_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'stat height: Some(uint!(13)), width: Some(uint!(37)), size: Some(uint!(42)), - blurhash: None, - is_animated: None, + ..Default::default() })); let handle = q .send_attachment(filename, content_type, data, config) @@ -85,8 +84,7 @@ async fn queue_attachment_with_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'st height: Some(uint!(13)), width: Some(uint!(37)), size: Some(uint!(42)), - blurhash: None, - is_animated: None, + ..Default::default() }, )); @@ -1812,8 +1810,8 @@ async fn test_media_uploads() { height: Some(uint!(14)), width: Some(uint!(38)), size: Some(uint!(43)), - blurhash: None, is_animated: Some(false), + ..Default::default() }); let transaction_id = TransactionId::new(); From a79d409f9d18323bdd71a6ec8897a40b8e135e4b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jan 2025 09:17:22 +0100 Subject: [PATCH 925/979] task(bindings): Expose `withdraw_verification` in `UserIdentity` --- bindings/matrix-sdk-ffi/CHANGELOG.md | 1 + bindings/matrix-sdk-ffi/src/encryption.rs | 9 +++++++++ crates/matrix-sdk/src/encryption/identities/users.rs | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index e41d3540d1e..d13c93d1a22 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -34,3 +34,4 @@ Additions: - Add `Encryption::get_user_identity` which returns `UserIdentity` - Add `ClientBuilder::room_key_recipient_strategy` - Add `Room::send_raw` +- Expose `withdraw_verification` to `UserIdentity` diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index 1ec9547cd92..b7baaea8943 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -478,6 +478,15 @@ impl UserIdentity { Ok(self.inner.pin().await?) } + /// Remove the requirement for this identity to be verified. + /// + /// If an identity was previously verified and is not anymore it will be + /// reported to the user. In order to remove this notice users have to + /// verify again or to withdraw the verification requirement. + pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> { + Ok(self.inner.withdraw_verification().await?) + } + /// Get the public part of the Master key of this user identity. /// /// The public part of the Master key is usually used to uniquely identify diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index 0edeae8b997..5c3ef7f12aa 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -415,7 +415,7 @@ impl UserIdentity { /// Remove the requirement for this identity to be verified. /// - /// If an identity was previously verified and is not any more it will be + /// If an identity was previously verified and is not anymore it will be /// reported to the user. In order to remove this notice users have to /// verify again or to withdraw the verification requirement. pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> { From 4ebf5056bee7cc5724b37b556e0bbf24f3b96ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 10 Jan 2025 20:03:33 +0100 Subject: [PATCH 926/979] chore: Remove our ancient upgrade guide --- UPGRADING-0.5-to-0.6.md | 121 ---------------------------------------- 1 file changed, 121 deletions(-) delete mode 100644 UPGRADING-0.5-to-0.6.md diff --git a/UPGRADING-0.5-to-0.6.md b/UPGRADING-0.5-to-0.6.md deleted file mode 100644 index 3b8bb91c386..00000000000 --- a/UPGRADING-0.5-to-0.6.md +++ /dev/null @@ -1,121 +0,0 @@ -# Upgrades 0.5 ➜ 0.6 - -This is a rough migration guide to help you upgrade your code using matrix-sdk 0.5 to the newly released matrix-sdk 0.6 . While it won't cover all edge cases and problems, we are trying to get the most common issues covered. If you experience any other difficulties in upgrade or need support with using the matrix-sdk in general, please approach us in our [matrix-sdk channel on matrix.org][matrix-channel]. - -## Minimum Supported Rust Version Update: `1.60` - -We have updated the minimal rust version you need in order to build `matrix-sdk`, as we require some new dependency resolving features from it: - -> These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.60 - -## Dependencies - -Many dependencies have been upgraded. Most notably, we are using `ruma` at version `0.7.0` now. It has seen some renamings and restructurings since our last release, so you might find that some Types have new names now. - -## Repo Structure Updates - -If you are looking at the repository itself, you will find we've rearranged the code quite a bit: we have split out any bindings-specific and testing related crates (and other things) into respective folders, and we've moved all `examples` into its own top-level-folder with each example as their own crate (rendering them easier to find and copy as starting points), all in all slimming down the `crates` folder to the core aspects. - - -## Architecture Changes / API overall - -### Builder Pattern - -We are moving to the [builder pattern][] (familiar from e.g. `std::io:process:Command`) as the main configurable path for many aspects of the API, including to construct Matrix-Requests and workflows. This has been and is an on-going effort, and this release sees a lot of APIs transitioning to this pattern, you should already be familiar with from the `matrix_sdk::Client::builder()` in `0.5`. This pattern been extended onto: - - the [login configuration][login builder] and [login with sso][ssologin builder], - - [`SledStore` configuratiion][sled-store builder] - - [`Indexeddb` configuration][indexeddb builder] - -Most have fallback (though maybe with deprecation warning) support for an existing code path, but these are likely to be removed in upcoming releases. - -### Splitting of concerns: Media - -In an effort to declutter the `Client` API dedicated types have been created dealing with specific concerns in one place. In `0.5` we introduced `client.account()`, and `client.encryption()`, we are doing the same with `client.media()` to manage media and attachments in one place with the [`media::Media` type][media typ] now. - -The signatures of media uploads, have also changed slightly: rather than expecting a reader `R: Read + Seek`, it now is a simple `&[u8]`. Which also means no more unnecessary `seek(0)` to reset the cursor, as we are just taking an immutable reference now. - -### Event Handling & sync updaes - -If you are using the `client.register_event_handler` function to receive updates on incoming sync events, you'll find yourself with a deprecation warning now. That is because we've refactored and redesigned the event handler logic to allowing `removing` of event handlers on the fly, too. For that the new `add_event_handler()` (and `add_room_event_handler`) will hand you an `EventHandlerHandle` (pardon the pun), which you can pass to `remove_event_handler`, or by using the convenient `client.event_handler_drop_guard` to create a `DropGuard` that will remove the handler when the guard is dropped. While the code still works, we recommend you switch to the new one, as we will be removing the `register_event_handler` and `register_event_handler_context` in a coming release. - -Secondly, you will find a new [`sync_with_result_callback` sync function][sync with result]. Other than the previous sync functions, this will pass the entire `Result` to your callback, allowing you to handle errors or even raise some yourself to stop the loop. Further more, it will propagate any unhandled errors (it still handles retries as before) to the outer caller, allowing the higher level to decide how to handle that (e.g. in case of a network failure). This result-returning-behavior also punshes through the existing `sync` and `sync_with_callback`-API, allowing you to handle them on a higher level now (rather than the futures just resolving). If you find that warning, just adding a `?` to the `.await` of the call is probably the quickest way to move forward. - -### Refresh Tokens - -This release now [supports `refresh_token`s][refresh tokens PR] as part of the [`Session`][session]. It is implemented with a default-flag in serde so deserializing a previously serialized Session (e.g. in a store) will work as before. As part of `refresh_token` support, you can now configure the client via `ClientBuilder.request_refresh_token()` to refresh the access token automagically on certain failures or do it manually by calling `client.refresh_access_token()` yourself. Auto-refresh is _off_ by default. - -You can stay informed about updates on the access token by listening to `client.session_tokens_signal()`. - -### Further changes - - - [`MessageOptions`][message options] has been updated to Matrix 1.3 by making the `from` parameter optional (and function signatures have been updated, too). You can now request the server sends you messages from the first one you are allowed to have received. - - `client.user_id()` is not a `future` anymore. Remove any `.await` you had behind it. - - `verified()`, `blacklisted()` and `deleted()` on `matrix_sdk::encryption::identities::Device` have been renamed with a `is_` prefix. - - `verified()` on `matrix_sdk::encryption::identities::UserIdentity`, too has been prefixed with `is_` and thus is now called `is_verified()`. - - The top-level crypto and state-store types of Indexeddb and Sled have been renamed to unique types> - - `state_store` and `crypto_store` do not need to be boxed anymore when passed to the [`StoreConfig`][store config] - - Indexeddb's `SerializationError` is now `IndexedDBStoreError` - - Javascript specific features are now behind the `js` feature-gate - - The new experimental next generation of sync ("sliding sync"), with a totally revamped api, can be found behind the optional `sliding-sync`-feature-gate - - -## Quick Troubleshooting - -You find yourself focused with any of these, here are the steps to follow to upgrade your code accordingly: - -### warning: use of deprecated associated function `matrix_sdk::Client::register_event_handler`: Use [`Client::add_event_handler`](#method.add_event_handler) instead - -As it says on the tin: we have deprecated this function in favor of the newer removable handler approach (see above). You can still continue to use this `fn` for now, but it will be removed in a future release. - -### warning: use of deprecated associated function `matrix_sdk::Client::login`: Replaced by [`Client::login_username`](#method.login_username) - -We have replaced the login facilities with a `LoginBuilder` and recommend you use that from now on. This isn't an error yet, but the function will be removed in a future release. - -### expected slice `[u8]`, found struct ... - -We've updated the `send_attachment` and `Media` signatures to use `&[u8]` rather than `reader: Read + Seek` as it is more convenient and common place for most architectures anyways. If you are using `File::open(path)?` to get that handler, you can just replace that with `std::fs::read(path)?` - -### no method named `verified` found for struct `matrix_sdk::encryption::identities::Device` in the current scope - -Boolean flags like `verified`, `deleted`, `blacklisted`, etc have been renamed with a `is_` prefix. So, just follow the cargo suggestion: -``` - | -69 | device.verified() - | ^^^^^^^^ help: there is an associated function with a similar name: `is_verified` - ``` - - ### unresolved import `matrix_sdk::ruma::events::AnySyncRoomEvent` - - Ruma has been updated to `0.7.0`, you will find some ruma Events names have changed, most notably, the `AnySyncRoomEvent` is now named `AnySyncTimelineEvent` (and not `AnySyncStateEvent`, which cargo wrongly suggests). Just rename the import and usage of it. - -### `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future - -You are seeing something along the lines of: -``` -19 | if room_member.state_key != client.user_id().await.unwrap() { - | ^^^^^^ `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future - | - = help: the trait `Future` is not implemented for `std::option::Option<&matrix_sdk::ruma::UserId>` - = note: std::option::Option<&matrix_sdk::ruma::UserId> must be a future or must implement `IntoFuture` to be awaited - = note: required because of the requirements on the impl of `IntoFuture` for `std::option::Option<&matrix_sdk::ruma::UserId>` -help: remove the `.await` - | -19 - if room_member.state_key != client.user_id().await.unwrap() { -19 + if room_member.state_key != client.user_id().unwrap() { -``` - -You are using `client.user_id().await` but `user_id()` is no longer `async`. Just follow the cargo suggestion and remove the `.await`, it is not necessary any longer. - - - [matrix-channel]: https://matrix.to/#/#matrix-rust-sdk:matrix.org - [builder pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html - [login builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.LoginBuilder.html - [ssologin builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.SsoLoginBuilder.html - [sled-store builder]: https://docs.rs/matrix-sdk-sled/latest/matrix_sdk_sled/struct.SledStateStoreBuilder.html - [indexeddb builder]: https://docs.rs/matrix-sdk-indexeddb/latest/matrix_sdk_indexeddb/struct.IndexeddbStateStoreBuilder.html - [media type]: https://docs.rs/matrix-sdk/latest/matrix_sdk//media/struct.Media.html - [sync with result]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Client.html#method.sync_with_result_callback - [session]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Session.html - [refresh tokens PR]: https://github.com/matrix-org/matrix-rust-sdk/pull/892 - [store config]: https://docs.rs/matrix-sdk-base/latest/matrix_sdk_base/store/struct.StoreConfig.html - [message options]: https://docs.rs/matrix-sdk/latest/matrix_sdk/room/struct.MessagesOptions.html From d6c2a63f5cd8652cba7cac52e61e3dc90511d213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 10 Jan 2025 12:57:22 +0100 Subject: [PATCH 927/979] refactor: Use the simplified locks in the encryption tasks --- crates/matrix-sdk/src/encryption/backups/mod.rs | 4 ++-- crates/matrix-sdk/src/encryption/mod.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index ba1e99f06dc..538bc238c26 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -1007,7 +1007,7 @@ impl Backups { room_id: OwnedRoomId, event: Raw, ) { - let tasks = self.client.inner.e2ee.tasks.lock().unwrap(); + let tasks = self.client.inner.e2ee.tasks.lock(); if let Some(task) = tasks.download_room_keys.as_ref() { task.trigger_download_for_utd_event(room_id, event); } @@ -1016,7 +1016,7 @@ impl Backups { /// Send a notification to the task which is responsible for uploading room /// keys to the backup that it might have new room keys to back up. pub(crate) fn maybe_trigger_backup(&self) { - let tasks = self.client.inner.e2ee.tasks.lock().unwrap(); + let tasks = self.client.inner.e2ee.tasks.lock(); if let Some(tasks) = tasks.upload_room_keys.as_ref() { tasks.trigger_upload(); diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 4c730711189..e59bf74ab8e 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -21,7 +21,7 @@ use std::{ io::{Cursor, Read, Write}, iter, path::PathBuf, - sync::{Arc, Mutex as StdMutex}, + sync::Arc, }; use eyeball::{SharedObservable, Subscriber}; @@ -37,7 +37,7 @@ use matrix_sdk_base::crypto::{ }, CrossSigningBootstrapRequests, OlmMachine, }; -use matrix_sdk_common::executor::spawn; +use matrix_sdk_common::{executor::spawn, locks::Mutex as StdMutex}; use ruma::{ api::client::{ keys::{ @@ -131,7 +131,7 @@ impl EncryptionData { pub fn initialize_room_key_tasks(&self, client: &Arc) { let weak_client = WeakClient::from_inner(client); - let mut tasks = self.tasks.lock().unwrap(); + let mut tasks = self.tasks.lock(); tasks.upload_room_keys = Some(BackupUploadingTask::new(weak_client.clone())); if self.encryption_settings.backup_download_strategy @@ -147,7 +147,7 @@ impl EncryptionData { /// This should happen after the usual tasks have been set up and after the /// E2EE initialization tasks have been set up. pub fn initialize_recovery_state_update_task(&self, client: &Client) { - let mut guard = self.tasks.lock().unwrap(); + let mut guard = self.tasks.lock(); let future = Recovery::update_state_after_backup_state_change(client); let join_handle = spawn(future); @@ -1653,7 +1653,7 @@ impl Encryption { /// allow for the initial upload of cross-signing keys without /// authentication, rendering this parameter obsolete. pub(crate) fn spawn_initialization_task(&self, auth_data: Option) { - let mut tasks = self.client.inner.e2ee.tasks.lock().unwrap(); + let mut tasks = self.client.inner.e2ee.tasks.lock(); let this = self.clone(); tasks.setup_e2ee = Some(spawn(async move { @@ -1679,7 +1679,7 @@ impl Encryption { /// Waits for end-to-end encryption initialization tasks to finish, if any /// was running in the background. pub async fn wait_for_e2ee_initialization_tasks(&self) { - let task = self.client.inner.e2ee.tasks.lock().unwrap().setup_e2ee.take(); + let task = self.client.inner.e2ee.tasks.lock().setup_e2ee.take(); if let Some(task) = task { if let Err(err) = task.await { From e37ad11b47a1be8cacabf71e0a97327a6271fe3a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 9 Jan 2025 21:12:27 +0100 Subject: [PATCH 928/979] refactor(ui): Use RPITIT / AFIT for RoomDataProvider --- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 79 +++---- crates/matrix-sdk-ui/src/timeline/traits.rs | 218 ++++++++---------- 2 files changed, 135 insertions(+), 162 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 13f974f1621..d47e0b78dba 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -16,7 +16,6 @@ use std::{ collections::{BTreeMap, HashMap}, - future::ready, ops::Sub, sync::Arc, time::{Duration, SystemTime}, @@ -25,7 +24,6 @@ use std::{ use eyeball::{SharedObservable, Subscriber}; use eyeball_im::VectorDiff; use futures_core::Stream; -use futures_util::FutureExt as _; use indexmap::IndexMap; use matrix_sdk::{ config::RequestConfig, @@ -380,8 +378,8 @@ impl RoomDataProvider for TestRoomDataProvider { RoomVersionId::V10 } - fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo> { - ready(CryptoContextInfo { + async fn crypto_context_info(&self) -> CryptoContextInfo { + CryptoContextInfo { device_creation_ts: MilliSecondsSinceUnixEpoch::from_system_time( SystemTime::now().sub(Duration::from_secs(60 * 3)), ) @@ -389,47 +387,42 @@ impl RoomDataProvider for TestRoomDataProvider { is_backup_configured: false, this_device_is_verified: true, backup_exists_on_server: true, - }) - .boxed() + } } - fn profile_from_user_id<'a>(&'a self, _user_id: &'a UserId) -> BoxFuture<'a, Option> { - ready(None).boxed() + async fn profile_from_user_id<'a>(&'a self, _user_id: &'a UserId) -> Option { + None } fn profile_from_latest_event(&self, _latest_event: &LatestEvent) -> Option { None } - fn load_user_receipt( - &self, + async fn load_user_receipt<'a>( + &'a self, receipt_type: ReceiptType, thread: ReceiptThread, - user_id: &UserId, - ) -> BoxFuture<'_, Option<(OwnedEventId, Receipt)>> { - ready( - self.initial_user_receipts - .get(&receipt_type) - .and_then(|thread_map| thread_map.get(&thread)) - .and_then(|user_map| user_map.get(user_id)) - .cloned(), - ) - .boxed() - } - - fn load_event_receipts( - &self, - event_id: &EventId, - ) -> BoxFuture<'_, IndexMap> { - ready(if event_id == event_id!("$event_with_bob_receipt") { + user_id: &'a UserId, + ) -> Option<(OwnedEventId, Receipt)> { + self.initial_user_receipts + .get(&receipt_type) + .and_then(|thread_map| thread_map.get(&thread)) + .and_then(|user_map| user_map.get(user_id)) + .cloned() + } + + async fn load_event_receipts<'a>( + &'a self, + event_id: &'a EventId, + ) -> IndexMap { + if event_id == event_id!("$event_with_bob_receipt") { [(BOB.to_owned(), Receipt::new(MilliSecondsSinceUnixEpoch(uint!(10))))].into() } else { IndexMap::new() - }) - .boxed() + } } - fn push_rules_and_context(&self) -> BoxFuture<'_, Option<(Ruleset, PushConditionRoomCtx)>> { + async fn push_rules_and_context(&self) -> Option<(Ruleset, PushConditionRoomCtx)> { let push_rules = Ruleset::server_default(&ALICE); let power_levels = PushConditionPowerLevelsCtx { users: BTreeMap::new(), @@ -444,32 +437,26 @@ impl RoomDataProvider for TestRoomDataProvider { power_levels: Some(power_levels), }; - ready(Some((push_rules, push_context))).boxed() + Some((push_rules, push_context)) } - fn load_fully_read_marker(&self) -> BoxFuture<'_, Option> { - ready(self.fully_read_marker.clone()).boxed() + async fn load_fully_read_marker(&self) -> Option { + self.fully_read_marker.clone() } - fn send(&self, content: AnyMessageLikeEventContent) -> BoxFuture<'_, Result<(), super::Error>> { - async move { - self.sent_events.write().await.push(content); - Ok(()) - } - .boxed() + async fn send(&self, content: AnyMessageLikeEventContent) -> Result<(), super::Error> { + self.sent_events.write().await.push(content); + Ok(()) } - fn redact<'a>( + async fn redact<'a>( &'a self, event_id: &'a EventId, _reason: Option<&'a str>, _transaction_id: Option, - ) -> BoxFuture<'a, Result<(), super::Error>> { - async move { - self.redacted.write().await.push(event_id.to_owned()); - Ok(()) - } - .boxed() + ) -> Result<(), super::Error> { + self.redacted.write().await.push(event_id.to_owned()); + Ok(()) } fn room_info(&self) -> Subscriber { diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index b865ff8e33f..55cae54db5f 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -15,14 +15,12 @@ use std::future::Future; use eyeball::Subscriber; -use futures_util::FutureExt as _; use indexmap::IndexMap; #[cfg(test)] use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement}; use matrix_sdk::{ crypto::types::events::CryptoContextInfo, deserialized_responses::TimelineEvent, - event_cache::paginator::PaginableRoom, AsyncTraitDeps, BoxFuture, Result, Room, - SendOutsideWasm, + event_cache::paginator::PaginableRoom, AsyncTraitDeps, Result, Room, SendOutsideWasm, }; use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo}; use ruma::{ @@ -78,9 +76,13 @@ pub(super) trait RoomDataProvider: fn own_user_id(&self) -> &UserId; fn room_version(&self) -> RoomVersionId; - fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo>; + fn crypto_context_info(&self) + -> impl Future + SendOutsideWasm + '_; - fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option>; + fn profile_from_user_id<'a>( + &'a self, + user_id: &'a UserId, + ) -> impl Future> + SendOutsideWasm + 'a; fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option; /// Loads a user receipt from the storage backend. @@ -89,21 +91,26 @@ pub(super) trait RoomDataProvider: receipt_type: ReceiptType, thread: ReceiptThread, user_id: &'a UserId, - ) -> BoxFuture<'a, Option<(OwnedEventId, Receipt)>>; + ) -> impl Future> + SendOutsideWasm + 'a; /// Loads read receipts for an event from the storage backend. fn load_event_receipts<'a>( &'a self, event_id: &'a EventId, - ) -> BoxFuture<'a, IndexMap>; + ) -> impl Future> + SendOutsideWasm + 'a; /// Load the current fully-read event id, from storage. - fn load_fully_read_marker(&self) -> BoxFuture<'_, Option>; + fn load_fully_read_marker(&self) -> impl Future> + '_; - fn push_rules_and_context(&self) -> BoxFuture<'_, Option<(Ruleset, PushConditionRoomCtx)>>; + fn push_rules_and_context( + &self, + ) -> impl Future> + SendOutsideWasm + '_; /// Send an event to that room. - fn send(&self, content: AnyMessageLikeEventContent) -> BoxFuture<'_, Result<(), super::Error>>; + fn send( + &self, + content: AnyMessageLikeEventContent, + ) -> impl Future> + SendOutsideWasm + '_; /// Redact an event from that room. fn redact<'a>( @@ -111,7 +118,7 @@ pub(super) trait RoomDataProvider: event_id: &'a EventId, reason: Option<&'a str>, transaction_id: Option, - ) -> BoxFuture<'a, Result<(), super::Error>>; + ) -> impl Future> + SendOutsideWasm + 'a; fn room_info(&self) -> Subscriber; } @@ -125,27 +132,24 @@ impl RoomDataProvider for Room { (**self).clone_info().room_version_or_default() } - fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo> { - async move { self.crypto_context_info().await }.boxed() + async fn crypto_context_info(&self) -> CryptoContextInfo { + self.crypto_context_info().await } - fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option> { - async move { - match self.get_member_no_sync(user_id).await { - Ok(Some(member)) => Some(Profile { - display_name: member.display_name().map(ToOwned::to_owned), - display_name_ambiguous: member.name_ambiguous(), - avatar_url: member.avatar_url().map(ToOwned::to_owned), - }), - Ok(None) if self.are_members_synced() => Some(Profile::default()), - Ok(None) => None, - Err(e) => { - error!(%user_id, "Failed to fetch room member information: {e}"); - None - } + async fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> Option { + match self.get_member_no_sync(user_id).await { + Ok(Some(member)) => Some(Profile { + display_name: member.display_name().map(ToOwned::to_owned), + display_name_ambiguous: member.name_ambiguous(), + avatar_url: member.avatar_url().map(ToOwned::to_owned), + }), + Ok(None) if self.are_members_synced() => Some(Profile::default()), + Ok(None) => None, + Err(e) => { + error!(%user_id, "Failed to fetch room member information: {e}"); + None } } - .boxed() } fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option { @@ -160,128 +164,110 @@ impl RoomDataProvider for Room { }) } - fn load_user_receipt<'a>( + async fn load_user_receipt<'a>( &'a self, receipt_type: ReceiptType, thread: ReceiptThread, user_id: &'a UserId, - ) -> BoxFuture<'a, Option<(OwnedEventId, Receipt)>> { - async move { - match self.load_user_receipt(receipt_type.clone(), thread.clone(), user_id).await { - Ok(receipt) => receipt, - Err(e) => { - error!( - ?receipt_type, - ?thread, - ?user_id, - "Failed to get read receipt for user: {e}" - ); - None - } + ) -> Option<(OwnedEventId, Receipt)> { + match self.load_user_receipt(receipt_type.clone(), thread.clone(), user_id).await { + Ok(receipt) => receipt, + Err(e) => { + error!( + ?receipt_type, + ?thread, + ?user_id, + "Failed to get read receipt for user: {e}" + ); + None } } - .boxed() } - fn load_event_receipts<'a>( + async fn load_event_receipts<'a>( &'a self, event_id: &'a EventId, - ) -> BoxFuture<'a, IndexMap> { - async move { - let mut unthreaded_receipts = match self - .load_event_receipts(ReceiptType::Read, ReceiptThread::Unthreaded, event_id) - .await - { - Ok(receipts) => receipts.into_iter().collect(), - Err(e) => { - error!(?event_id, "Failed to get unthreaded read receipts for event: {e}"); - IndexMap::new() - } - }; + ) -> IndexMap { + let mut unthreaded_receipts = match self + .load_event_receipts(ReceiptType::Read, ReceiptThread::Unthreaded, event_id) + .await + { + Ok(receipts) => receipts.into_iter().collect(), + Err(e) => { + error!(?event_id, "Failed to get unthreaded read receipts for event: {e}"); + IndexMap::new() + } + }; - let main_thread_receipts = match self - .load_event_receipts(ReceiptType::Read, ReceiptThread::Main, event_id) - .await - { - Ok(receipts) => receipts, - Err(e) => { - error!(?event_id, "Failed to get main thread read receipts for event: {e}"); - Vec::new() - } - }; + let main_thread_receipts = match self + .load_event_receipts(ReceiptType::Read, ReceiptThread::Main, event_id) + .await + { + Ok(receipts) => receipts, + Err(e) => { + error!(?event_id, "Failed to get main thread read receipts for event: {e}"); + Vec::new() + } + }; - unthreaded_receipts.extend(main_thread_receipts); - unthreaded_receipts - } - .boxed() + unthreaded_receipts.extend(main_thread_receipts); + unthreaded_receipts } - fn push_rules_and_context(&self) -> BoxFuture<'_, Option<(Ruleset, PushConditionRoomCtx)>> { - async { - match self.push_context().await { - Ok(Some(push_context)) => match self.client().account().push_rules().await { - Ok(push_rules) => Some((push_rules, push_context)), - Err(e) => { - error!("Could not get push rules: {e}"); - None - } - }, - Ok(None) => { - debug!("Could not aggregate push context"); - None - } + async fn push_rules_and_context(&self) -> Option<(Ruleset, PushConditionRoomCtx)> { + match self.push_context().await { + Ok(Some(push_context)) => match self.client().account().push_rules().await { + Ok(push_rules) => Some((push_rules, push_context)), Err(e) => { - error!("Could not get push context: {e}"); + error!("Could not get push rules: {e}"); None } + }, + Ok(None) => { + debug!("Could not aggregate push context"); + None + } + Err(e) => { + error!("Could not get push context: {e}"); + None } } - .boxed() } - fn load_fully_read_marker(&self) -> BoxFuture<'_, Option> { - async { - match self.account_data_static::().await { - Ok(Some(fully_read)) => match fully_read.deserialize() { - Ok(fully_read) => Some(fully_read.content.event_id), - Err(e) => { - error!("Failed to deserialize fully-read account data: {e}"); - None - } - }, + async fn load_fully_read_marker(&self) -> Option { + match self.account_data_static::().await { + Ok(Some(fully_read)) => match fully_read.deserialize() { + Ok(fully_read) => Some(fully_read.content.event_id), Err(e) => { - error!("Failed to get fully-read account data from the store: {e}"); + error!("Failed to deserialize fully-read account data: {e}"); None } - _ => None, + }, + Err(e) => { + error!("Failed to get fully-read account data from the store: {e}"); + None } + _ => None, } - .boxed() } - fn send(&self, content: AnyMessageLikeEventContent) -> BoxFuture<'_, Result<(), super::Error>> { - async move { - let _ = self.send_queue().send(content).await?; - Ok(()) - } - .boxed() + async fn send(&self, content: AnyMessageLikeEventContent) -> Result<(), super::Error> { + let _ = self.send_queue().send(content).await?; + Ok(()) } - fn redact<'a>( + async fn redact<'a>( &'a self, event_id: &'a EventId, reason: Option<&'a str>, transaction_id: Option, - ) -> BoxFuture<'a, Result<(), super::Error>> { - async move { - let _ = self - .redact(event_id, reason, transaction_id) - .await - .map_err(RedactError::HttpError) - .map_err(super::Error::RedactError)?; - Ok(()) - } - .boxed() + ) -> Result<(), super::Error> { + let _ = self + .redact(event_id, reason, transaction_id) + .await + .map_err(RedactError::HttpError) + .map_err(super::Error::RedactError)?; + Ok(()) } fn room_info(&self) -> Subscriber { From f173aea6e47708851bd38a4f55b78a1a46e1666e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 12 Jan 2025 20:32:58 +0100 Subject: [PATCH 929/979] feat(sdk): Expose Client::server_versions publicly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/CHANGELOG.md | 2 ++ crates/matrix-sdk/src/client/mod.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index c8f627b8b52..8ad98b41958 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to this project will be documented in this file. ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) - Implement `Default` for `BaseImageInfo`, `BaseVideoInfo`, `BaseAudioInfo` and `BaseFileInfo`. ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) +- Expose `Client::server_versions()` publicly to allow users of the library to + get the versions of Matrix supported by the homeserver. ### Refactor diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 033bb2ac69f..5c9396105e3 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -1736,11 +1736,30 @@ impl Client { Ok(f(&guard).unwrap()) } - pub(crate) async fn server_versions(&self) -> HttpResult> { + /// Get the Matrix versions supported by the homeserver by fetching them + /// from the server or the cache. + /// + /// # Examples + /// + /// ```no_run + /// use ruma::api::MatrixVersion; + /// # use matrix_sdk::{Client, config::SyncSettings}; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let mut client = Client::new(homeserver).await?; + /// + /// let server_versions = client.server_versions().await?; + /// let supports_1_1 = server_versions.contains(&MatrixVersion::V1_1); + /// println!("The homeserver supports Matrix 1.1: {supports_1_1:?}"); + /// # anyhow::Ok(()) }; + /// ``` + pub async fn server_versions(&self) -> HttpResult> { self.get_or_load_and_cache_server_capabilities(|caps| caps.server_versions.clone()).await } - /// Get unstable features from by fetching from the server or the cache. + /// Get the unstable features supported by the homeserver by fetching them + /// from the server or the cache. /// /// # Examples /// @@ -1751,7 +1770,9 @@ impl Client { /// # let homeserver = Url::parse("http://localhost:8080")?; /// # let mut client = Client::new(homeserver).await?; /// let unstable_features = client.unstable_features().await?; - /// let msc_x = unstable_features.get("msc_x").unwrap_or(&false); + /// let supports_msc_x = + /// unstable_features.get("msc_x").copied().unwrap_or(false); + /// println!("The homeserver supports msc X: {supports_msc_x:?}"); /// # anyhow::Ok(()) }; /// ``` pub async fn unstable_features(&self) -> HttpResult> { From ca9eb70db5c057c2e3340028af53536a5dffe755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 12 Jan 2025 20:43:14 +0100 Subject: [PATCH 930/979] Add PR link to changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 8ad98b41958..6673f1e2a9b 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -13,7 +13,8 @@ All notable changes to this project will be documented in this file. - Implement `Default` for `BaseImageInfo`, `BaseVideoInfo`, `BaseAudioInfo` and `BaseFileInfo`. ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) - Expose `Client::server_versions()` publicly to allow users of the library to - get the versions of Matrix supported by the homeserver. + get the versions of Matrix supported by the homeserver. + ([#4519](https://github.com/matrix-org/matrix-rust-sdk/pull/4519)) ### Refactor From c9a49006f6901fcceeb6658c50957255fbc7af96 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 10:43:54 +0100 Subject: [PATCH 931/979] chore(xtask): tweak the TWiM report to include only merged PRs, not created PRs As an outsider, I am mostly interested in features and new developments that have happened, not those that *may* happen. An open-but-not-merged PR may not get merged in the end, or it may not get merged any time soon, creating false expectations. Merged PRs, on the other hand, have definitely happened (even if they get undone, that happens via other PRs that will get merged later). As such, I think it brings more value to outsiders. --- xtask/src/release.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtask/src/release.rs b/xtask/src/release.rs index 093c831a23a..010dcdceb8c 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -142,7 +142,7 @@ fn weekly_report() -> Result<()> { cmd!( sh, - "gh pr list --search created:>{one_week_ago} --json {JSON_FIELDS} --template {template}" + "gh pr list --search merged:>{one_week_ago} --json {JSON_FIELDS} --template {template}" ) .quiet() .run()?; From f61ad19ae66cc28ecbd64aff650bc841afc6b6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:11:58 +0100 Subject: [PATCH 932/979] feat(room): Add `RoomPrivacySettings` helper struct. This can be accessed through `fn Room::privacy_settings` and will wrap the functionality related to a room's access and privacy settings. This commit includes the `fn RoomPrivacySettings::update_canonical_alias` to modify the canonical alias of a room. --- crates/matrix-sdk/CHANGELOG.md | 1 + crates/matrix-sdk/src/room/mod.rs | 9 ++ .../matrix-sdk/src/room/privacy_settings.rs | 122 ++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 crates/matrix-sdk/src/room/privacy_settings.rs diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 6673f1e2a9b..a07382e6940 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Expose `Client::server_versions()` publicly to allow users of the library to get the versions of Matrix supported by the homeserver. ([#4519](https://github.com/matrix-org/matrix-rust-sdk/pull/4519)) +- Create `RoomPrivacySettings` helper to group room settings functionality related to room access and visibility ([#4401](https://github.com/matrix-org/matrix-rust-sdk/pull/4401)). ### Refactor diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 6ea0470e7a9..28ca6523da4 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -145,6 +145,7 @@ use crate::{ room::{ knock_requests::{KnockRequest, KnockRequestMemberInfo}, power_levels::{RoomPowerLevelChanges, RoomPowerLevelsExt}, + privacy_settings::RoomPrivacySettings, }, sync::RoomUpdate, utils::{IntoRawMessageLikeEventContent, IntoRawStateEventContent}, @@ -162,6 +163,9 @@ mod member; mod messages; pub mod power_levels; +/// Contains all the functionality for modifying the privacy settings in a room. +pub mod privacy_settings; + /// A struct containing methods that are common for Joined, Invited and Left /// Rooms #[derive(Debug, Clone)] @@ -3357,6 +3361,11 @@ impl Room { }) .collect()) } + + /// Access the room settings related to privacy and visibility. + pub fn privacy_settings(&self) -> RoomPrivacySettings<'_> { + RoomPrivacySettings::new(&self.inner, &self.client) + } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] diff --git a/crates/matrix-sdk/src/room/privacy_settings.rs b/crates/matrix-sdk/src/room/privacy_settings.rs new file mode 100644 index 00000000000..f3ede39243f --- /dev/null +++ b/crates/matrix-sdk/src/room/privacy_settings.rs @@ -0,0 +1,122 @@ +use matrix_sdk_base::Room as BaseRoom; +use ruma::{ + api::client::{ + state::send_state_event, + }, + assign, + events::{ + room::{ + canonical_alias::RoomCanonicalAliasEventContent, + }, + EmptyStateKey, + }, + OwnedRoomAliasId, +}; + +use crate::{Client, Result}; + +/// A helper to group the methods in [Room](crate::Room) related to the room's +/// visibility and access. +#[derive(Debug)] +pub struct RoomPrivacySettings<'a> { + room: &'a BaseRoom, + client: &'a Client, +} + +impl<'a> RoomPrivacySettings<'a> { + pub(crate) fn new(room: &'a BaseRoom, client: &'a Client) -> Self { + Self { room, client } + } + + /// Update the canonical alias of the room. + /// + /// # Arguments: + /// * `alias` - The new main alias to use for the room. A `None` value + /// removes the existing main canonical alias. + /// * `alt_aliases` - The list of alternative aliases for this room. + /// + /// See for more info about the canonical alias. + /// + /// Note that publishing the alias in the room directory is done separately, + /// and a room alias must have already been published before it can be set + /// as the canonical alias. + pub async fn update_canonical_alias( + &'a self, + alias: Option, + alt_aliases: Vec, + ) -> Result<()> { + // Create a new alias event combining both the new and previous values + let content = assign!( + RoomCanonicalAliasEventContent::new(), + { alias, alt_aliases } + ); + + // Send the state event + let request = send_state_event::v3::Request::new( + self.room.room_id().to_owned(), + &EmptyStateKey, + &content, + )?; + self.client.send(request).await?; + + Ok(()) + } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use matrix_sdk_test::{async_test}; + use ruma::{ + event_id, + events::{ + StateEventType, + }, + owned_room_alias_id, room_id, + }; + + use crate::test_utils::mocks::MatrixMockServer; + + #[async_test] + async fn test_update_canonical_alias_with_some_value() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + server + .mock_room_send_state() + .for_type(StateEventType::RoomCanonicalAlias) + .ok(event_id!("$a:b.c")) + .mock_once() + .mount() + .await; + + let room_alias = owned_room_alias_id!("#a:b.c"); + let ret = room + .privacy_settings() + .update_canonical_alias(Some(room_alias.clone()), Vec::new()) + .await; + assert!(ret.is_ok()); + } + + #[async_test] + async fn test_update_canonical_alias_with_no_value() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + server + .mock_room_send_state() + .for_type(StateEventType::RoomCanonicalAlias) + .ok(event_id!("$a:b.c")) + .mock_once() + .mount() + .await; + + let ret = room.privacy_settings().update_canonical_alias(None, Vec::new()).await; + assert!(ret.is_ok()); + } +} From 4fbe79a27d22a7b5b113d0b884c45a3008ddb124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:13:52 +0100 Subject: [PATCH 933/979] feat(room): Add `fn RoomPrivacySettings::update_room_history_visibility`. --- .../matrix-sdk/src/room/privacy_settings.rs | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/room/privacy_settings.rs b/crates/matrix-sdk/src/room/privacy_settings.rs index f3ede39243f..2f2679a3e83 100644 --- a/crates/matrix-sdk/src/room/privacy_settings.rs +++ b/crates/matrix-sdk/src/room/privacy_settings.rs @@ -1,12 +1,11 @@ use matrix_sdk_base::Room as BaseRoom; use ruma::{ - api::client::{ - state::send_state_event, - }, + api::client::state::send_state_event, assign, events::{ room::{ canonical_alias::RoomCanonicalAliasEventContent, + history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, }, EmptyStateKey, }, @@ -61,16 +60,33 @@ impl<'a> RoomPrivacySettings<'a> { Ok(()) } + + /// Update room history visibility for this room. + /// + /// The history visibility controls whether a user can see the events that + /// happened in a room before they joined. + /// + /// See for more info. + pub async fn update_room_history_visibility( + &'a self, + new_value: HistoryVisibility, + ) -> Result<()> { + let request = send_state_event::v3::Request::new( + self.room.room_id().to_owned(), + &EmptyStateKey, + &RoomHistoryVisibilityEventContent::new(new_value), + )?; + self.client.send(request).await?; + Ok(()) + } } #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use matrix_sdk_test::{async_test}; + use matrix_sdk_test::async_test; use ruma::{ event_id, - events::{ - StateEventType, - }, + events::{room::history_visibility::HistoryVisibility, StateEventType}, owned_room_alias_id, room_id, }; @@ -119,4 +135,25 @@ mod tests { let ret = room.privacy_settings().update_canonical_alias(None, Vec::new()).await; assert!(ret.is_ok()); } + + #[async_test] + async fn test_update_room_history_visibility() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + server + .mock_room_send_state() + .for_type(StateEventType::RoomHistoryVisibility) + .ok(event_id!("$a:b.c")) + .mock_once() + .mount() + .await; + + let ret = + room.privacy_settings().update_room_history_visibility(HistoryVisibility::Joined).await; + assert!(ret.is_ok()); + } } From 49985e54766e9c6bb8ad001eb1f833d8298ad712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:15:05 +0100 Subject: [PATCH 934/979] feat(room): Add `fn RoomPrivacySettings::update_join_rule`. --- .../matrix-sdk/src/room/privacy_settings.rs | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/room/privacy_settings.rs b/crates/matrix-sdk/src/room/privacy_settings.rs index 2f2679a3e83..dde3d170617 100644 --- a/crates/matrix-sdk/src/room/privacy_settings.rs +++ b/crates/matrix-sdk/src/room/privacy_settings.rs @@ -6,6 +6,7 @@ use ruma::{ room::{ canonical_alias::RoomCanonicalAliasEventContent, history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, + join_rules::{JoinRule, RoomJoinRulesEventContent}, }, EmptyStateKey, }, @@ -79,6 +80,22 @@ impl<'a> RoomPrivacySettings<'a> { self.client.send(request).await?; Ok(()) } + + /// Update the join rule for this room. + /// + /// The join rules controls if and how a new user can get access to the + /// room. + /// + /// See for more info. + pub async fn update_join_rule(&'a self, new_rule: JoinRule) -> Result<()> { + let request = send_state_event::v3::Request::new( + self.room.room_id().to_owned(), + &EmptyStateKey, + &RoomJoinRulesEventContent::new(new_rule), + )?; + self.client.send(request).await?; + Ok(()) + } } #[cfg(all(test, not(target_arch = "wasm32")))] @@ -86,7 +103,10 @@ mod tests { use matrix_sdk_test::async_test; use ruma::{ event_id, - events::{room::history_visibility::HistoryVisibility, StateEventType}, + events::{ + room::{history_visibility::HistoryVisibility, join_rules::JoinRule}, + StateEventType, + }, owned_room_alias_id, room_id, }; @@ -156,4 +176,24 @@ mod tests { room.privacy_settings().update_room_history_visibility(HistoryVisibility::Joined).await; assert!(ret.is_ok()); } + + #[async_test] + async fn test_update_join_rule() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + server + .mock_room_send_state() + .for_type(StateEventType::RoomJoinRules) + .ok(event_id!("$a:b.c")) + .mock_once() + .mount() + .await; + + let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await; + assert!(ret.is_ok()); + } } From 587545ae82264dc0d9b78ea86d70d683f9d81516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:18:49 +0100 Subject: [PATCH 935/979] feat(room): Add `fn RoomPrivacySettings::get_room_visibility`. --- .../matrix-sdk/src/room/privacy_settings.rs | 32 ++++++++++- crates/matrix-sdk/src/test_utils/mocks.rs | 54 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/room/privacy_settings.rs b/crates/matrix-sdk/src/room/privacy_settings.rs index dde3d170617..b1a436e5120 100644 --- a/crates/matrix-sdk/src/room/privacy_settings.rs +++ b/crates/matrix-sdk/src/room/privacy_settings.rs @@ -1,6 +1,6 @@ use matrix_sdk_base::Room as BaseRoom; use ruma::{ - api::client::state::send_state_event, + api::client::{directory::get_room_visibility, room::Visibility, state::send_state_event}, assign, events::{ room::{ @@ -96,6 +96,16 @@ impl<'a> RoomPrivacySettings<'a> { self.client.send(request).await?; Ok(()) } + + /// Returns the visibility for this room in the room directory. + /// + /// [Public](`Visibility::Public`) rooms are listed in the room directory + /// and can be found using it. + pub async fn get_room_visibility(&'a self) -> Result { + let request = get_room_visibility::v3::Request::new(self.room.room_id().to_owned()); + let response = self.client.send(request).await?; + Ok(response.visibility) + } } #[cfg(all(test, not(target_arch = "wasm32")))] @@ -196,4 +206,24 @@ mod tests { let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await; assert!(ret.is_ok()); } + + #[async_test] + async fn test_get_room_visibility() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + server + .mock_room_send_state() + .for_type(StateEventType::RoomJoinRules) + .ok(event_id!("$a:b.c")) + .mock_once() + .mount() + .await; + + let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await; + assert!(ret.is_ok()); + } } diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 4891a76bdef..dded61f792d 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -28,6 +28,7 @@ use matrix_sdk_test::{ SyncResponseBuilder, }; use ruma::{ + api::client::room::Visibility, directory::PublicRoomsChunk, events::{ room::member::RoomMemberEvent, AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, @@ -565,6 +566,46 @@ impl MatrixMockServer { MockEndpoint { mock, server: &self.server, endpoint: PublicRoomsEndpoint } } + /// Create a prebuilt mock for getting a room's visibility in the room + /// directory. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// use ruma::api::client::room::Visibility; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server + /// .mock_room_directory_get_room_visibility() + /// .ok(Visibility::Public) + /// .mock_once() + /// .mount() + /// .await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let visibility = room + /// .privacy_settings() + /// .get_room_visibility() + /// .await + /// .expect("We should be able to get the room's visibility"); + /// assert_eq!(visibility, Visibility::Public); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_room_directory_get_room_visibility( + &self, + ) -> MockEndpoint<'_, GetRoomVisibilityEndpoint> { + let mock = Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/v3/directory/list/room/.*$")); + MockEndpoint { mock, server: &self.server, endpoint: GetRoomVisibilityEndpoint } + } + /// Create a prebuilt mock for fetching information about key storage /// backups. /// @@ -1820,6 +1861,19 @@ impl<'a> MockEndpoint<'a, PublicRoomsEndpoint> { } } +/// A prebuilt mock for getting the room's visibility in the room directory. +pub struct GetRoomVisibilityEndpoint; + +impl<'a> MockEndpoint<'a, GetRoomVisibilityEndpoint> { + /// Returns an endpoint that get the room's public visibility. + pub fn ok(self, visibility: Visibility) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "visibility": visibility, + }))); + MatrixMock { server: self.server, mock } + } +} + /// A prebuilt mock for `GET room_keys/version`: storage ("backup") of room /// keys. pub struct RoomKeysVersionEndpoint; From d807d71e2227d2e5cfade58650b948796b60be10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:20:40 +0100 Subject: [PATCH 936/979] feat(room): Add `fn RoomPrivacySettings::update_room_visibility`. --- .../matrix-sdk/src/room/privacy_settings.rs | 31 +++++++++++- crates/matrix-sdk/src/test_utils/mocks.rs | 49 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/room/privacy_settings.rs b/crates/matrix-sdk/src/room/privacy_settings.rs index b1a436e5120..4ff946e76fd 100644 --- a/crates/matrix-sdk/src/room/privacy_settings.rs +++ b/crates/matrix-sdk/src/room/privacy_settings.rs @@ -12,7 +12,7 @@ use ruma::{ }, OwnedRoomAliasId, }; - +use ruma::api::client::directory::set_room_visibility; use crate::{Client, Result}; /// A helper to group the methods in [Room](crate::Room) related to the room's @@ -106,6 +106,19 @@ impl<'a> RoomPrivacySettings<'a> { let response = self.client.send(request).await?; Ok(response.visibility) } + + /// Update the visibility for this room in the room directory. + /// + /// [Public](`Visibility::Public`) rooms are listed in the room directory + /// and can be found using it. + pub async fn update_room_visibility(&'a self, visibility: Visibility) -> Result<()> { + let request = + set_room_visibility::v3::Request::new(self.room.room_id().to_owned(), visibility); + + self.client.send(request).await?; + + Ok(()) + } } #[cfg(all(test, not(target_arch = "wasm32")))] @@ -119,7 +132,7 @@ mod tests { }, owned_room_alias_id, room_id, }; - + use ruma::api::client::room::Visibility; use crate::test_utils::mocks::MatrixMockServer; #[async_test] @@ -226,4 +239,18 @@ mod tests { let ret = room.privacy_settings().update_join_rule(JoinRule::Public).await; assert!(ret.is_ok()); } + + #[async_test] + async fn test_update_room_visibility() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + server.mock_room_directory_set_room_visibility().ok().mock_once().mount().await; + + let ret = room.privacy_settings().update_room_visibility(Visibility::Private).await; + assert!(ret.is_ok()); + } } diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index dded61f792d..500faf486ea 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -566,6 +566,44 @@ impl MatrixMockServer { MockEndpoint { mock, server: &self.server, endpoint: PublicRoomsEndpoint } } + /// Create a prebuilt mock for setting a room's visibility in the room + /// directory. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer}; + /// use ruma::api::client::room::Visibility; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server + /// .mock_room_directory_set_room_visibility() + /// .ok() + /// .mock_once() + /// .mount() + /// .await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// room.privacy_settings() + /// .update_room_visibility(Visibility::Private) + /// .await + /// .expect("We should be able to update the room's visibility"); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_room_directory_set_room_visibility( + &self, + ) -> MockEndpoint<'_, SetRoomVisibilityEndpoint> { + let mock = Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/v3/directory/list/room/.*$")); + MockEndpoint { mock, server: &self.server, endpoint: SetRoomVisibilityEndpoint } + } + /// Create a prebuilt mock for getting a room's visibility in the room /// directory. /// @@ -1874,6 +1912,17 @@ impl<'a> MockEndpoint<'a, GetRoomVisibilityEndpoint> { } } +/// A prebuilt mock for setting the room's visibility in the room directory. +pub struct SetRoomVisibilityEndpoint; + +impl<'a> MockEndpoint<'a, SetRoomVisibilityEndpoint> { + /// Returns an endpoint that updates the room's visibility. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + MatrixMock { server: self.server, mock } + } +} + /// A prebuilt mock for `GET room_keys/version`: storage ("backup") of room /// keys. pub struct RoomKeysVersionEndpoint; From d6a74d389db522c6ef2157a3ee0f4ca9f391e9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:24:23 +0100 Subject: [PATCH 937/979] feat(room): Add `fn RoomPrivacySettings::publish_room_alias_in_room_directory`. This also needs some new mocks for resolving room aliases. --- Cargo.lock | 1 + crates/matrix-sdk/Cargo.toml | 1 + crates/matrix-sdk/src/client/mod.rs | 4 +- .../matrix-sdk/src/room/privacy_settings.rs | 93 +++++++++++++++- crates/matrix-sdk/src/test_utils/mocks.rs | 101 +++++++++++++++++- 5 files changed, 192 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 36fb56dec1a..ac2f7d71988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3110,6 +3110,7 @@ dependencies = [ "mime2ext", "once_cell", "openidconnect", + "percent-encoding", "pin-project-lite", "proptest", "rand", diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 9a1b62dbb1e..a62365f63d1 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -94,6 +94,7 @@ matrix-sdk-test = { workspace = true, optional = true } mime = { workspace = true } mime2ext = "0.1.53" once_cell = { workspace = true } +percent-encoding = "2.3.1" pin-project-lite = { workspace = true } rand = { workspace = true , optional = true } ruma = { workspace = true, features = [ diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 5c9396105e3..fdc053d7260 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -1201,7 +1201,7 @@ impl Client { } } - /// Creates a new room alias associated with a room. + /// Adds a new room alias associated with a room to the room directory. pub async fn create_room_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> HttpResult<()> { let request = create_alias::v3::Request::new(alias.to_owned(), room_id.to_owned()); self.send(request).await?; @@ -3163,7 +3163,7 @@ pub(crate) mod tests { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; - server.mock_create_room_alias().ok().expect(1).mount().await; + server.mock_room_directory_create_room_alias().ok().expect(1).mount().await; let ret = client .create_room_alias( diff --git a/crates/matrix-sdk/src/room/privacy_settings.rs b/crates/matrix-sdk/src/room/privacy_settings.rs index 4ff946e76fd..a0fba9c9928 100644 --- a/crates/matrix-sdk/src/room/privacy_settings.rs +++ b/crates/matrix-sdk/src/room/privacy_settings.rs @@ -1,6 +1,10 @@ use matrix_sdk_base::Room as BaseRoom; use ruma::{ - api::client::{directory::get_room_visibility, room::Visibility, state::send_state_event}, + api::client::{ + directory::{get_room_visibility, set_room_visibility}, + room::Visibility, + state::send_state_event, + }, assign, events::{ room::{ @@ -10,9 +14,9 @@ use ruma::{ }, EmptyStateKey, }, - OwnedRoomAliasId, + OwnedRoomAliasId, RoomAliasId, }; -use ruma::api::client::directory::set_room_visibility; + use crate::{Client, Result}; /// A helper to group the methods in [Room](crate::Room) related to the room's @@ -28,6 +32,24 @@ impl<'a> RoomPrivacySettings<'a> { Self { room, client } } + /// Publish a new room alias for this room in the room directory. + /// + /// Returns: + /// - `true` if the room alias didn't exist and it's now published. + /// - `false` if the room alias was already present so it couldn't be + /// published. + pub async fn publish_room_alias_in_room_directory( + &'a self, + alias: &RoomAliasId, + ) -> Result { + if self.client.is_room_alias_available(alias).await? { + self.client.create_room_alias(alias, self.room.room_id()).await?; + return Ok(true); + } + + Ok(false) + } + /// Update the canonical alias of the room. /// /// # Arguments: @@ -123,8 +145,11 @@ impl<'a> RoomPrivacySettings<'a> { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { + use std::ops::Not; + use matrix_sdk_test::async_test; use ruma::{ + api::client::room::Visibility, event_id, events::{ room::{history_visibility::HistoryVisibility, join_rules::JoinRule}, @@ -132,9 +157,69 @@ mod tests { }, owned_room_alias_id, room_id, }; - use ruma::api::client::room::Visibility; + use crate::test_utils::mocks::MatrixMockServer; + #[async_test] + async fn test_publish_room_alias_to_room_directory() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + let room_alias = owned_room_alias_id!("#a:b.c"); + + // First we'd check if the new alias needs to be created + server + .mock_room_directory_resolve_alias() + .for_alias(room_alias.to_string()) + .not_found() + .mock_once() + .mount() + .await; + + // After that, we'd create a new room alias association in the room directory + server.mock_room_directory_create_room_alias().ok().mock_once().mount().await; + + let published = room + .privacy_settings() + .publish_room_alias_in_room_directory(&room_alias) + .await + .expect("we should get a result value, not an error"); + assert!(published); + } + + #[async_test] + async fn test_publish_room_alias_to_room_directory_when_alias_exists() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + let room_alias = owned_room_alias_id!("#a:b.c"); + + // First we'd check if the new alias needs to be created. It does not. + server + .mock_room_directory_resolve_alias() + .for_alias(room_alias.to_string()) + .ok(room_id.as_ref(), Vec::new()) + .mock_once() + .mount() + .await; + + // Since the room alias already exists we won't create it again. + server.mock_room_directory_create_room_alias().ok().never().mount().await; + + let published = room + .privacy_settings() + .publish_room_alias_in_room_directory(&room_alias) + .await + .expect("we should get a result value, not an error"); + assert!(published.not()); + } + #[async_test] async fn test_update_canonical_alias_with_some_value() { let server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 500faf486ea..1ee4d0c7b37 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -27,6 +27,7 @@ use matrix_sdk_test::{ test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, }; +use percent_encoding::{AsciiSet, CONTROLS}; use ruma::{ api::client::room::Visibility, directory::PublicRoomsChunk, @@ -510,14 +511,69 @@ impl MatrixMockServer { } /// Create a prebuilt mock for resolving room aliases. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{owned_room_id, room_alias_id}, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server + /// .mock_room_directory_resolve_alias() + /// .ok("!a:b.c", Vec::new()) + /// .mock_once() + /// .mount() + /// .await; + /// + /// let res = client + /// .resolve_room_alias(room_alias_id!("#a:b.c")) + /// .await + /// .expect("We should be able to resolve the room alias"); + /// assert_eq!(res.room_id, owned_room_id!("!a:b.c")); + /// # anyhow::Ok(()) }); + /// ``` pub fn mock_room_directory_resolve_alias(&self) -> MockEndpoint<'_, ResolveRoomAliasEndpoint> { let mock = Mock::given(method("GET")).and(path_regex(r"/_matrix/client/v3/directory/room/.*")); MockEndpoint { mock, server: &self.server, endpoint: ResolveRoomAliasEndpoint } } - /// Create a prebuilt mock for creating room aliases. - pub fn mock_create_room_alias(&self) -> MockEndpoint<'_, CreateRoomAliasEndpoint> { + /// Create a prebuilt mock for publishing room aliases in the room + /// directory. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{room_alias_id, room_id}, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server + /// .mock_room_directory_create_room_alias() + /// .ok() + /// .mock_once() + /// .mount() + /// .await; + /// + /// client + /// .create_room_alias(room_alias_id!("#a:b.c"), room_id!("!a:b.c")) + /// .await + /// .expect("We should be able to create a room alias"); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_room_directory_create_room_alias( + &self, + ) -> MockEndpoint<'_, CreateRoomAliasEndpoint> { let mock = Mock::given(method("PUT")).and(path_regex(r"/_matrix/client/v3/directory/room/.*")); MockEndpoint { mock, server: &self.server, endpoint: CreateRoomAliasEndpoint } @@ -871,6 +927,29 @@ impl From for AnyRoomBuilder { } } +/// The [path percent-encode set] as defined in the WHATWG URL standard + `/` +/// since we always encode single segments of the path. +/// +/// [path percent-encode set]: https://url.spec.whatwg.org/#path-percent-encode-set +/// +/// Copied from Ruma: +/// https://github.com/ruma/ruma/blob/e4cb409ff3aaa16f31a7fe1e61fee43b2d144f7b/crates/ruma-common/src/percent_encode.rs#L7 +const PATH_PERCENT_ENCODE_SET: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'<') + .add(b'>') + .add(b'?') + .add(b'`') + .add(b'{') + .add(b'}') + .add(b'/'); + +fn percent_encoded_path(path: &str) -> String { + percent_encoding::utf8_percent_encode(path, PATH_PERCENT_ENCODE_SET).to_string() +} + /// A wrapper for a [`Mock`] as well as a [`MockServer`], allowing us to call /// [`Mock::mount`] or [`Mock::mount_as_scoped`] without having to pass the /// [`MockServer`] reference (i.e. call `mount()` instead of `mount(&server)`). @@ -911,6 +990,11 @@ impl MatrixMock<'_> { Self { mock: self.mock.up_to_n_times(1).expect(1), ..self } } + /// Makes sure the endpoint is never reached. + pub fn never(self) -> Self { + Self { mock: self.mock.expect(0), ..self } + } + /// Specify an upper limit to the number of times you would like this /// [`MatrixMock`] to respond to incoming requests that satisfy the /// conditions imposed by your matchers. @@ -1816,6 +1900,19 @@ impl<'a> MockEndpoint<'a, UploadEndpoint> { pub struct ResolveRoomAliasEndpoint; impl<'a> MockEndpoint<'a, ResolveRoomAliasEndpoint> { + /// Sets up the endpoint to only intercept requests for the given room + /// alias. + pub fn for_alias(self, alias: impl Into) -> Self { + let alias = alias.into(); + Self { + mock: self.mock.and(path_regex(format!( + r"^/_matrix/client/v3/directory/room/{}", + percent_encoded_path(&alias) + ))), + ..self + } + } + /// Returns a data endpoint with a resolved room alias. pub fn ok(self, room_id: &str, servers: Vec) -> MatrixMock<'a> { let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ From 588702756b53b82cd1eed3de7ab27ae72e6cf7e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:27:58 +0100 Subject: [PATCH 938/979] feat(room): Add `fn RoomPrivacySettings::remove_room_alias_in_room_directory`. --- crates/matrix-sdk/src/client/mod.rs | 9 +- .../matrix-sdk/src/room/privacy_settings.rs | 84 ++++++++++++++++++- crates/matrix-sdk/src/test_utils/mocks.rs | 46 ++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index fdc053d7260..f2daad94aee 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -41,7 +41,7 @@ use ruma::{ api::{ client::{ account::whoami, - alias::{create_alias, get_alias}, + alias::{create_alias, delete_alias, get_alias}, device::{delete_devices, get_devices, update_device}, directory::{get_public_rooms, get_public_rooms_filtered}, discovery::{ @@ -1208,6 +1208,13 @@ impl Client { Ok(()) } + /// Removes a room alias from the room directory. + pub async fn remove_room_alias(&self, alias: &RoomAliasId) -> HttpResult<()> { + let request = delete_alias::v3::Request::new(alias.to_owned()); + self.send(request).await?; + Ok(()) + } + /// Update the homeserver from the login response well-known if needed. /// /// # Arguments diff --git a/crates/matrix-sdk/src/room/privacy_settings.rs b/crates/matrix-sdk/src/room/privacy_settings.rs index a0fba9c9928..8964bfd35d3 100644 --- a/crates/matrix-sdk/src/room/privacy_settings.rs +++ b/crates/matrix-sdk/src/room/privacy_settings.rs @@ -50,6 +50,24 @@ impl<'a> RoomPrivacySettings<'a> { Ok(false) } + /// Remove an existing room alias for this room in the room directory. + /// + /// Returns: + /// - `true` if the room alias was present and it's now removed from the + /// room directory. + /// - `false` if the room alias didn't exist so it couldn't be removed. + pub async fn remove_room_alias_from_room_directory( + &'a self, + alias: &RoomAliasId, + ) -> Result { + if self.client.resolve_room_alias(alias).await.is_ok() { + self.client.remove_room_alias(alias).await?; + return Ok(true); + } + + Ok(false) + } + /// Update the canonical alias of the room. /// /// # Arguments: @@ -147,7 +165,7 @@ impl<'a> RoomPrivacySettings<'a> { mod tests { use std::ops::Not; - use matrix_sdk_test::async_test; + use matrix_sdk_test::{async_test, JoinedRoomBuilder, StateTestEvent}; use ruma::{ api::client::room::Visibility, event_id, @@ -220,6 +238,70 @@ mod tests { assert!(published.not()); } + #[async_test] + async fn test_remove_room_alias() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let joined_room_builder = + JoinedRoomBuilder::new(room_id).add_state_event(StateTestEvent::Alias); + let room = server.sync_room(&client, joined_room_builder).await; + + let room_alias = owned_room_alias_id!("#a:b.c"); + + // First we'd check if the alias exists + server + .mock_room_directory_resolve_alias() + .for_alias(room_alias.to_string()) + .ok(room_id.as_ref(), Vec::new()) + .mock_once() + .mount() + .await; + + // After that we'd remove it + server.mock_room_directory_remove_room_alias().ok().mock_once().mount().await; + + let removed = room + .privacy_settings() + .remove_room_alias_from_room_directory(&room_alias) + .await + .expect("we should get a result value, not an error"); + assert!(removed); + } + + #[async_test] + async fn test_remove_room_alias_if_it_does_not_exist() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let joined_room_builder = + JoinedRoomBuilder::new(room_id).add_state_event(StateTestEvent::Alias); + let room = server.sync_room(&client, joined_room_builder).await; + + let room_alias = owned_room_alias_id!("#a:b.c"); + + // First we'd check if the alias exists. It doesn't. + server + .mock_room_directory_resolve_alias() + .for_alias(room_alias.to_string()) + .not_found() + .mock_once() + .mount() + .await; + + // So we can't remove it after the check. + server.mock_room_directory_remove_room_alias().ok().never().mount().await; + + let removed = room + .privacy_settings() + .remove_room_alias_from_room_directory(&room_alias) + .await + .expect("we should get a result value, not an error"); + assert!(removed.not()); + } + #[async_test] async fn test_update_canonical_alias_with_some_value() { let server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 1ee4d0c7b37..45557784a58 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -579,6 +579,41 @@ impl MatrixMockServer { MockEndpoint { mock, server: &self.server, endpoint: CreateRoomAliasEndpoint } } + /// Create a prebuilt mock for removing room aliases from the room + /// directory. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::room_alias_id, test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server + /// .mock_room_directory_remove_room_alias() + /// .ok() + /// .mock_once() + /// .mount() + /// .await; + /// + /// client + /// .remove_room_alias(room_alias_id!("#a:b.c")) + /// .await + /// .expect("We should be able to remove the room alias"); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_room_directory_remove_room_alias( + &self, + ) -> MockEndpoint<'_, RemoveRoomAliasEndpoint> { + let mock = + Mock::given(method("DELETE")).and(path_regex(r"/_matrix/client/v3/directory/room/.*")); + MockEndpoint { mock, server: &self.server, endpoint: RemoveRoomAliasEndpoint } + } + /// Create a prebuilt mock for listing public rooms. /// /// # Examples @@ -1943,6 +1978,17 @@ impl<'a> MockEndpoint<'a, CreateRoomAliasEndpoint> { } } +/// A prebuilt mock for removing a room alias. +pub struct RemoveRoomAliasEndpoint; + +impl<'a> MockEndpoint<'a, RemoveRoomAliasEndpoint> { + /// Returns a data endpoint for removing a room alias. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + MatrixMock { server: self.server, mock } + } +} + /// A prebuilt mock for paginating the public room list. pub struct PublicRoomsEndpoint; From d9c1188f87903f6a6cfb5e507f5f55552c99638a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:28:20 +0100 Subject: [PATCH 939/979] test(room): Add integration tests for publishing and removing room aliases --- .../src/tests.rs | 1 + .../src/tests/room_privacy.rs | 242 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 testing/matrix-sdk-integration-testing/src/tests/room_privacy.rs diff --git a/testing/matrix-sdk-integration-testing/src/tests.rs b/testing/matrix-sdk-integration-testing/src/tests.rs index cd85e97d604..67f77483210 100644 --- a/testing/matrix-sdk-integration-testing/src/tests.rs +++ b/testing/matrix-sdk-integration-testing/src/tests.rs @@ -6,5 +6,6 @@ mod redaction; mod repeated_join; mod room; mod room_directory_search; +mod room_privacy; mod sliding_sync; mod timeline; diff --git a/testing/matrix-sdk-integration-testing/src/tests/room_privacy.rs b/testing/matrix-sdk-integration-testing/src/tests/room_privacy.rs new file mode 100644 index 00000000000..6bce878c555 --- /dev/null +++ b/testing/matrix-sdk-integration-testing/src/tests/room_privacy.rs @@ -0,0 +1,242 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::Duration; + +use assert_matches2::assert_matches; +use assign::assign; +use matrix_sdk::{ + config::SyncSettings, + ruma::{ + api::client::{ + directory::get_public_rooms_filtered, + room::{create_room::v3::Request as CreateRoomRequest, Visibility}, + }, + directory::Filter, + events::room::{ + canonical_alias::{InitialRoomCanonicalAliasEvent, RoomCanonicalAliasEventContent}, + history_visibility::{ + HistoryVisibility, InitialRoomHistoryVisibilityEvent, + RoomHistoryVisibilityEventContent, + }, + }, + serde::Raw, + RoomAliasId, + }, +}; +use matrix_sdk_base::ruma::events::room::canonical_alias::SyncRoomCanonicalAliasEvent; +use rand::random; +use tokio::sync::mpsc::unbounded_channel; + +use crate::helpers::TestClientBuilder; + +#[tokio::test] +async fn test_publishing_room_alias() -> anyhow::Result<()> { + let client = TestClientBuilder::new("alice").build().await?; + let server_name = client.user_id().expect("A user id should exist").server_name(); + + let sync_handle = tokio::task::spawn({ + let client = client.clone(); + async move { + client.sync(SyncSettings::default()).await.expect("Sync should not fail"); + } + }); + + // The room can only be visible in the public room directory later if its join + // rule is one of [public, knock, knock_restricted] or its history + // visibility is `world_readable`. Let's use this last option. + let room_history_visibility = InitialRoomHistoryVisibilityEvent::new( + RoomHistoryVisibilityEventContent::new(HistoryVisibility::WorldReadable), + ) + .to_raw_any(); + let room_id = client + .create_room(assign!(CreateRoomRequest::new(), { + initial_state: vec![ + room_history_visibility, + ] + })) + .await? + .room_id() + .to_owned(); + + // Wait for the room to be synced + let room = client.await_room_remote_echo(&room_id).await; + + // Initial checks for the room's state + let room_visibility = room.privacy_settings().get_room_visibility().await?; + assert_matches!(room_visibility, Visibility::Private); + + let canonical_alias = room.canonical_alias(); + assert!(canonical_alias.is_none()); + + let alternative_aliases = room.alt_aliases(); + assert!(alternative_aliases.is_empty()); + + // We'll add a room alias to it + let random_id: u128 = random(); + let raw_room_alias = format!("#a-room-alias-{random_id}:{server_name}"); + let room_alias = RoomAliasId::parse(raw_room_alias).expect("The room alias should be valid"); + + // We publish the room alias + let published = + room.privacy_settings().publish_room_alias_in_room_directory(&room_alias).await?; + assert!(published); + + // We can't publish it again + let published = + room.privacy_settings().publish_room_alias_in_room_directory(&room_alias).await?; + assert!(!published); + + // We can publish an alternative alias too + let alt_alias = RoomAliasId::parse(format!("#alt-alias-{random_id}:{server_name}")) + .expect("The alt room alias should be valid"); + let published = + room.privacy_settings().publish_room_alias_in_room_directory(&alt_alias).await?; + assert!(published); + + // Since we only published the room alias, the canonical alias is not set yet + let canonical_alias = room.canonical_alias(); + assert!(canonical_alias.is_none()); + + // We update the canonical alias now + room.privacy_settings() + .update_canonical_alias(Some(room_alias.clone()), vec![alt_alias.clone()]) + .await?; + + // Wait until we receive the canonical alias event through sync + let (tx, mut rx) = unbounded_channel(); + let handle = room.add_event_handler(move |_: Raw| { + let _ = tx.send(()); + async {} + }); + let _ = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await?; + client.remove_event_handler(handle); + + // And we can check it actually changed the aliases + let canonical_alias = room.canonical_alias(); + assert!(canonical_alias.is_some()); + assert_eq!(canonical_alias.unwrap(), room_alias); + + let alternative_aliases = room.alt_aliases(); + assert_eq!(alternative_aliases, vec![alt_alias]); + + // Since the room is still not public, the room directory can't find it + let public_rooms_filter = assign!(Filter::new(), { + generic_search_term: Some(room_alias.to_string()), + }); + let public_rooms_request = assign!(get_public_rooms_filtered::v3::Request::new(), { + filter: public_rooms_filter, + }); + let results = client.public_rooms_filtered(public_rooms_request.clone()).await?.chunk; + assert!(results.is_empty()); + + // We can set the room as visible now in the public room directory + room.privacy_settings().update_room_visibility(Visibility::Public).await?; + + // And confirm it's public + let room_visibility = room.privacy_settings().get_room_visibility().await?; + assert_matches!(room_visibility, Visibility::Public); + + // We can check again the public room directory and we should have some results + let results = client.public_rooms_filtered(public_rooms_request).await?.chunk; + assert_eq!(results.len(), 1); + + sync_handle.abort(); + + Ok(()) +} + +#[tokio::test] +async fn test_removing_published_room_alias() -> anyhow::Result<()> { + let client = TestClientBuilder::new("alice").build().await?; + let server_name = client.user_id().expect("A user id should exist").server_name(); + + let sync_handle = tokio::task::spawn({ + let client = client.clone(); + async move { + client.sync(SyncSettings::default()).await.expect("Sync should not fail"); + } + }); + + // We'll add a room alias to it + let random_id: u128 = random(); + let local_part_room_alias = format!("a-room-alias-{}", random_id); + let raw_room_alias = format!("#{local_part_room_alias}:{server_name}"); + let room_alias = RoomAliasId::parse(raw_room_alias).expect("The room alias should be valid"); + + // The room can only be visible in the public room directory later if its join + // rule is one of [public, knock, knock_restricted] or its history + // visibility is `world_readable`. Let's use this last option. + // This room will be created with a room alias and being visible in the public + // room directory. + let room_history_visibility = InitialRoomHistoryVisibilityEvent::new( + RoomHistoryVisibilityEventContent::new(HistoryVisibility::WorldReadable), + ) + .to_raw_any(); + let canonical_alias = InitialRoomCanonicalAliasEvent::new( + assign!(RoomCanonicalAliasEventContent::new(), { alias: Some(room_alias.clone()) }), + ) + .to_raw_any(); + let room_id = client + .create_room(assign!(CreateRoomRequest::new(), { + room_alias_name: Some(local_part_room_alias), + initial_state: vec![ + room_history_visibility, + canonical_alias, + ], + visibility: Visibility::Public, + })) + .await? + .room_id() + .to_owned(); + + // Wait for the room to be synced + let room = client.await_room_remote_echo(&room_id).await; + + // Initial checks for the room's state + let room_visibility = room.privacy_settings().get_room_visibility().await?; + assert_matches!(room_visibility, Visibility::Public); + + let alternative_aliases = room.alt_aliases(); + assert!(alternative_aliases.is_empty()); + + // We can check the room is published + let public_rooms_filter = assign!(Filter::new(), { + generic_search_term: Some(room_alias.to_string()), + }); + let public_rooms_request = assign!(get_public_rooms_filtered::v3::Request::new(), { + filter: public_rooms_filter, + }); + let results = client.public_rooms_filtered(public_rooms_request.clone()).await?.chunk; + assert_eq!(results.len(), 1); + + // We remove the room alias + let removed = + room.privacy_settings().remove_room_alias_from_room_directory(&room_alias).await?; + assert!(removed); + + // We can't remove it again + let removed = + room.privacy_settings().remove_room_alias_from_room_directory(&room_alias).await?; + assert!(!removed); + + // If we check the public room list again using the room alias as the search + // term, we don't have any results now + let results = client.public_rooms_filtered(public_rooms_request.clone()).await?.chunk; + assert!(results.is_empty()); + + sync_handle.abort(); + + Ok(()) +} From 5548f383932ab49546f8b5a365233e23597838b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 9 Jan 2025 16:45:45 +0100 Subject: [PATCH 940/979] feat(ffi): Add FFI bindings for the new room privacy settings feature. --- bindings/matrix-sdk-ffi/src/client.rs | 25 ++-- bindings/matrix-sdk-ffi/src/error.rs | 6 + bindings/matrix-sdk-ffi/src/room.rs | 166 ++++++++++++++++++++++- bindings/matrix-sdk-ffi/src/room_info.rs | 8 +- bindings/matrix-sdk-ffi/src/room_list.rs | 2 +- 5 files changed, 190 insertions(+), 17 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 54e6ac79bda..af8ddb4389e 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1152,17 +1152,6 @@ impl Client { let alias = RoomAliasId::parse(alias)?; self.inner.is_room_alias_available(&alias).await.map_err(Into::into) } - - /// Creates a new room alias associated with the provided room id. - pub async fn create_room_alias( - &self, - room_alias: String, - room_id: String, - ) -> Result<(), ClientError> { - let room_alias = RoomAliasId::parse(room_alias)?; - let room_id = RoomId::parse(room_id)?; - self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into) - } } #[matrix_sdk_ffi_macros::export(callback_interface)] @@ -1462,6 +1451,9 @@ pub enum RoomVisibility { /// Indicates that the room will not be shown in the published room list. Private, + + /// A custom value that's not present in the spec. + Custom { value: String }, } impl From for Visibility { @@ -1469,6 +1461,17 @@ impl From for Visibility { match value { RoomVisibility::Public => Self::Public, RoomVisibility::Private => Self::Private, + RoomVisibility::Custom { value } => value.as_str().into(), + } + } +} + +impl From for RoomVisibility { + fn from(value: Visibility) -> Self { + match value { + Visibility::Public => Self::Public, + Visibility::Private => Self::Private, + _ => Self::Custom { value: value.as_str().to_owned() }, } } } diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index 9d793b07020..5ce1bf48e33 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -155,6 +155,12 @@ impl From for ClientError { } } +impl From for ClientError { + fn from(_: NotYetImplemented) -> Self { + Self::new("This functionality is not implemented yet.") + } +} + /// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple /// String. /// diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index e76395b381e..62f7b678021 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -20,7 +20,8 @@ use ruma::{ call::notify, room::{ avatar::ImageInfo as RumaAvatarImageInfo, - message::RoomMessageEventContentWithoutRelation, + history_visibility::HistoryVisibility as RumaHistoryVisibility, + join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation, power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource, }, AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType, @@ -33,7 +34,8 @@ use tracing::error; use super::RUNTIME; use crate::{ chunk_iterator::ChunkIterator, - error::{ClientError, MediaInfoError, RoomError}, + client::{JoinRule, RoomVisibility}, + error::{ClientError, MediaInfoError, NotYetImplemented, RoomError}, event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType}, identity_status_change::IdentityStatusChange, room_info::RoomInfo, @@ -345,7 +347,7 @@ impl Room { } pub async fn room_info(&self) -> Result { - Ok(RoomInfo::new(&self.inner).await?) + RoomInfo::new(&self.inner).await } pub fn subscribe_to_room_info_updates( @@ -941,6 +943,105 @@ impl Room { let (cache, _drop_guards) = self.inner.event_cache().await?; Ok(cache.debug_string().await) } + + /// Update the canonical alias of the room. + /// + /// Note that publishing the alias in the room directory is done separately. + pub async fn update_canonical_alias( + &self, + alias: Option, + alt_aliases: Vec, + ) -> Result<(), ClientError> { + let new_alias = alias.map(TryInto::try_into).transpose()?; + let new_alt_aliases = + alt_aliases.into_iter().map(RoomAliasId::parse).collect::>()?; + self.inner + .privacy_settings() + .update_canonical_alias(new_alias, new_alt_aliases) + .await + .map_err(Into::into) + } + + /// Publish a new room alias for this room in the room directory. + /// + /// Returns: + /// - `true` if the room alias didn't exist and it's now published. + /// - `false` if the room alias was already present so it couldn't be + /// published. + pub async fn publish_room_alias_in_room_directory( + &self, + alias: String, + ) -> Result { + let new_alias = RoomAliasId::parse(alias)?; + self.inner + .privacy_settings() + .publish_room_alias_in_room_directory(&new_alias) + .await + .map_err(Into::into) + } + + /// Remove an existing room alias for this room in the room directory. + /// + /// Returns: + /// - `true` if the room alias was present and it's now removed from the + /// room directory. + /// - `false` if the room alias didn't exist so it couldn't be removed. + pub async fn remove_room_alias_from_room_directory( + &self, + alias: String, + ) -> Result { + let alias = RoomAliasId::parse(alias)?; + self.inner + .privacy_settings() + .remove_room_alias_from_room_directory(&alias) + .await + .map_err(Into::into) + } + + /// Enable End-to-end encryption in this room. + pub async fn enable_encryption(&self) -> Result<(), ClientError> { + self.inner.enable_encryption().await.map_err(Into::into) + } + + /// Update room history visibility for this room. + pub async fn update_history_visibility( + &self, + visibility: RoomHistoryVisibility, + ) -> Result<(), ClientError> { + let visibility: RumaHistoryVisibility = visibility.try_into()?; + self.inner + .privacy_settings() + .update_room_history_visibility(visibility) + .await + .map_err(Into::into) + } + + /// Update the join rule for this room. + pub async fn update_join_rules(&self, new_rule: JoinRule) -> Result<(), ClientError> { + let new_rule: RumaJoinRule = new_rule.try_into()?; + self.inner.privacy_settings().update_join_rule(new_rule).await.map_err(Into::into) + } + + /// Update the room's visibility in the room directory. + pub async fn update_room_visibility( + &self, + visibility: RoomVisibility, + ) -> Result<(), ClientError> { + self.inner + .privacy_settings() + .update_room_visibility(visibility.into()) + .await + .map_err(Into::into) + } + + /// Returns the visibility for this room in the room directory. + /// + /// [Public](`RoomVisibility::Public`) rooms are listed in the room + /// directory and can be found using it. + pub async fn get_room_visibility(&self) -> Result { + let visibility = self.inner.privacy_settings().get_room_visibility().await?; + Ok(visibility.into()) + } } impl From for KnockRequest { @@ -1254,3 +1355,62 @@ impl TryFrom for SdkComposerDraftType { Ok(draft_type) } } + +#[derive(Debug, Clone, uniffi::Enum)] +pub enum RoomHistoryVisibility { + /// Previous events are accessible to newly joined members from the point + /// they were invited onwards. + /// + /// Events stop being accessible when the member's state changes to + /// something other than *invite* or *join*. + Invited, + + /// Previous events are accessible to newly joined members from the point + /// they joined the room onwards. + /// Events stop being accessible when the member's state changes to + /// something other than *join*. + Joined, + + /// Previous events are always accessible to newly joined members. + /// + /// All events in the room are accessible, even those sent when the member + /// was not a part of the room. + Shared, + + /// All events while this is the `HistoryVisibility` value may be shared by + /// any participating homeserver with anyone, regardless of whether they + /// have ever joined the room. + WorldReadable, + + /// A custom visibility value. + Custom { value: String }, +} + +impl TryFrom for RoomHistoryVisibility { + type Error = NotYetImplemented; + fn try_from(value: RumaHistoryVisibility) -> Result { + match value { + RumaHistoryVisibility::Invited => Ok(RoomHistoryVisibility::Invited), + RumaHistoryVisibility::Shared => Ok(RoomHistoryVisibility::Shared), + RumaHistoryVisibility::WorldReadable => Ok(RoomHistoryVisibility::WorldReadable), + RumaHistoryVisibility::Joined => Ok(RoomHistoryVisibility::Joined), + RumaHistoryVisibility::_Custom(_) => { + Ok(RoomHistoryVisibility::Custom { value: value.to_string() }) + } + _ => Err(NotYetImplemented), + } + } +} + +impl TryFrom for RumaHistoryVisibility { + type Error = NotYetImplemented; + fn try_from(value: RoomHistoryVisibility) -> Result { + match value { + RoomHistoryVisibility::Invited => Ok(RumaHistoryVisibility::Invited), + RoomHistoryVisibility::Shared => Ok(RumaHistoryVisibility::Shared), + RoomHistoryVisibility::Joined => Ok(RumaHistoryVisibility::Joined), + RoomHistoryVisibility::WorldReadable => Ok(RumaHistoryVisibility::WorldReadable), + RoomHistoryVisibility::Custom { .. } => Err(NotYetImplemented), + } + } +} diff --git a/bindings/matrix-sdk-ffi/src/room_info.rs b/bindings/matrix-sdk-ffi/src/room_info.rs index 1b0f27e7114..1ed6a48ddb9 100644 --- a/bindings/matrix-sdk-ffi/src/room_info.rs +++ b/bindings/matrix-sdk-ffi/src/room_info.rs @@ -5,8 +5,9 @@ use tracing::warn; use crate::{ client::JoinRule, + error::ClientError, notification_settings::RoomNotificationMode, - room::{Membership, RoomHero}, + room::{Membership, RoomHero, RoomHistoryVisibility}, room_member::RoomMember, }; @@ -60,10 +61,12 @@ pub struct RoomInfo { pinned_event_ids: Vec, /// The join rule for this room, if known. join_rule: Option, + /// The history visibility for this room, if known. + history_visibility: RoomHistoryVisibility, } impl RoomInfo { - pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result { + pub(crate) async fn new(room: &matrix_sdk::Room) -> Result { let unread_notification_counts = room.unread_notification_counts(); let power_levels_map = room.users_with_power_levels().await; @@ -128,6 +131,7 @@ impl RoomInfo { num_unread_mentions: room.num_unread_mentions(), pinned_event_ids, join_rule: join_rule.ok(), + history_visibility: room.history_visibility_or_default().try_into()?, }) } } diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 1c193b0ac8d..09e49da741d 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -566,7 +566,7 @@ impl RoomListItem { } async fn room_info(&self) -> Result { - Ok(RoomInfo::new(self.inner.inner_room()).await?) + RoomInfo::new(self.inner.inner_room()).await } /// The room's current membership state. From 5af326b36e7a9276c7596aa910d602d51fdbf889 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 9 Jan 2025 17:37:49 +0100 Subject: [PATCH 941/979] fix(event cache): keep the previous-batch token when we haven't enabled storage --- crates/matrix-sdk/src/event_cache/room/mod.rs | 13 +++- .../tests/integration/event_cache.rs | 78 +++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index e7f95058090..08b18b69aa4 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -320,10 +320,15 @@ impl RoomEventCacheInner { // Add all the events to the backend. trace!("adding new events"); - // Only keep the previous-batch token if we have a limited timeline; otherwise, - // we know about all the events, and we don't need to back-paginate, - // so we wouldn't make use of the given previous-batch token. - let prev_batch = if timeline.limited { timeline.prev_batch } else { None }; + // If we have storage, only keep the previous-batch token if we have a limited + // timeline. Otherwise, we know about all the events, and we don't need to + // back-paginate, so we wouldn't make use of the given previous-batch token. + // + // If we don't have storage, even if the timeline isn't limited, we may not have + // saved the previous events in any cache, so we should always be + // able to retrieve those. + let prev_batch = + if has_storage && !timeline.limited { None } else { timeline.prev_batch }; let mut state = self.state.write().await; self.append_events_locked( diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 602c3732838..df1dc4fba9c 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -978,6 +978,84 @@ async fn test_limited_timeline_with_storage() { assert!(subscriber.is_empty()); } +#[async_test] +async fn test_limited_timeline_without_storage() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + event_cache.subscribe().unwrap(); + + let room_id = room_id!("!galette:saucisse.bzh"); + let room = server.sync_joined_room(&client, room_id).await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh")); + + // Get a sync for a non-limited timeline, but with a prev-batch token. + // + // When we don't have storage, we should still keep this prev-batch around, + // because the previous sync events may not have been saved to disk in the + // first place. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("hey yo")) + .set_timeline_prev_batch("prev-batch".to_owned()), + ) + .await; + + let (initial_events, mut subscriber) = room_event_cache.subscribe().await.unwrap(); + + // This is racy: either the sync has been handled, or it hasn't yet. + if initial_events.is_empty() { + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() + ); + assert_eq!(diffs.len(), 1); + + assert_let!(VectorDiff::Append { values: events } = &diffs[0]); + assert_eq!(events.len(), 1); + assert_event_matches_msg(&events[0], "hey yo"); + } else { + assert_eq!(initial_events.len(), 1); + assert_event_matches_msg(&initial_events[0], "hey yo"); + } + + // Back-pagination should thus work. The real assertion is in the "mock_once" + // call, which checks this endpoint is called once. + server + .mock_room_messages() + .from("prev-batch") + .ok( + "start-token-unused2".to_owned(), + None, + vec![f.text_msg("oh well").event_id(event_id!("$1"))], + Vec::new(), + ) + .mock_once() + .mount() + .await; + + // We run back-pagination with success. + room_event_cache.pagination().run_backwards(20, once).await.unwrap(); + + // And we get the back-paginated event. + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() + ); + assert_eq!(diffs.len(), 1); + + assert_let!(VectorDiff::Insert { index: 0, value: event } = &diffs[0]); + assert_event_matches_msg(event, "oh well"); + + // That's all, folks! + assert!(subscriber.is_empty()); +} + #[async_test] async fn test_backpaginate_with_no_initial_events() { let server = MatrixMockServer::new().await; From c4563564247c50300b5d299e19c480a3caf0e0e2 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 12:46:00 +0100 Subject: [PATCH 942/979] tests: add a helper to create a room /messages response --- .../tests/integration/timeline/edit.rs | 9 +- crates/matrix-sdk/src/test_utils/mocks.rs | 54 +++++-- .../tests/integration/event_cache.rs | 144 ++++++------------ crates/matrix-sdk/tests/integration/widget.rs | 7 +- 4 files changed, 95 insertions(+), 119 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index d07af71056f..ef54ac1e989 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -22,7 +22,10 @@ use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ config::SyncSettings, room::edit::EditedContent, - test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, + test_utils::{ + logged_in_client_with_server, + mocks::{MatrixMockServer, RoomMessagesResponse}, + }, Client, }; use matrix_sdk_test::{ @@ -48,7 +51,7 @@ use ruma::{ MessageType, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, TextMessageEventContent, }, - AnyMessageLikeEventContent, AnyStateEvent, AnyTimelineEvent, + AnyMessageLikeEventContent, AnyTimelineEvent, }, owned_event_id, room_id, serde::Raw, @@ -867,7 +870,7 @@ impl PendingEditHelper { async fn handle_backpagination(&mut self, events: Vec>, batch_size: u16) { self.server .mock_room_messages() - .ok("123".to_owned(), Some("yolo".to_owned()), events, Vec::>::new()) + .ok(RoomMessagesResponse::default().end_token("yolo").events(events)) .mock_once() .mount() .await; diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 45557784a58..5682ffafbcc 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -1895,23 +1895,55 @@ impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { /// /// Note: pass `chunk` in the correct order: topological for forward /// pagination, reverse topological for backwards pagination. - pub fn ok( - self, - start: String, - end: Option, - chunk: Vec>>, - state: Vec>, - ) -> MatrixMock<'a> { + pub fn ok(self, response: RoomMessagesResponse) -> MatrixMock<'a> { let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "start": start, - "end": end, - "chunk": chunk.into_iter().map(|ev| ev.into()).collect::>(), - "state": state, + "start": response.start, + "end": response.end, + "chunk": response.chunk, + "state": response.state, }))); MatrixMock { server: self.server, mock } } } +/// A response to a [`RoomMessagesEndpoint`] query. +pub struct RoomMessagesResponse { + /// The start token for this /messages query. + pub start: String, + /// The end token for this /messages query (previous batch for back + /// paginations, next batch for forward paginations). + pub end: Option, + /// The set of timeline events returned by this query. + pub chunk: Vec>, + /// The set of state events returned by this query. + pub state: Vec>, +} + +impl RoomMessagesResponse { + /// Fill the events returned as part of this response. + pub fn events(mut self, chunk: Vec>>) -> Self { + self.chunk = chunk.into_iter().map(Into::into).collect(); + self + } + + /// Fill the end token. + pub fn end_token(mut self, token: impl Into) -> Self { + self.end = Some(token.into()); + self + } +} + +impl Default for RoomMessagesResponse { + fn default() -> Self { + Self { + start: "start-token-unused".to_owned(), + end: Default::default(), + chunk: Default::default(), + state: Default::default(), + } + } +} + /// A prebuilt mock for uploading media. pub struct UploadEndpoint; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index df1dc4fba9c..ca0700d3ffe 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -14,12 +14,15 @@ use matrix_sdk::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, PaginationToken, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, }, - test_utils::{assert_event_matches_msg, mocks::MatrixMockServer}, + test_utils::{ + assert_event_matches_msg, + mocks::{MatrixMockServer, RoomMessagesResponse}, + }, }; use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, }; -use ruma::{event_id, events::AnyTimelineEvent, room_id, serde::Raw, user_id}; +use ruma::{event_id, room_id, user_id}; use serde_json::json; use tokio::{spawn, sync::broadcast, time::sleep}; use wiremock::ResponseTemplate; @@ -259,15 +262,10 @@ async fn test_backpaginate_once() { server .mock_room_messages() .from("prev_batch") - .ok( - "start-token-unused".to_owned(), - None, - vec![ - f.text_msg("world").event_id(event_id!("$2")), - f.text_msg("hello").event_id(event_id!("$3")), - ], - Vec::new(), - ) + .ok(RoomMessagesResponse::default().events(vec![ + f.text_msg("world").event_id(event_id!("$2")), + f.text_msg("hello").event_id(event_id!("$3")), + ])) .mock_once() .mount() .await; @@ -352,15 +350,10 @@ async fn test_backpaginate_many_times_with_many_iterations() { server .mock_room_messages() .from("prev_batch") - .ok( - "start-token-unused".to_owned(), - Some("prev_batch2".to_owned()), - vec![ - f.text_msg("world").event_id(event_id!("$2")), - f.text_msg("hello").event_id(event_id!("$3")), - ], - Vec::new(), - ) + .ok(RoomMessagesResponse::default().end_token("prev_batch2").events(vec![ + f.text_msg("world").event_id(event_id!("$2")), + f.text_msg("hello").event_id(event_id!("$3")), + ])) .mock_once() .mount() .await; @@ -369,12 +362,8 @@ async fn test_backpaginate_many_times_with_many_iterations() { server .mock_room_messages() .from("prev_batch2") - .ok( - "start-token-unused".to_owned(), - None, - vec![f.text_msg("oh well").event_id(event_id!("$4"))], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("oh well").event_id(event_id!("$4"))])) .mock_once() .mount() .await; @@ -499,15 +488,10 @@ async fn test_backpaginate_many_times_with_one_iteration() { server .mock_room_messages() .from("prev_batch") - .ok( - "start-token-unused1".to_owned(), - Some("prev_batch2".to_owned()), - vec![ - f.text_msg("world").event_id(event_id!("$2")), - f.text_msg("hello").event_id(event_id!("$3")), - ], - Vec::new(), - ) + .ok(RoomMessagesResponse::default().end_token("prev_batch2").events(vec![ + f.text_msg("world").event_id(event_id!("$2")), + f.text_msg("hello").event_id(event_id!("$3")), + ])) .mock_once() .mount() .await; @@ -516,12 +500,8 @@ async fn test_backpaginate_many_times_with_one_iteration() { server .mock_room_messages() .from("prev_batch2") - .ok( - "start-token-unused2".to_owned(), - None, - vec![f.text_msg("oh well").event_id(event_id!("$4"))], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("oh well").event_id(event_id!("$4"))])) .mock_once() .mount() .await; @@ -680,12 +660,9 @@ async fn test_reset_while_backpaginating() { server .mock_room_messages() .from("second_backpagination") - .ok( - "start-token-unused".to_owned(), - Some("third_backpagination".to_owned()), - vec![f.text_msg("finally!").into_raw_timeline()], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .end_token("third_backpagination") + .events(vec![f.text_msg("finally!").into_raw_timeline()])) .mock_once() .mount() .await; @@ -794,12 +771,8 @@ async fn test_backpaginating_without_token() { server .mock_room_messages() - .ok( - "start-token-unused".to_owned(), - None, - vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()])) .mock_once() .mount() .await; @@ -856,12 +829,8 @@ async fn test_limited_timeline_resets_pagination() { server .mock_room_messages() - .ok( - "start-token-unused".to_owned(), - None, - vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()])) .mock_once() .mount() .await; @@ -1030,12 +999,8 @@ async fn test_limited_timeline_without_storage() { server .mock_room_messages() .from("prev-batch") - .ok( - "start-token-unused2".to_owned(), - None, - vec![f.text_msg("oh well").event_id(event_id!("$1"))], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("oh well").event_id(event_id!("$1"))])) .mock_once() .mount() .await; @@ -1122,12 +1087,8 @@ async fn test_backpaginate_with_no_initial_events() { server .mock_room_messages() .from("prev_batch") - .ok( - "start-token-unused2".to_owned(), - None, - vec![f.text_msg("oh well").event_id(event_id!("$1"))], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("oh well").event_id(event_id!("$1"))])) .mock_once() .mount() .await; @@ -1199,12 +1160,7 @@ async fn test_backpaginate_replace_empty_gap() { // The first back-pagination will return a previous-batch token, but no events. server .mock_room_messages() - .ok( - "start-token-unused1".to_owned(), - Some("prev_batch".to_owned()), - Vec::>::new(), - Vec::new(), - ) + .ok(RoomMessagesResponse::default().end_token("prev_batch")) .mock_once() .mount() .await; @@ -1213,12 +1169,8 @@ async fn test_backpaginate_replace_empty_gap() { server .mock_room_messages() .from("prev_batch") - .ok( - "start-token-unused2".to_owned(), - None, - vec![f.text_msg("hello").event_id(event_id!("$1"))], - Vec::new(), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("hello").event_id(event_id!("$1"))])) .mock_once() .mount() .await; @@ -1280,12 +1232,7 @@ async fn test_no_gap_stored_after_deduplicated_sync() { drop(events); // Backpagination will return nothing. - server - .mock_room_messages() - .ok("start-token-unused1".to_owned(), None, Vec::>::new(), Vec::new()) - .mock_once() - .mount() - .await; + server.mock_room_messages().ok(RoomMessagesResponse::default()).mock_once().mount().await; let pagination = room_event_cache.pagination(); @@ -1395,7 +1342,7 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { server .mock_room_messages() .from("prev-batch2") - .ok("start-token-unused".to_owned(), None, Vec::>::new(), Vec::new()) + .ok(RoomMessagesResponse::default()) .mock_once() .mount() .await; @@ -1405,16 +1352,11 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { server .mock_room_messages() .from("prev-batch") - .ok( - "start-token-unused".to_owned(), - Some("prev-batch3".to_owned()), - vec![ - // Items in reverse order, since this is back-pagination. - f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), - f.text_msg("hello").event_id(event_id!("$1")).into_raw_timeline(), - ], - Vec::new(), - ) + .ok(RoomMessagesResponse::default().end_token("prev-batch3").events(vec![ + // Items in reverse order, since this is back-pagination. + f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), + f.text_msg("hello").event_id(event_id!("$1")).into_raw_timeline(), + ])) .mock_once() .mount() .await; @@ -1496,7 +1438,7 @@ async fn test_dont_delete_gap_that_wasnt_inserted() { server .mock_room_messages() .from("prev-batch") - .ok("start-token-unused".to_owned(), None, Vec::>::new(), Vec::new()) + .ok(RoomMessagesResponse::default()) .mock_once() .mount() .await; diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index 1ed2b91dc7f..173421c4403 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -18,7 +18,7 @@ use assert_matches::assert_matches; use async_trait::async_trait; use futures_util::FutureExt; use matrix_sdk::{ - test_utils::mocks::MatrixMockServer, + test_utils::mocks::{MatrixMockServer, RoomMessagesResponse}, widget::{ Capabilities, CapabilitiesProvider, WidgetDriver, WidgetDriverHandle, WidgetSettings, }, @@ -304,8 +304,7 @@ async fn test_read_messages_with_msgtype_capabilities() { let f = EventFactory::new().room(&ROOM_ID).sender(user_id!("@example:localhost")); { - let start = "t392-516_47314_0_7_1_1_1_11444_1".to_owned(); - let end = Some("t47409-4357353_219380_26003_2269".to_owned()); + let end = "t47409-4357353_219380_26003_2269"; let chunk2 = vec![ f.notice("custom content").event_id(event_id!("$msda7m0df9E9op3")).into_raw_timeline(), f.text_msg("hello").event_id(event_id!("$msda7m0df9E9op5")).into_raw_timeline(), @@ -314,7 +313,7 @@ async fn test_read_messages_with_msgtype_capabilities() { mock_server .mock_room_messages() .limit(3) - .ok(start, end, chunk2, Vec::new()) + .ok(RoomMessagesResponse::default().end_token(end).events(chunk2)) .mock_once() .mount() .await; From e6dc10933c928925cbfc2676c89dba0a8180c814 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 12:56:03 +0100 Subject: [PATCH 943/979] tests: add helper to delay a room /messages response This removes a few manual uses of `ResponseTemplate`, which is sweet and guarantees some better typing for those responses overall. --- .../tests/integration/room_list_service.rs | 18 ++++------ crates/matrix-sdk/src/test_utils/mocks.rs | 19 +++++++++-- .../tests/integration/event_cache.rs | 34 ++++++------------- 3 files changed, 33 insertions(+), 38 deletions(-) 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 a1ca670a288..d6bd6928b21 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -6,8 +6,9 @@ use futures_util::{pin_mut, FutureExt, StreamExt}; use matrix_sdk::{ config::RequestConfig, test_utils::{ - logged_in_client_with_server, mocks::MatrixMockServer, set_client_session, - test_client_builder, + logged_in_client_with_server, + mocks::{MatrixMockServer, RoomMessagesResponse}, + set_client_session, test_client_builder, }, Client, }; @@ -2845,16 +2846,9 @@ async fn test_multiple_timeline_init() { // Send back-pagination responses with a small delay. server .mock_room_messages() - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({ - "start": "unused-start", - "end": null, - "chunk": vec![f.text_msg("hello").into_raw_timeline()], - "state": [], - })) - .set_delay(Duration::from_millis(500)), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("hello").into_raw_timeline()]) + .delayed(Duration::from_millis(500))) .mount() .await; diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 5682ffafbcc..a77701df9f9 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -1896,12 +1896,18 @@ impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { /// Note: pass `chunk` in the correct order: topological for forward /// pagination, reverse topological for backwards pagination. pub fn ok(self, response: RoomMessagesResponse) -> MatrixMock<'a> { - let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + let mut template = ResponseTemplate::new(200).set_body_json(json!({ "start": response.start, "end": response.end, "chunk": response.chunk, "state": response.state, - }))); + })); + + if let Some(delay) = response.delay { + template = template.set_delay(delay); + } + + let mock = self.mock.respond_with(template); MatrixMock { server: self.server, mock } } } @@ -1917,6 +1923,8 @@ pub struct RoomMessagesResponse { pub chunk: Vec>, /// The set of state events returned by this query. pub state: Vec>, + /// Optional delay to respond to the query. + pub delay: Option, } impl RoomMessagesResponse { @@ -1931,6 +1939,12 @@ impl RoomMessagesResponse { self.end = Some(token.into()); self } + + /// Respond with a given delay to the query. + pub fn delayed(mut self, delay: Duration) -> Self { + self.delay = Some(delay); + self + } } impl Default for RoomMessagesResponse { @@ -1940,6 +1954,7 @@ impl Default for RoomMessagesResponse { end: Default::default(), chunk: Default::default(), state: Default::default(), + delay: None, } } } diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index ca0700d3ffe..aacb6250f42 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -25,7 +25,6 @@ use matrix_sdk_test::{ use ruma::{event_id, room_id, user_id}; use serde_json::json; use tokio::{spawn, sync::broadcast, time::sleep}; -use wiremock::ResponseTemplate; async fn once( outcome: BackPaginationOutcome, @@ -642,15 +641,9 @@ async fn test_reset_while_backpaginating() { server .mock_room_messages() .from("first_backpagination") - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({ - "chunk": vec![f.text_msg("lalala").into_raw_timeline()], - "start": "t392-516_47314_0_7_1_1_1_11444_1", - })) - // This is why we don't use `server.mock_room_messages()`. - .set_delay(Duration::from_millis(500)), - ) + .ok(RoomMessagesResponse::default() + .events(vec![f.text_msg("lalala").into_raw_timeline()]) + .delayed(Duration::from_millis(500))) .mock_once() .mount() .await; @@ -1065,20 +1058,13 @@ async fn test_backpaginate_with_no_initial_events() { let wait_time = Duration::from_millis(500); server .mock_room_messages() - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({ - "chunk": vec![ - f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), - f.text_msg("hello").event_id(event_id!("$3")).into_raw_timeline(), - ], - "start": "start-token-unused1", - "end": "prev_batch" - })) - // This is why we don't use `server.mock_room_messages()`. - // This delay has to be greater than the one used to return the sync response. - .set_delay(2 * wait_time), - ) + .ok(RoomMessagesResponse::default() + .end_token("prev_batch") + .events(vec![ + f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), + f.text_msg("hello").event_id(event_id!("$3")).into_raw_timeline(), + ]) + .delayed(2 * wait_time)) .mock_once() .mount() .await; From 9514388108c7007cbdc822c582d26ad9e89af5d9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 14:33:45 +0100 Subject: [PATCH 944/979] tests: rename `RoomMessagesResponse` to `RoomMessagesResponseTemplate` --- .../tests/integration/room_list_service.rs | 4 +- .../tests/integration/timeline/edit.rs | 4 +- crates/matrix-sdk/src/test_utils/mocks.rs | 8 ++-- .../tests/integration/event_cache.rs | 43 +++++++++++-------- crates/matrix-sdk/tests/integration/widget.rs | 4 +- 5 files changed, 34 insertions(+), 29 deletions(-) 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 d6bd6928b21..342ba7c9ea2 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -7,7 +7,7 @@ use matrix_sdk::{ config::RequestConfig, test_utils::{ logged_in_client_with_server, - mocks::{MatrixMockServer, RoomMessagesResponse}, + mocks::{MatrixMockServer, RoomMessagesResponseTemplate}, set_client_session, test_client_builder, }, Client, @@ -2846,7 +2846,7 @@ async fn test_multiple_timeline_init() { // Send back-pagination responses with a small delay. server .mock_room_messages() - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("hello").into_raw_timeline()]) .delayed(Duration::from_millis(500))) .mount() diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index ef54ac1e989..3ce9395379c 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -24,7 +24,7 @@ use matrix_sdk::{ room::edit::EditedContent, test_utils::{ logged_in_client_with_server, - mocks::{MatrixMockServer, RoomMessagesResponse}, + mocks::{MatrixMockServer, RoomMessagesResponseTemplate}, }, Client, }; @@ -870,7 +870,7 @@ impl PendingEditHelper { async fn handle_backpagination(&mut self, events: Vec>, batch_size: u16) { self.server .mock_room_messages() - .ok(RoomMessagesResponse::default().end_token("yolo").events(events)) + .ok(RoomMessagesResponseTemplate::default().end_token("yolo").events(events)) .mock_once() .mount() .await; diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index a77701df9f9..31df9ee1daa 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -1895,7 +1895,7 @@ impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { /// /// Note: pass `chunk` in the correct order: topological for forward /// pagination, reverse topological for backwards pagination. - pub fn ok(self, response: RoomMessagesResponse) -> MatrixMock<'a> { + pub fn ok(self, response: RoomMessagesResponseTemplate) -> MatrixMock<'a> { let mut template = ResponseTemplate::new(200).set_body_json(json!({ "start": response.start, "end": response.end, @@ -1913,7 +1913,7 @@ impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { } /// A response to a [`RoomMessagesEndpoint`] query. -pub struct RoomMessagesResponse { +pub struct RoomMessagesResponseTemplate { /// The start token for this /messages query. pub start: String, /// The end token for this /messages query (previous batch for back @@ -1927,7 +1927,7 @@ pub struct RoomMessagesResponse { pub delay: Option, } -impl RoomMessagesResponse { +impl RoomMessagesResponseTemplate { /// Fill the events returned as part of this response. pub fn events(mut self, chunk: Vec>>) -> Self { self.chunk = chunk.into_iter().map(Into::into).collect(); @@ -1947,7 +1947,7 @@ impl RoomMessagesResponse { } } -impl Default for RoomMessagesResponse { +impl Default for RoomMessagesResponseTemplate { fn default() -> Self { Self { start: "start-token-unused".to_owned(), diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index aacb6250f42..e43fcbb1f2a 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -16,7 +16,7 @@ use matrix_sdk::{ }, test_utils::{ assert_event_matches_msg, - mocks::{MatrixMockServer, RoomMessagesResponse}, + mocks::{MatrixMockServer, RoomMessagesResponseTemplate}, }, }; use matrix_sdk_test::{ @@ -261,7 +261,7 @@ async fn test_backpaginate_once() { server .mock_room_messages() .from("prev_batch") - .ok(RoomMessagesResponse::default().events(vec![ + .ok(RoomMessagesResponseTemplate::default().events(vec![ f.text_msg("world").event_id(event_id!("$2")), f.text_msg("hello").event_id(event_id!("$3")), ])) @@ -349,7 +349,7 @@ async fn test_backpaginate_many_times_with_many_iterations() { server .mock_room_messages() .from("prev_batch") - .ok(RoomMessagesResponse::default().end_token("prev_batch2").events(vec![ + .ok(RoomMessagesResponseTemplate::default().end_token("prev_batch2").events(vec![ f.text_msg("world").event_id(event_id!("$2")), f.text_msg("hello").event_id(event_id!("$3")), ])) @@ -361,7 +361,7 @@ async fn test_backpaginate_many_times_with_many_iterations() { server .mock_room_messages() .from("prev_batch2") - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("oh well").event_id(event_id!("$4"))])) .mock_once() .mount() @@ -487,7 +487,7 @@ async fn test_backpaginate_many_times_with_one_iteration() { server .mock_room_messages() .from("prev_batch") - .ok(RoomMessagesResponse::default().end_token("prev_batch2").events(vec![ + .ok(RoomMessagesResponseTemplate::default().end_token("prev_batch2").events(vec![ f.text_msg("world").event_id(event_id!("$2")), f.text_msg("hello").event_id(event_id!("$3")), ])) @@ -499,7 +499,7 @@ async fn test_backpaginate_many_times_with_one_iteration() { server .mock_room_messages() .from("prev_batch2") - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("oh well").event_id(event_id!("$4"))])) .mock_once() .mount() @@ -641,7 +641,7 @@ async fn test_reset_while_backpaginating() { server .mock_room_messages() .from("first_backpagination") - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("lalala").into_raw_timeline()]) .delayed(Duration::from_millis(500))) .mock_once() @@ -653,7 +653,7 @@ async fn test_reset_while_backpaginating() { server .mock_room_messages() .from("second_backpagination") - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .end_token("third_backpagination") .events(vec![f.text_msg("finally!").into_raw_timeline()])) .mock_once() @@ -764,7 +764,7 @@ async fn test_backpaginating_without_token() { server .mock_room_messages() - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()])) .mock_once() .mount() @@ -822,7 +822,7 @@ async fn test_limited_timeline_resets_pagination() { server .mock_room_messages() - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("hi").event_id(event_id!("$2")).into_raw_timeline()])) .mock_once() .mount() @@ -992,7 +992,7 @@ async fn test_limited_timeline_without_storage() { server .mock_room_messages() .from("prev-batch") - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("oh well").event_id(event_id!("$1"))])) .mock_once() .mount() @@ -1058,7 +1058,7 @@ async fn test_backpaginate_with_no_initial_events() { let wait_time = Duration::from_millis(500); server .mock_room_messages() - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .end_token("prev_batch") .events(vec![ f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), @@ -1073,7 +1073,7 @@ async fn test_backpaginate_with_no_initial_events() { server .mock_room_messages() .from("prev_batch") - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("oh well").event_id(event_id!("$1"))])) .mock_once() .mount() @@ -1146,7 +1146,7 @@ async fn test_backpaginate_replace_empty_gap() { // The first back-pagination will return a previous-batch token, but no events. server .mock_room_messages() - .ok(RoomMessagesResponse::default().end_token("prev_batch")) + .ok(RoomMessagesResponseTemplate::default().end_token("prev_batch")) .mock_once() .mount() .await; @@ -1155,7 +1155,7 @@ async fn test_backpaginate_replace_empty_gap() { server .mock_room_messages() .from("prev_batch") - .ok(RoomMessagesResponse::default() + .ok(RoomMessagesResponseTemplate::default() .events(vec![f.text_msg("hello").event_id(event_id!("$1"))])) .mock_once() .mount() @@ -1218,7 +1218,12 @@ async fn test_no_gap_stored_after_deduplicated_sync() { drop(events); // Backpagination will return nothing. - server.mock_room_messages().ok(RoomMessagesResponse::default()).mock_once().mount().await; + server + .mock_room_messages() + .ok(RoomMessagesResponseTemplate::default()) + .mock_once() + .mount() + .await; let pagination = room_event_cache.pagination(); @@ -1328,7 +1333,7 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { server .mock_room_messages() .from("prev-batch2") - .ok(RoomMessagesResponse::default()) + .ok(RoomMessagesResponseTemplate::default()) .mock_once() .mount() .await; @@ -1338,7 +1343,7 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { server .mock_room_messages() .from("prev-batch") - .ok(RoomMessagesResponse::default().end_token("prev-batch3").events(vec![ + .ok(RoomMessagesResponseTemplate::default().end_token("prev-batch3").events(vec![ // Items in reverse order, since this is back-pagination. f.text_msg("world").event_id(event_id!("$2")).into_raw_timeline(), f.text_msg("hello").event_id(event_id!("$1")).into_raw_timeline(), @@ -1424,7 +1429,7 @@ async fn test_dont_delete_gap_that_wasnt_inserted() { server .mock_room_messages() .from("prev-batch") - .ok(RoomMessagesResponse::default()) + .ok(RoomMessagesResponseTemplate::default()) .mock_once() .mount() .await; diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index 173421c4403..eeefaa96fbf 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -18,7 +18,7 @@ use assert_matches::assert_matches; use async_trait::async_trait; use futures_util::FutureExt; use matrix_sdk::{ - test_utils::mocks::{MatrixMockServer, RoomMessagesResponse}, + test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate}, widget::{ Capabilities, CapabilitiesProvider, WidgetDriver, WidgetDriverHandle, WidgetSettings, }, @@ -313,7 +313,7 @@ async fn test_read_messages_with_msgtype_capabilities() { mock_server .mock_room_messages() .limit(3) - .ok(RoomMessagesResponse::default().end_token(end).events(chunk2)) + .ok(RoomMessagesResponseTemplate::default().end_token(end).events(chunk2)) .mock_once() .mount() .await; From 279c78b3e2d7e3ba46ad2385fc50339d54175d4e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 15:19:15 +0100 Subject: [PATCH 945/979] chore!(encryption): rename `are_we_the_last_man_standing` to `is_last_device` While the former name is arguably more fun, the latter is more descriptive of what the function does. --- bindings/matrix-sdk-ffi/src/encryption.rs | 2 +- crates/matrix-sdk/CHANGELOG.md | 1 + crates/matrix-sdk/src/encryption/recovery/mod.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/encryption.rs b/bindings/matrix-sdk-ffi/src/encryption.rs index b7baaea8943..8744947428a 100644 --- a/bindings/matrix-sdk-ffi/src/encryption.rs +++ b/bindings/matrix-sdk-ffi/src/encryption.rs @@ -281,7 +281,7 @@ impl Encryption { } pub async fn is_last_device(&self) -> Result { - Ok(self.inner.recovery().are_we_the_last_man_standing().await?) + Ok(self.inner.recovery().is_last_device().await?) } pub async fn wait_for_backup_upload_steady_state( diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index a07382e6940..c8aa7f0c34a 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -28,6 +28,7 @@ All notable changes to this project will be documented in this file. call `AttachmentConfig::new().thumbnail(thumbnail)` now instead. - [**breaking**] `Room::send_attachment()` and `RoomSendQueue::send_attachment()` now take any type that implements `Into` for the filename. +- [**breaking**] `Recovery::are_we_the_last_man_standing()` has been renamed to `is_last_device()`. ## [0.9.0] - 2024-12-18 diff --git a/crates/matrix-sdk/src/encryption/recovery/mod.rs b/crates/matrix-sdk/src/encryption/recovery/mod.rs index 18d0a53a3e1..f64898548a8 100644 --- a/crates/matrix-sdk/src/encryption/recovery/mod.rs +++ b/crates/matrix-sdk/src/encryption/recovery/mod.rs @@ -462,7 +462,7 @@ impl Recovery { /// If the user does not enable recovery before logging out of their last /// device, they will not be able to decrypt historic messages once they /// create a new device. - pub async fn are_we_the_last_man_standing(&self) -> Result { + pub async fn is_last_device(&self) -> Result { let olm_machine = self.client.olm_machine().await; let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?; let user_id = olm_machine.user_id(); From 67d2cb790de9661931271ee7b3f12839b05dafc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 13 Jan 2025 17:09:57 +0100 Subject: [PATCH 946/979] chore: Fix a couple of typos --- crates/matrix-sdk-base/src/deserialized_responses.rs | 2 +- crates/matrix-sdk-base/src/store/ambiguity_map.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index 1f4bac92903..95d13561ff6 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -602,7 +602,7 @@ mod test { } #[test] - fn test_display_name_equality_cyrilic() { + fn test_display_name_equality_cyrillic() { // Display name with scritpure symbols assert_display_name_eq!("alice", "аlice"); } diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index 45a53a794bc..56888233ffc 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -430,7 +430,7 @@ mod test { assert_ambiguity!( [("@alice:localhost", "alice"), ("@bob:localhost", "аlice")], [("alice", true)], - "Bob tries to impersonate Alice using a cyrilic а" + "Bob tries to impersonate Alice using a cyrillic а" ); assert_ambiguity!( From 7f04a9a18bb65b2cea80bf6aca54e3fa1172edd6 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 15:06:56 +0100 Subject: [PATCH 947/979] fix(memory chunk): only remove a given room's events when clearing a roo --- .../src/linked_chunk/relational.rs | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index ec3e297de48..fabc55a2e14 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -188,8 +188,8 @@ impl RelationalLinkedChunk { Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ } Update::Clear => { - self.chunks.clear(); - self.items.clear(); + self.chunks.retain(|chunk| chunk.room_id != room_id); + self.items.retain(|chunk| chunk.room_id != room_id); } } } @@ -777,11 +777,12 @@ mod tests { #[test] fn test_clear() { - let room_id = room_id!("!r0:matrix.org"); + let r0 = room_id!("!r0:matrix.org"); + let r1 = room_id!("!r1:matrix.org"); let mut relational_linked_chunk = RelationalLinkedChunk::::new(); relational_linked_chunk.apply_updates( - room_id, + r0, vec![ // new chunk (this is not mandatory for this test, but let's try to be realistic) Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, @@ -790,42 +791,84 @@ mod tests { ], ); + relational_linked_chunk.apply_updates( + r1, + vec![ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x'] }, + ], + ); + // Chunks are correctly linked. assert_eq!( relational_linked_chunk.chunks, - &[ChunkRow { - room_id: room_id.to_owned(), - previous_chunk: None, - chunk: CId::new(0), - next_chunk: None, - }], + &[ + ChunkRow { + room_id: r0.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: None, + }, + ChunkRow { + room_id: r1.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: None, + } + ], ); + // Items contains the pushed items. assert_eq!( relational_linked_chunk.items, &[ ItemRow { - room_id: room_id.to_owned(), + room_id: r0.to_owned(), position: Position::new(CId::new(0), 0), item: Either::Item('a') }, ItemRow { - room_id: room_id.to_owned(), + room_id: r0.to_owned(), position: Position::new(CId::new(0), 1), item: Either::Item('b') }, ItemRow { - room_id: room_id.to_owned(), + room_id: r0.to_owned(), position: Position::new(CId::new(0), 2), item: Either::Item('c') }, + ItemRow { + room_id: r1.to_owned(), + position: Position::new(CId::new(0), 0), + item: Either::Item('x') + }, ], ); // Now, time for a clean up. - relational_linked_chunk.apply_updates(room_id, vec![Update::Clear]); - assert!(relational_linked_chunk.chunks.is_empty()); - assert!(relational_linked_chunk.items.is_empty()); + relational_linked_chunk.apply_updates(r0, vec![Update::Clear]); + + // Only items from r1 remain. + assert_eq!( + relational_linked_chunk.chunks, + &[ChunkRow { + room_id: r1.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: None, + }], + ); + + assert_eq!( + relational_linked_chunk.items, + &[ItemRow { + room_id: r1.to_owned(), + position: Position::new(CId::new(0), 0), + item: Either::Item('x') + },], + ); } #[test] From e647ff935e2984db1064514634c442f1271da0d1 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 15:07:50 +0100 Subject: [PATCH 948/979] feat(event cache store): allow removing an entire room at once --- .../event_cache/store/integration_tests.rs | 67 +++++++++++++++++-- .../src/event_cache/store/traits.rs | 7 ++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index de2174c2f0a..83ab9b3eda1 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -21,7 +21,10 @@ use matrix_sdk_common::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind, VerificationState, }, - linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, Position, RawChunk, Update}, + linked_chunk::{ + ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk, + Update, + }, }; use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use ruma::{ @@ -117,6 +120,9 @@ pub trait EventCacheStoreIntegrationTests { /// Test that clear all the rooms' linked chunks works. async fn test_clear_all_rooms_chunks(&self); + + /// Test that removing a room from storage empties all associated data. + async fn test_remove_room(&self); } fn rebuild_linked_chunk(raws: Vec>) -> Option> { @@ -299,8 +305,6 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { } async fn test_handle_updates_and_rebuild_linked_chunk(&self) { - use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId; - let room_id = room_id!("!r0:matrix.org"); self.handle_linked_chunk_updates( @@ -383,8 +387,6 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { } async fn test_clear_all_rooms_chunks(&self) { - use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId; - let r0 = room_id!("!r0:matrix.org"); let r1 = room_id!("!r1:matrix.org"); @@ -440,6 +442,54 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore { assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none()); assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none()); } + + async fn test_remove_room(&self) { + let r0 = room_id!("!r0:matrix.org"); + let r1 = room_id!("!r1:matrix.org"); + + // Add updates to the first room. + self.handle_linked_chunk_updates( + r0, + vec![ + // new chunk + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")], + }, + ], + ) + .await + .unwrap(); + + // Add updates to the second room. + self.handle_linked_chunk_updates( + r1, + vec![ + // new chunk + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec![make_test_event(r0, "yummy")], + }, + ], + ) + .await + .unwrap(); + + // Try to remove content from r0. + self.remove_room(r0).await.unwrap(); + + // Check that r0 doesn't have a linked chunk anymore. + let r0_linked_chunk = self.reload_linked_chunk(r0).await.unwrap(); + assert!(r0_linked_chunk.is_empty()); + + // Check that r1 is unaffected. + let r1_linked_chunk = self.reload_linked_chunk(r1).await.unwrap(); + assert!(!r1_linked_chunk.is_empty()); + } } /// Macro building to allow your `EventCacheStore` implementation to run the @@ -515,6 +565,13 @@ macro_rules! event_cache_store_integration_tests { get_event_cache_store().await.unwrap().into_event_cache_store(); event_cache_store.test_clear_all_rooms_chunks().await; } + + #[async_test] + async fn test_remove_room() { + let event_cache_store = + get_event_cache_store().await.unwrap().into_event_cache_store(); + event_cache_store.test_remove_room().await; + } } }; } diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index 5eb66d5fbba..654706273ff 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -57,6 +57,13 @@ pub trait EventCacheStore: AsyncTraitDeps { updates: Vec>, ) -> Result<(), Self::Error>; + /// Remove all data tied to a given room from the cache. + async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> { + // Right now, this means removing all the linked chunk. If implementations + // override this behavior, they should *also* include this code. + self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await + } + /// Return all the raw components of a linked chunk, so the caller may /// reconstruct the linked chunk later. async fn reload_linked_chunk( From a8ca77f4fc70d6e83ded30175fc79a012f92ed52 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 13 Jan 2025 15:08:04 +0100 Subject: [PATCH 949/979] feat(base): remove cached events when forgetting about a room --- crates/matrix-sdk-base/src/client.rs | 10 +++++++-- crates/matrix-sdk-base/src/error.rs | 11 ++++++++++ .../matrix-sdk/tests/integration/room/left.rs | 22 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 7c66f271f01..625c66baff5 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1516,8 +1516,14 @@ impl BaseClient { /// # Arguments /// /// * `room_id` - The id of the room that should be forgotten. - pub async fn forget_room(&self, room_id: &RoomId) -> StoreResult<()> { - self.store.forget_room(room_id).await + pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> { + // Forget the room in the state store. + self.store.forget_room(room_id).await?; + + // Remove the room in the event cache store too. + self.event_cache_store().lock().await?.remove_room(room_id).await?; + + Ok(()) } /// Get the olm machine. diff --git a/crates/matrix-sdk-base/src/error.rs b/crates/matrix-sdk-base/src/error.rs index 8d1a2dd3e22..bc3f2a13633 100644 --- a/crates/matrix-sdk-base/src/error.rs +++ b/crates/matrix-sdk-base/src/error.rs @@ -15,10 +15,13 @@ //! Error conditions. +use matrix_sdk_common::store_locks::LockStoreError; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError}; use thiserror::Error; +use crate::event_cache::store::EventCacheStoreError; + /// Result type of the rust-sdk. pub type Result = std::result::Result; @@ -42,6 +45,14 @@ pub enum Error { #[error(transparent)] StateStore(#[from] crate::store::StoreError), + /// An error happened while manipulating the event cache store. + #[error(transparent)] + EventCacheStore(#[from] EventCacheStoreError), + + /// An error happened while attempting to lock the event cache store. + #[error(transparent)] + EventCacheLock(#[from] LockStoreError), + /// An error occurred in the crypto store. #[cfg(feature = "e2e-encryption")] #[error(transparent)] diff --git a/crates/matrix-sdk/tests/integration/room/left.rs b/crates/matrix-sdk/tests/integration/room/left.rs index 7bad0b2377b..723b8572f0a 100644 --- a/crates/matrix-sdk/tests/integration/room/left.rs +++ b/crates/matrix-sdk/tests/integration/room/left.rs @@ -12,6 +12,7 @@ use ruma::{ user_id, OwnedRoomOrAliasId, }; use serde_json::json; +use tokio::task::yield_now; use wiremock::{ matchers::{header, method, path, path_regex}, Mock, ResponseTemplate, @@ -24,6 +25,10 @@ async fn test_forget_non_direct_room() { let (client, server) = logged_in_client_with_server().await; let user_id = client.user_id().unwrap(); + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + Mock::given(method("POST")) .and(path_regex(r"^/_matrix/client/r0/rooms/.*/forget$")) .and(header("authorization", "Bearer 1234")) @@ -47,12 +52,29 @@ async fn test_forget_non_direct_room() { let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); let _response = client.sync_once(sync_settings).await.unwrap(); + // Let the event cache process updates. + yield_now().await; + + { + // There is some data in the cache store. + let event_cache_store = client.event_cache_store().lock().await.unwrap(); + let room_data = event_cache_store.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap(); + assert!(!room_data.is_empty()); + } + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); assert_eq!(room.state(), RoomState::Left); room.forget().await.unwrap(); assert!(client.get_room(&DEFAULT_TEST_ROOM_ID).is_none()); + + { + // Data in the event cache store has been removed. + let event_cache_store = client.event_cache_store().lock().await.unwrap(); + let room_data = event_cache_store.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap(); + assert!(room_data.is_empty()); + } } #[async_test] From 9641aa9082542b547209fe9e1ffbc6a656c948fc Mon Sep 17 00:00:00 2001 From: Daniel Salinas Date: Mon, 13 Jan 2025 11:41:05 -0500 Subject: [PATCH 950/979] feat(send queue): Add an enqueued time to to-be-sent events (#4385) Add a new created_at to the send_queue_events and dependent_send_queue_events stored records. This will allow clients to understand how stale a pending message might be in the event that the queue encounters and error and becomes wedged. This change is exposed through the FFI on the `EventTimelineItem` struct as a new optional field named `local_created_at`. It will be `None` for any Remote event, and `Some` for Local events (except for those that were enqueued before the migrations were run). Signed-off-by: Daniel Salinas --------- Signed-off-by: Daniel Salinas Co-authored-by: Daniel Salinas Co-authored-by: Benjamin Bouvier Co-authored-by: Daniel Salinas --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 2 + .../src/store/integration_tests.rs | 107 ++++++++++++++++-- .../matrix-sdk-base/src/store/memory_store.rs | 9 +- .../matrix-sdk-base/src/store/send_queue.rs | 9 +- crates/matrix-sdk-base/src/store/traits.rs | 12 +- .../src/state_store/mod.rs | 24 +++- .../010_send_queue_enqueue_time.sql | 6 + crates/matrix-sdk-sqlite/src/state_store.rs | 99 ++++++++++++---- .../src/timeline/event_item/mod.rs | 8 ++ crates/matrix-sdk/src/send_queue/mod.rs | 72 +++++++++++- crates/matrix-sdk/src/send_queue/upload.rs | 16 ++- 11 files changed, 312 insertions(+), 52 deletions(-) create mode 100644 crates/matrix-sdk-sqlite/migrations/state_store/010_send_queue_enqueue_time.sql diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 7a4df082f0d..3520ce64c8a 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1033,6 +1033,7 @@ pub struct EventTimelineItem { timestamp: Timestamp, reactions: Vec, local_send_state: Option, + local_created_at: Option, read_receipts: HashMap, origin: Option, can_be_replied_to: bool, @@ -1070,6 +1071,7 @@ impl From for EventTimelineItem { timestamp: item.timestamp().into(), reactions, local_send_state: item.send_state().map(|s| s.into()), + local_created_at: item.local_created_at().map(|t| t.0.into()), read_receipts, origin: item.origin(), can_be_replied_to: item.can_be_replied_to(), diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index c6b3b5a7c3c..94eb18221ca 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -29,7 +29,8 @@ use ruma::{ }, owned_event_id, owned_mxc_uri, room_id, serde::Raw, - uint, user_id, EventId, OwnedEventId, OwnedUserId, RoomId, TransactionId, UserId, + uint, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, + TransactionId, UserId, }; use serde_json::{json, value::Value as JsonValue}; @@ -980,13 +981,21 @@ impl StateStoreIntegrationTests for DynStateStore { let ev = SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into()) .unwrap(); - self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?; + self.save_send_queue_request( + room_id, + txn.clone(), + MilliSecondsSinceUnixEpoch::now(), + ev.into(), + 0, + ) + .await?; // Add a single dependent queue request. self.save_dependent_queued_request( room_id, &txn, ChildTransactionId::new(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::RedactEvent, ) .await?; @@ -1242,7 +1251,15 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into()) .unwrap(); - self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap(); + self.save_send_queue_request( + room_id, + txn0.clone(), + MilliSecondsSinceUnixEpoch::now(), + event0.into(), + 0, + ) + .await + .unwrap(); // Reading it will work. let pending = self.load_send_queue_requests(room_id).await.unwrap(); @@ -1266,7 +1283,15 @@ impl StateStoreIntegrationTests for DynStateStore { ) .unwrap(); - self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap(); + self.save_send_queue_request( + room_id, + txn, + MilliSecondsSinceUnixEpoch::now(), + event.into(), + 0, + ) + .await + .unwrap(); } // Reading all the events should work. @@ -1364,7 +1389,15 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into()) .unwrap(); - self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap(); + self.save_send_queue_request( + room_id2, + txn.clone(), + MilliSecondsSinceUnixEpoch::now(), + event.into(), + 0, + ) + .await + .unwrap(); } // Add and remove one event for room3. @@ -1374,7 +1407,15 @@ impl StateStoreIntegrationTests for DynStateStore { let event = SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into()) .unwrap(); - self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap(); + self.save_send_queue_request( + room_id3, + txn.clone(), + MilliSecondsSinceUnixEpoch::now(), + event.into(), + 0, + ) + .await + .unwrap(); self.remove_send_queue_request(room_id3, &txn).await.unwrap(); } @@ -1399,21 +1440,45 @@ impl StateStoreIntegrationTests for DynStateStore { let ev0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into()) .unwrap(); - self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap(); + self.save_send_queue_request( + room_id, + low0_txn.clone(), + MilliSecondsSinceUnixEpoch::now(), + ev0.into(), + 2, + ) + .await + .unwrap(); // Saving one request with higher priority should work. let high_txn = TransactionId::new(); let ev1 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into()) .unwrap(); - self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap(); + self.save_send_queue_request( + room_id, + high_txn.clone(), + MilliSecondsSinceUnixEpoch::now(), + ev1.into(), + 10, + ) + .await + .unwrap(); // Saving another request with the low priority should work. let low1_txn = TransactionId::new(); let ev2 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into()) .unwrap(); - self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap(); + self.save_send_queue_request( + room_id, + low1_txn.clone(), + MilliSecondsSinceUnixEpoch::now(), + ev2.into(), + 2, + ) + .await + .unwrap(); // The requests should be ordered from higher priority to lower, and when equal, // should use the insertion order instead. @@ -1453,7 +1518,15 @@ impl StateStoreIntegrationTests for DynStateStore { let event0 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into()) .unwrap(); - self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap(); + self.save_send_queue_request( + room_id, + txn0.clone(), + MilliSecondsSinceUnixEpoch::now(), + event0.into(), + 0, + ) + .await + .unwrap(); // No dependents, to start with. assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty()); @@ -1464,6 +1537,7 @@ impl StateStoreIntegrationTests for DynStateStore { room_id, &txn0, child_txn.clone(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::RedactEvent, ) .await @@ -1515,12 +1589,21 @@ impl StateStoreIntegrationTests for DynStateStore { let event1 = SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into()) .unwrap(); - self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap(); + self.save_send_queue_request( + room_id, + txn1.clone(), + MilliSecondsSinceUnixEpoch::now(), + event1.into(), + 0, + ) + .await + .unwrap(); self.save_dependent_queued_request( room_id, &txn0, ChildTransactionId::new(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::RedactEvent, ) .await @@ -1531,6 +1614,7 @@ impl StateStoreIntegrationTests for DynStateStore { room_id, &txn1, ChildTransactionId::new(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::EditEvent { new_content: SerializableEventContent::new( &RoomMessageEventContent::text_plain("edit").into(), @@ -1563,6 +1647,7 @@ impl StateStoreIntegrationTests for DynStateStore { room_id, &txn, child_txn.clone(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::RedactEvent, ) .await diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 9148c9b34da..94c10c95f8f 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -30,8 +30,8 @@ use ruma::{ }, serde::Raw, time::Instant, - CanonicalJsonObject, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, - OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId, + CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, + OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId, }; use tracing::{debug, instrument, warn}; @@ -750,6 +750,7 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, + created_at: MilliSecondsSinceUnixEpoch, kind: QueuedRequestKind, priority: usize, ) -> Result<(), Self::Error> { @@ -759,7 +760,7 @@ impl StateStore for MemoryStore { .send_queue_events .entry(room_id.to_owned()) .or_default() - .push(QueuedRequest { kind, transaction_id, error: None, priority }); + .push(QueuedRequest { kind, transaction_id, error: None, priority, created_at }); Ok(()) } @@ -858,6 +859,7 @@ impl StateStore for MemoryStore { room: &RoomId, parent_transaction_id: &TransactionId, own_transaction_id: ChildTransactionId, + created_at: MilliSecondsSinceUnixEpoch, content: DependentQueuedRequestKind, ) -> Result<(), Self::Error> { self.inner @@ -871,6 +873,7 @@ impl StateStore for MemoryStore { parent_transaction_id: parent_transaction_id.to_owned(), own_transaction_id, parent_key: None, + created_at, }); Ok(()) } diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index ece50344e9f..8c87b6c5ebb 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -23,7 +23,8 @@ use ruma::{ AnyMessageLikeEventContent, EventContent as _, RawExt as _, }, serde::Raw, - OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, UInt, + MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, + TransactionId, UInt, }; use serde::{Deserialize, Serialize}; @@ -131,6 +132,9 @@ pub struct QueuedRequest { /// The bigger the value, the higher the priority at which this request /// should be handled. pub priority: usize, + + /// The time that the request was originally attempted. + pub created_at: MilliSecondsSinceUnixEpoch, } impl QueuedRequest { @@ -371,6 +375,9 @@ pub struct DependentQueuedRequest { /// If the parent request has been sent, the parent's request identifier /// returned by the server once the local echo has been sent out. pub parent_key: Option, + + /// The time that the request was originally attempted. + pub created_at: MilliSecondsSinceUnixEpoch, } impl DependentQueuedRequest { diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 5f651483f5b..446bc9fbddd 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -35,8 +35,8 @@ use ruma::{ }, serde::Raw, time::SystemTime, - EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, - TransactionId, UserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId, + OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, }; use serde::{Deserialize, Serialize}; @@ -359,6 +359,7 @@ pub trait StateStore: AsyncTraitDeps { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, + created_at: MilliSecondsSinceUnixEpoch, request: QueuedRequestKind, priority: usize, ) -> Result<(), Self::Error>; @@ -421,6 +422,7 @@ pub trait StateStore: AsyncTraitDeps { room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, + created_at: MilliSecondsSinceUnixEpoch, content: DependentQueuedRequestKind, ) -> Result<(), Self::Error>; @@ -657,11 +659,12 @@ impl StateStore for EraseStateStoreError { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, + created_at: MilliSecondsSinceUnixEpoch, content: QueuedRequestKind, priority: usize, ) -> Result<(), Self::Error> { self.0 - .save_send_queue_request(room_id, transaction_id, content, priority) + .save_send_queue_request(room_id, transaction_id, created_at, content, priority) .await .map_err(Into::into) } @@ -711,10 +714,11 @@ impl StateStore for EraseStateStoreError { room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, + created_at: MilliSecondsSinceUnixEpoch, content: DependentQueuedRequestKind, ) -> Result<(), Self::Error> { self.0 - .save_dependent_queued_request(room_id, parent_txn_id, own_txn_id, content) + .save_dependent_queued_request(room_id, parent_txn_id, own_txn_id, created_at, content) .await .map_err(Into::into) } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 01d386f354f..ff8142104fb 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -44,8 +44,8 @@ use ruma::{ GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, SyncStateEvent, }, serde::Raw, - CanonicalJsonObject, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, - OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId, + CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, + OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId, }; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tracing::{debug, warn}; @@ -442,6 +442,10 @@ struct PersistedQueuedRequest { priority: Option, + /// The time the original message was first attempted to be sent at. + #[serde(default = "created_now")] + created_at: MilliSecondsSinceUnixEpoch, + // Migrated fields: keep these private, they're not used anymore elsewhere in the code base. /// Deprecated (from old format), now replaced with error field. is_wedged: Option, @@ -449,6 +453,10 @@ struct PersistedQueuedRequest { event: Option, } +fn created_now() -> MilliSecondsSinceUnixEpoch { + MilliSecondsSinceUnixEpoch::now() +} + impl PersistedQueuedRequest { fn into_queued_request(self) -> Option { let kind = @@ -467,7 +475,13 @@ impl PersistedQueuedRequest { // By default, events without a priority have a priority of 0. let priority = self.priority.unwrap_or(0); - Some(QueuedRequest { kind, transaction_id: self.transaction_id, error, priority }) + Some(QueuedRequest { + kind, + transaction_id: self.transaction_id, + error, + priority, + created_at: self.created_at, + }) } } @@ -1370,6 +1384,7 @@ impl_state_store!({ &self, room_id: &RoomId, transaction_id: OwnedTransactionId, + created_at: MilliSecondsSinceUnixEpoch, kind: QueuedRequestKind, priority: usize, ) -> Result<()> { @@ -1401,6 +1416,7 @@ impl_state_store!({ is_wedged: None, event: None, priority: Some(priority), + created_at, }); // Save the new vector into db. @@ -1570,6 +1586,7 @@ impl_state_store!({ room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, + created_at: MilliSecondsSinceUnixEpoch, content: DependentQueuedRequestKind, ) -> Result<()> { let encoded_key = self.encode_key(keys::DEPENDENT_SEND_QUEUE, room_id); @@ -1596,6 +1613,7 @@ impl_state_store!({ parent_transaction_id: parent_txn_id.to_owned(), own_transaction_id: own_txn_id, parent_key: None, + created_at, }); // Save the new vector into db. diff --git a/crates/matrix-sdk-sqlite/migrations/state_store/010_send_queue_enqueue_time.sql b/crates/matrix-sdk-sqlite/migrations/state_store/010_send_queue_enqueue_time.sql new file mode 100644 index 00000000000..7dcf3bf6d04 --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/state_store/010_send_queue_enqueue_time.sql @@ -0,0 +1,6 @@ +-- Migration script to add the created_at column to the send_queue_events table +ALTER TABLE "send_queue_events" +ADD COLUMN "created_at" INTEGER DEFAULT NULL; + +ALTER TABLE "dependent_send_queue_events" +ADD COLUMN "created_at" INTEGER DEFAULT NULL; diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index adfd9d5b5a3..e47fca481e1 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -32,8 +32,8 @@ use ruma::{ GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, serde::Raw, - CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomId, OwnedTransactionId, OwnedUserId, - RoomId, RoomVersionId, TransactionId, UserId, + CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, + OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UInt, UserId, }; use rusqlite::{OptionalExtension, Transaction}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -69,7 +69,7 @@ mod keys { /// This is used to figure whether the sqlite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in /// the [`SqliteStateStore::run_migrations`] function.. -const DATABASE_VERSION: u8 = 10; +const DATABASE_VERSION: u8 = 11; /// A sqlite based cryptostore. #[derive(Clone)] @@ -318,6 +318,17 @@ impl SqliteStateStore { .await?; } + if from < 11 && to >= 11 { + conn.with_transaction(move |txn| { + // Run the migration. + txn.execute_batch(include_str!( + "../migrations/state_store/010_send_queue_enqueue_time.sql" + ))?; + txn.set_db_version(11) + }) + .await?; + } + Ok(()) } @@ -1757,6 +1768,7 @@ impl StateStore for SqliteStateStore { &self, room_id: &RoomId, transaction_id: OwnedTransactionId, + created_at: MilliSecondsSinceUnixEpoch, content: QueuedRequestKind, priority: usize, ) -> Result<(), Self::Error> { @@ -1764,16 +1776,16 @@ impl StateStore for SqliteStateStore { let room_id_value = self.serialize_value(&room_id.to_owned())?; let content = self.serialize_json(&content)?; - // The transaction id is used both as a key (in remove/update) and a value (as // it's useful for the callers), so we keep it as is, and neither hash // it (with encode_key) or encrypt it (through serialize_value). After // all, it carries no personal information, so this is considered fine. + let created_at_ts: u64 = created_at.0.into(); self.acquire() .await? .with_transaction(move |txn| { - txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content, priority) VALUES (?, ?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content, priority))?; + txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content, priority, created_at) VALUES (?, ?, ?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content, priority, created_at_ts))?; Ok(()) }) .await @@ -1835,14 +1847,14 @@ impl StateStore for SqliteStateStore { // Note: ROWID is always present and is an auto-incremented integer counter. We // want to maintain the insertion order, so we can sort using it. // Note 2: transaction_id is not encoded, see why in `save_send_queue_event`. - let res: Vec<(String, Vec, Option>, usize)> = self + let res: Vec<(String, Vec, Option>, usize, Option)> = self .acquire() .await? .prepare( - "SELECT transaction_id, content, wedge_reason, priority FROM send_queue_events WHERE room_id = ? ORDER BY priority DESC, ROWID", + "SELECT transaction_id, content, wedge_reason, priority, created_at FROM send_queue_events WHERE room_id = ? ORDER BY priority DESC, ROWID", |mut stmt| { stmt.query((room_id,))? - .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))) + .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?))) .collect() }, ) @@ -1850,11 +1862,16 @@ impl StateStore for SqliteStateStore { let mut requests = Vec::with_capacity(res.len()); for entry in res { + let created_at = entry + .4 + .and_then(UInt::new) + .map_or_else(MilliSecondsSinceUnixEpoch::now, MilliSecondsSinceUnixEpoch); requests.push(QueuedRequest { transaction_id: entry.0.into(), kind: self.deserialize_json(&entry.1)?, error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?, priority: entry.3, + created_at, }); } @@ -1912,6 +1929,7 @@ impl StateStore for SqliteStateStore { room_id: &RoomId, parent_txn_id: &TransactionId, own_txn_id: ChildTransactionId, + created_at: MilliSecondsSinceUnixEpoch, content: DependentQueuedRequestKind, ) -> Result<()> { let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id); @@ -1921,15 +1939,22 @@ impl StateStore for SqliteStateStore { let parent_txn_id = parent_txn_id.to_string(); let own_txn_id = own_txn_id.to_string(); + let created_at_ts: u64 = created_at.0.into(); self.acquire() .await? .with_transaction(move |txn| { txn.prepare_cached( r#"INSERT INTO dependent_send_queue_events - (room_id, parent_transaction_id, own_transaction_id, content) - VALUES (?, ?, ?, ?)"#, + (room_id, parent_transaction_id, own_transaction_id, content, created_at) + VALUES (?, ?, ?, ?, ?)"#, )? - .execute((room_id, parent_txn_id, own_txn_id, content))?; + .execute(( + room_id, + parent_txn_id, + own_txn_id, + content, + created_at_ts, + ))?; Ok(()) }) .await @@ -2022,14 +2047,14 @@ impl StateStore for SqliteStateStore { let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id); // Note: transaction_id is not encoded, see why in `save_send_queue_event`. - let res: Vec<(String, String, Option>, Vec)> = self + let res: Vec<(String, String, Option>, Vec, Option)> = self .acquire() .await? .prepare( - "SELECT own_transaction_id, parent_transaction_id, parent_key, content FROM dependent_send_queue_events WHERE room_id = ? ORDER BY ROWID", + "SELECT own_transaction_id, parent_transaction_id, parent_key, content, created_at FROM dependent_send_queue_events WHERE room_id = ? ORDER BY ROWID", |mut stmt| { stmt.query((room_id,))? - .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))) + .mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?))) .collect() }, ) @@ -2037,11 +2062,16 @@ impl StateStore for SqliteStateStore { let mut dependent_events = Vec::with_capacity(res.len()); for entry in res { + let created_at = entry + .4 + .and_then(UInt::new) + .map_or_else(MilliSecondsSinceUnixEpoch::now, MilliSecondsSinceUnixEpoch); dependent_events.push(DependentQueuedRequest { own_transaction_id: entry.0.into(), parent_transaction_id: entry.1.into(), parent_key: entry.2.map(|bytes| self.deserialize_value(&bytes)).transpose()?, kind: self.deserialize_json(&entry.3)?, + created_at, }); } @@ -2395,16 +2425,15 @@ mod migration_tests { let wedge_tx = wedged_event_transaction_id.clone(); let local_tx = local_event_transaction_id.clone(); - db.save_dependent_queued_request( - room_id, - &local_tx, - ChildTransactionId::new(), - DependentQueuedRequestKind::RedactEvent, - ) - .await - .unwrap(); - conn.with_transaction(move |txn| { + add_dependent_send_queue_event_v7( + &db, + txn, + room_id, + &local_tx, + ChildTransactionId::new(), + DependentQueuedRequestKind::RedactEvent, + )?; add_send_queue_event_v7(&db, txn, &wedge_tx, room_id, true)?; add_send_queue_event_v7(&db, txn, &local_tx, room_id, false)?; Result::<_, Error>::Ok(()) @@ -2444,4 +2473,28 @@ mod migration_tests { Ok(()) } + + fn add_dependent_send_queue_event_v7( + this: &SqliteStateStore, + txn: &Transaction<'_>, + room_id: &RoomId, + parent_txn_id: &TransactionId, + own_txn_id: ChildTransactionId, + content: DependentQueuedRequestKind, + ) -> Result<(), Error> { + let room_id_value = this.serialize_value(&room_id.to_owned())?; + + let parent_txn_id = parent_txn_id.to_string(); + let own_txn_id = own_txn_id.to_string(); + let content = this.serialize_json(&content)?; + + txn.prepare_cached( + "INSERT INTO dependent_send_queue_events + (room_id, parent_transaction_id, own_transaction_id, content) + VALUES (?, ?, ?, ?)", + )? + .execute((room_id_value, parent_txn_id, own_txn_id, content))?; + + Ok(()) + } } 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 a10f9fc95a6..47918eba1e1 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -268,6 +268,14 @@ impl EventTimelineItem { as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state) } + /// Get the time that the local event was pushed in the send queue at. + pub fn local_created_at(&self) -> Option { + match &self.kind { + EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at), + EventTimelineItemKind::Remote(_) => None, + } + } + /// Get the unique identifier of this item. /// /// Returns the transaction ID for a local echo item that has not been sent diff --git a/crates/matrix-sdk/src/send_queue/mod.rs b/crates/matrix-sdk/src/send_queue/mod.rs index 749917b19f5..be839ec39a7 100644 --- a/crates/matrix-sdk/src/send_queue/mod.rs +++ b/crates/matrix-sdk/src/send_queue/mod.rs @@ -162,7 +162,7 @@ use ruma::{ AnyMessageLikeEventContent, EventContent as _, Mentions, }, serde::Raw, - OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, + MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, }; use tokio::sync::{broadcast, oneshot, Mutex, Notify, OwnedMutexGuard}; use tracing::{debug, error, info, instrument, trace, warn}; @@ -444,7 +444,8 @@ impl RoomSendQueue { let content = SerializableEventContent::from_raw(content, event_type); - let transaction_id = self.inner.queue.push(content.clone().into()).await?; + let created_at = MilliSecondsSinceUnixEpoch::now(); + let transaction_id = self.inner.queue.push(content.clone().into(), created_at).await?; trace!(%transaction_id, "manager sends a raw event to the background task"); self.inner.notifier.notify_one(); @@ -453,6 +454,7 @@ impl RoomSendQueue { room: self.clone(), transaction_id: transaction_id.clone(), media_handles: None, + created_at, }; let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { @@ -949,6 +951,7 @@ impl QueueStorage { async fn push( &self, request: QueuedRequestKind, + created_at: MilliSecondsSinceUnixEpoch, ) -> Result { let transaction_id = TransactionId::new(); @@ -960,6 +963,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, transaction_id.clone(), + created_at, request, Self::LOW_PRIORITY, ) @@ -1115,6 +1119,7 @@ impl QueueStorage { &self.room_id, transaction_id, ChildTransactionId::new(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::RedactEvent, ) .await?; @@ -1155,6 +1160,7 @@ impl QueueStorage { &self.room_id, transaction_id, ChildTransactionId::new(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::EditEvent { new_content: serializable }, ) .await?; @@ -1174,11 +1180,13 @@ impl QueueStorage { /// Push requests (and dependents) to upload a media. /// /// See the module-level description for details of the whole processus. + #[allow(clippy::too_many_arguments)] async fn push_media( &self, event: RoomMessageEventContent, content_type: Mime, send_event_txn: OwnedTransactionId, + created_at: MilliSecondsSinceUnixEpoch, upload_file_txn: OwnedTransactionId, file_media_request: MediaRequestParameters, thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequestParameters, Mime)>, @@ -1186,7 +1194,6 @@ impl QueueStorage { let guard = self.store.lock().await; let client = guard.client()?; let store = client.store(); - let thumbnail_info = if let Some((thumbnail_info, thumbnail_media_request, thumbnail_content_type)) = thumbnail @@ -1198,6 +1205,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, upload_thumbnail_txn.clone(), + created_at, QueuedRequestKind::MediaUpload { content_type: thumbnail_content_type.to_string(), cache_key: thumbnail_media_request, @@ -1214,6 +1222,7 @@ impl QueueStorage { &self.room_id, &upload_thumbnail_txn, upload_file_txn.clone().into(), + created_at, DependentQueuedRequestKind::UploadFileWithThumbnail { content_type: content_type.to_string(), cache_key: file_media_request, @@ -1229,6 +1238,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, upload_file_txn.clone(), + created_at, QueuedRequestKind::MediaUpload { content_type: content_type.to_string(), cache_key: file_media_request, @@ -1248,6 +1258,7 @@ impl QueueStorage { &self.room_id, &upload_file_txn, send_event_txn.into(), + created_at, DependentQueuedRequestKind::FinishUpload { local_echo: event, file_upload: upload_file_txn.clone(), @@ -1265,6 +1276,7 @@ impl QueueStorage { &self, transaction_id: &TransactionId, key: String, + created_at: MilliSecondsSinceUnixEpoch, ) -> Result, RoomSendQueueStorageError> { let guard = self.store.lock().await; let client = guard.client()?; @@ -1294,6 +1306,7 @@ impl QueueStorage { &self.room_id, transaction_id, reaction_txn_id.clone(), + created_at, DependentQueuedRequestKind::ReactEvent { key }, ) .await?; @@ -1322,6 +1335,7 @@ impl QueueStorage { room: room.clone(), transaction_id: queued.transaction_id, media_handles: None, + created_at: queued.created_at, }, send_error: queued.error, }, @@ -1381,6 +1395,7 @@ impl QueueStorage { upload_thumbnail_txn: thumbnail_info.map(|info| info.txn), upload_file_txn: file_upload, }), + created_at: dep.created_at, }, send_error: None, }, @@ -1470,6 +1485,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, dependent_request.own_transaction_id.into(), + dependent_request.created_at, serializable.into(), Self::HIGH_PRIORITY, ) @@ -1557,6 +1573,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, dependent_request.own_transaction_id.into(), + dependent_request.created_at, serializable.into(), Self::HIGH_PRIORITY, ) @@ -1887,6 +1904,9 @@ pub struct SendHandle { /// Additional handles for a media upload. media_handles: Option, + + /// The time at which the event to be sent has been created. + pub created_at: MilliSecondsSinceUnixEpoch, } impl SendHandle { @@ -2073,8 +2093,9 @@ impl SendHandle { ) -> Result, RoomSendQueueStorageError> { trace!("received an intent to react"); + let created_at = MilliSecondsSinceUnixEpoch::now(); if let Some(reaction_txn_id) = - self.room.inner.queue.react(&self.transaction_id, key.clone()).await? + self.room.inner.queue.react(&self.transaction_id, key.clone(), created_at).await? { trace!("successfully queued react"); @@ -2138,6 +2159,7 @@ impl SendReactionHandle { room: self.room.clone(), transaction_id: self.transaction_id.clone().into(), media_handles: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }; handle.abort().await @@ -2209,12 +2231,44 @@ mod tests { use matrix_sdk_test::{async_test, JoinedRoomBuilder, SyncResponseBuilder}; use ruma::{ events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, - room_id, TransactionId, + room_id, MilliSecondsSinceUnixEpoch, TransactionId, }; use super::canonicalize_dependent_requests; use crate::{client::WeakClient, test_utils::logged_in_client}; + #[test] + fn test_canonicalize_dependent_events_created_at() { + // Test to ensure the created_at field is being serialized and retrieved + // correctly. + let txn = TransactionId::new(); + let created_at = MilliSecondsSinceUnixEpoch::now(); + + let edit = DependentQueuedRequest { + own_transaction_id: ChildTransactionId::new(), + parent_transaction_id: txn.clone(), + kind: DependentQueuedRequestKind::EditEvent { + new_content: SerializableEventContent::new( + &RoomMessageEventContent::text_plain("edit").into(), + ) + .unwrap(), + }, + parent_key: None, + created_at, + }; + + let res = canonicalize_dependent_requests(&[edit]); + + assert_eq!(res.len(), 1); + assert_let!(DependentQueuedRequestKind::EditEvent { new_content } = &res[0].kind); + assert_let!( + AnyMessageLikeEventContent::RoomMessage(msg) = new_content.deserialize().unwrap() + ); + assert_eq!(msg.body(), "edit"); + assert_eq!(res[0].parent_transaction_id, txn); + assert_eq!(res[0].created_at, created_at); + } + #[async_test] async fn test_client_no_cycle_with_send_queue() { for enabled in [true, false] { @@ -2275,6 +2329,7 @@ mod tests { .unwrap(), }, parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }; let res = canonicalize_dependent_requests(&[edit]); @@ -2295,6 +2350,7 @@ mod tests { parent_transaction_id: txn.clone(), kind: DependentQueuedRequestKind::RedactEvent, parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }; let edit = DependentQueuedRequest { @@ -2307,6 +2363,7 @@ mod tests { .unwrap(), }, parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }; inputs.push({ @@ -2346,6 +2403,7 @@ mod tests { .unwrap(), }, parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }) .collect::>(); @@ -2377,6 +2435,7 @@ mod tests { kind: DependentQueuedRequestKind::RedactEvent, parent_transaction_id: txn1.clone(), parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }, // This one pertains to txn2. DependentQueuedRequest { @@ -2389,6 +2448,7 @@ mod tests { }, parent_transaction_id: txn2.clone(), parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }, ]; @@ -2419,6 +2479,7 @@ mod tests { kind: DependentQueuedRequestKind::ReactEvent { key: "🧠".to_owned() }, parent_transaction_id: txn.clone(), parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }; let edit_id = ChildTransactionId::new(); @@ -2432,6 +2493,7 @@ mod tests { }, parent_transaction_id: txn, parent_key: None, + created_at: MilliSecondsSinceUnixEpoch::now(), }; let res = canonicalize_dependent_requests(&[react, edit]); diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 1d951b7caf2..96a0df45858 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -28,7 +28,7 @@ use ruma::{ room::message::{FormattedBody, MessageType, RoomMessageEventContent}, AnyMessageLikeEventContent, Mentions, }, - OwnedTransactionId, TransactionId, + MilliSecondsSinceUnixEpoch, OwnedTransactionId, TransactionId, }; use tracing::{debug, error, instrument, trace, warn, Span}; @@ -185,6 +185,8 @@ impl RoomSendQueue { config.mentions, ); + let created_at = MilliSecondsSinceUnixEpoch::now(); + // Save requests in the queue storage. self.inner .queue @@ -192,6 +194,7 @@ impl RoomSendQueue { event_content.clone(), content_type, send_event_txn.clone().into(), + created_at, upload_file_txn.clone(), file_media_request, queue_thumbnail_info, @@ -206,6 +209,7 @@ impl RoomSendQueue { room: self.clone(), transaction_id: send_event_txn.clone().into(), media_handles: Some(MediaHandles { upload_thumbnail_txn, upload_file_txn }), + created_at, }; let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho { @@ -306,6 +310,7 @@ impl QueueStorage { .save_send_queue_request( &self.room_id, event_txn, + MilliSecondsSinceUnixEpoch::now(), new_content.into(), Self::HIGH_PRIORITY, ) @@ -350,7 +355,13 @@ impl QueueStorage { client .store() - .save_send_queue_request(&self.room_id, next_upload_txn, request, Self::HIGH_PRIORITY) + .save_send_queue_request( + &self.room_id, + next_upload_txn, + MilliSecondsSinceUnixEpoch::now(), + request, + Self::HIGH_PRIORITY, + ) .await .map_err(RoomSendQueueStorageError::StateStoreError)?; @@ -579,6 +590,7 @@ impl QueueStorage { &self.room_id, txn, ChildTransactionId::new(), + MilliSecondsSinceUnixEpoch::now(), DependentQueuedRequestKind::EditEvent { new_content: new_serialized }, ) .await?; From ee32b1f600eea36ea045c62347830df7cf55fb1b Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 13 Jan 2025 17:50:50 +0100 Subject: [PATCH 951/979] tests: Add an encrypted snapshot of a SQLite db for regression tests --- crates/matrix-sdk-sqlite/src/crypto_store.rs | 287 +++++++++++++++++- testing/data/storage/alice/README.md | 8 + .../storage/alice/matrix-sdk-crypto.sqlite3 | Bin 0 -> 249856 bytes 3 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 testing/data/storage/alice/README.md create mode 100644 testing/data/storage/alice/matrix-sdk-crypto.sqlite3 diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index fe6ce5e88a1..9b9e232ee4a 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -1404,11 +1404,14 @@ impl CryptoStore for SqliteCryptoStore { mod tests { use std::path::Path; + use matrix_sdk_common::deserialized_responses::WithheldCode; use matrix_sdk_crypto::{ - cryptostore_integration_tests, cryptostore_integration_tests_time, store::CryptoStore, + cryptostore_integration_tests, cryptostore_integration_tests_time, olm::SenderDataType, + store::CryptoStore, }; use matrix_sdk_test::async_test; use once_cell::sync::Lazy; + use ruma::{device_id, room_id, user_id}; use similar_asserts::assert_eq; use tempfile::{tempdir, TempDir}; use tokio::fs; @@ -1424,11 +1427,11 @@ mod tests { database: SqliteCryptoStore, } - async fn get_test_db() -> TestDb { + async fn get_test_db(data_path: &str, passphrase: Option<&str>) -> TestDb { let db_name = "matrix-sdk-crypto.sqlite3"; let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let database_path = manifest_path.join("testing/data/storage").join(db_name); + let database_path = manifest_path.join(data_path).join(db_name); let tmpdir = tempdir().unwrap(); let destination = tmpdir.path().join(db_name); @@ -1436,8 +1439,9 @@ mod tests { // Copy the test database to the tempdir so our test runs are idempotent. std::fs::copy(&database_path, destination).unwrap(); - let database = - SqliteCryptoStore::open(tmpdir.path(), None).await.expect("Can't open the test store"); + let database = SqliteCryptoStore::open(tmpdir.path(), passphrase) + .await + .expect("Can't open the test store"); TestDb { _dir: tmpdir, database } } @@ -1446,7 +1450,7 @@ mod tests { /// pre-filled database, or in other words use a test vector for this. #[async_test] async fn test_open_test_vector_store() { - let TestDb { _dir: _, database } = get_test_db().await; + let TestDb { _dir: _, database } = get_test_db("testing/data/storage", None).await; let account = database .load_account() @@ -1508,6 +1512,277 @@ mod tests { assert_eq!(master_key.to_base64(), "iCUEtB1RwANeqRa5epDrblLk4mer/36sylwQ5hYY3oE"); } + /// Test that we didn't regress in our storage layer by loading data from a + /// pre-filled database, or in other words use a test vector for this. + #[async_test] + async fn test_open_test_vector_encrypted_store() { + let TestDb { _dir: _, database } = get_test_db( + "testing/data/storage/alice", + Some(concat!( + "/rCia2fYAJ+twCZ1Xm2mxFCYcmJdyzkdJjwtgXsziWpYS/UeNxnixuSieuwZXm+x1VsJHmWpl", + "H+QIQBZpEGZtC9/S/l8xK+WOCesmET0o6yJ/KP73ofDtjBlnNpPwuHLKFpyTbyicpCgQ4UT+5E", + "UBuJ08TY9Ujdf1D13k5kr5tSZUefDKKCuG1fCRqlU8ByRas1PMQsZxT2W8t7QgBrQiiGmhpo/O", + "Ti4hfx97GOxncKcxTzppiYQNoHs/f15+XXQD7/oiCcqRIuUlXNsU6hRpFGmbYx2Pi1eyQViQCt", + "B5dAEiSD0N8U81wXYnpynuTPtnL+hfnOJIn7Sy7mkERQeKg" + )), + ) + .await; + + let account = database + .load_account() + .await + .unwrap() + .expect("The test database is prefilled with data, we should find an account"); + + let user_id = account.user_id(); + let device_id = account.device_id(); + + assert_eq!( + user_id.as_str(), + "@alice:localhost", + "The user ID should match to the one we expect." + ); + + assert_eq!( + device_id.as_str(), + "JVVORTHFXY", + "The device ID should match to the one we expect." + ); + + let tracked_users = + database.load_tracked_users().await.expect("Should be tracking some users"); + + assert_eq!(tracked_users.len(), 6); + + let known_users = vec![ + user_id!("@alice:localhost"), + user_id!("@dehydration3:localhost"), + user_id!("@eve:localhost"), + user_id!("@bob:localhost"), + user_id!("@malo:localhost"), + user_id!("@carl:localhost"), + ]; + + // load the identities + for user_id in known_users { + database.get_user_identity(user_id).await.expect("Should load this identity").unwrap(); + } + + let carl_identity = + database.get_user_identity(user_id!("@carl:localhost")).await.unwrap().unwrap(); + + assert_eq!( + carl_identity.master_key().get_first_key().unwrap().to_base64(), + "CdhKYYDeBDQveOioXEGWhTPCyzc63Irpar3CNyfun2Q" + ); + assert!(!carl_identity.was_previously_verified()); + + let bob_identity = + database.get_user_identity(user_id!("@bob:localhost")).await.unwrap().unwrap(); + + assert_eq!( + bob_identity.master_key().get_first_key().unwrap().to_base64(), + "COh2GYOJWSjem5QPRCaGp9iWV83IELG1IzLKW2S3pFY" + ); + // Bob is verified so this flag should be set + assert!(bob_identity.was_previously_verified()); + + let known_devices = vec![ + (device_id!("OPXQHCZSKW"), user_id!("@alice:localhost")), + // a dehydrated one + ( + device_id!("EvW+9IrGR10KVgVeZP25/KaPfx4R86FofVMcaz7VOho"), + user_id!("@alice:localhost"), + ), + (device_id!("HEEFRFQENV"), user_id!("@alice:localhost")), + (device_id!("JVVORTHFXY"), user_id!("@alice:localhost")), + (device_id!("NQUWWSKKHS"), user_id!("@alice:localhost")), + (device_id!("ORBLPFYCPG"), user_id!("@alice:localhost")), + (device_id!("YXOWENSEGM"), user_id!("@dehydration3:localhost")), + (device_id!("VXLFMYCHXC"), user_id!("@bob:localhost")), + (device_id!("FDGDQAEWOW"), user_id!("@bob:localhost")), + (device_id!("VXLFMYCHXC"), user_id!("@bob:localhost")), + (device_id!("FDGDQAEWOW"), user_id!("@bob:localhost")), + (device_id!("QKUKWJTTQC"), user_id!("@malo:localhost")), + (device_id!("LOUXJECTFG"), user_id!("@malo:localhost")), + (device_id!("MKKMAEVLPB"), user_id!("@carl:localhost")), + ]; + + for (device_id, user_id) in known_devices { + database.get_device(user_id, device_id).await.expect("Should load the device").unwrap(); + } + + let known_sender_key_to_session_count = vec![ + ("FfYcYfDF4nWy+LHdK6CEpIMlFAQDORc30WUkghL06kM", 1), + ("EvW+9IrGR10KVgVeZP25/KaPfx4R86FofVMcaz7VOho", 1), + ("hAGsoA4a9M6wwEUX5Q1jux1i+tUngLi01n5AmhDoHTY", 1), + ("aKqtSJymLzuoglWFwPGk1r/Vm2LE2hFESzXxn4RNjRM", 0), + ("zHK1psCrgeMn0kaz8hcdvA3INyar9jg1yfrSp0p1pHo", 1), + ("1QmBA316Wj5jIFRwNOti6N6Xh/vW0bsYCcR4uPfy8VQ", 1), + ("g5ef2vZF3VXgSPyODIeXpyHIRkuthvLhGvd6uwYggWU", 1), + ("o7hfupPd1VsNkRIvdlH6ujrEJFSKjFCGbxhAd31XxjI", 1), + ("Z3RxKQLxY7xpP+ZdOGR2SiNE37SrvmRhW7GPu1UGdm8", 1), + ("GDomaav8NiY3J+dNEeApJm+O0FooJ3IpVaIyJzCN4w4", 1), + ("7m7fqkHyEr47V5s/KjaxtJMOr3pSHrrns2q2lWpAQi8", 0), + ("9psAkPUIF8vNbWbnviX3PlwRcaeO53EHJdNtKpTY1X0", 0), + ("mqanh+ztw5oRtpqYQgLGW864i6NY2zpoKMIlrcyC+Aw", 0), + ("fJU/TJdbsv7tVbbpHw1Ke73ziElnM32cNhP2WIg4T10", 0), + ("sUIeFeFcCZoa5IC6nJ6Vrbvztcyx09m8BBg57XKRClg", 1), + ]; + + for (id, count) in known_sender_key_to_session_count { + let olm_sessions = + database.get_sessions(id).await.expect("Should have some olm sessions"); + + println!("### Session id: {:?}", id); + assert_eq!(olm_sessions.map_or(0, |v| v.len()), count); + } + + let inbound_group_sessions = database.get_inbound_group_sessions().await.unwrap(); + assert_eq!(inbound_group_sessions.len(), 15); + let known_inbound_group_sessions = vec![ + ( + "5hNAxrLai3VI0LKBwfh3wLfksfBFWds0W1a5X5/vSXA", + room_id!("!SRstFdydzrGwJYtVfm:localhost"), + ), + ( + "M6d2eU3y54gaYTbvGSlqa/xc1Az35l56Cp9sxzHWO4g", + room_id!("!SRstFdydzrGwJYtVfm:localhost"), + ), + ( + "IrydwXkRk2N2AqUMIVmLL3oJgMq14R9KId0P/uSD100", + room_id!("!SRstFdydzrGwJYtVfm:localhost"), + ), + ( + "Y74+l9jTo7N5UF+GQwdpgJGe4sn1+QtWITq7BxulHIE", + room_id!("!SRstFdydzrGwJYtVfm:localhost"), + ), + ( + "HpJxQR57WbQGdY6w2Q+C16znVvbXGa+JvQdRoMpWbXg", + room_id!("!SRstFdydzrGwJYtVfm:localhost"), + ), + ( + "Xetvi+ydFkZt8dpONGFbEusQb/Chc2V0XlLByZhsbgE", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "wv/WN/39akyerIXczTaIpjAuLnwgXKRtbXFSEHiJqxo", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "nA4gQwL//Cm8OdlyjABl/jChbPT/cP5V4Sd8iuE6H0s", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "bAAgqFeRDTjfEqL6Qf/c9mk55zoNDCSlboAIRd6b0hw", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "exPbsMMdGfAG2qmDdFtpAn+koVprfzS0Zip/RA9QRCE", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "h+om7oSw/ZV94fcKaoe8FGXJwQXWOfKQfzbGgNWQILI", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "ul3VXonpgk4lO2L3fEWubP/nxsTmLHqu5v8ZM9vHEcw", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "JXY15UxC3az2mwg8uX4qwgxfvCM4aygiIWMcdNiVQoc", + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + ), + ( + "OGB9lObr9kWUvha9tB5sMfOF/Mztk24JwQz/nwg3iFQ", + room_id!("!OgRiTRMaUzLdpCeDBM:localhost"), + ), + ( + "SFkHcbxjUOYF7mUAYI/oEMDZFaXszQbCN6Jza7iemj0", + room_id!("!OgRiTRMaUzLdpCeDBM:localhost"), + ), + ]; + + // ensure we can load them all + for (session_id, room_id) in &known_inbound_group_sessions { + database + .get_inbound_group_session(room_id, session_id) + .await + .expect("Should be able to load inbound group session") + .unwrap(); + } + + let bob_sender_verified = database + .get_inbound_group_session( + room_id!("!ZIwZcFqZVAYLAqVjfV:localhost"), + "exPbsMMdGfAG2qmDdFtpAn+koVprfzS0Zip/RA9QRCE", + ) + .await + .unwrap() + .unwrap(); + + assert_eq!(bob_sender_verified.sender_data.to_type(), SenderDataType::SenderVerified); + assert!(bob_sender_verified.backed_up()); + assert!(!bob_sender_verified.has_been_imported()); + + let alice_unknown_device = database + .get_inbound_group_session( + room_id!("!SRstFdydzrGwJYtVfm:localhost"), + "IrydwXkRk2N2AqUMIVmLL3oJgMq14R9KId0P/uSD100", + ) + .await + .unwrap() + .unwrap(); + + assert_eq!(alice_unknown_device.sender_data.to_type(), SenderDataType::UnknownDevice); + assert!(alice_unknown_device.backed_up()); + assert!(alice_unknown_device.has_been_imported()); + + let carl_tofu_session = database + .get_inbound_group_session( + room_id!("!OgRiTRMaUzLdpCeDBM:localhost"), + "OGB9lObr9kWUvha9tB5sMfOF/Mztk24JwQz/nwg3iFQ", + ) + .await + .unwrap() + .unwrap(); + + assert_eq!(carl_tofu_session.sender_data.to_type(), SenderDataType::SenderUnverified); + assert!(carl_tofu_session.backed_up()); + assert!(!carl_tofu_session.has_been_imported()); + + // Load outbound sessions + database + .get_outbound_group_session(room_id!("!OgRiTRMaUzLdpCeDBM:localhost")) + .await + .unwrap() + .unwrap(); + database + .get_outbound_group_session(room_id!("!ZIwZcFqZVAYLAqVjfV:localhost")) + .await + .unwrap() + .unwrap(); + database + .get_outbound_group_session(room_id!("!SRstFdydzrGwJYtVfm:localhost")) + .await + .unwrap() + .unwrap(); + + let withheld_info = database + .get_withheld_info( + room_id!("!OgRiTRMaUzLdpCeDBM:localhost"), + "SASgZ+EklvAF4QxJclMlDRlmL0fAMjAJJIKFMdb4Ht0", + ) + .await + .expect("This session should be withheld") + .unwrap(); + + assert_eq!(withheld_info.content.withheld_code(), WithheldCode::Unverified); + + let backup_keys = database.load_backup_keys().await.expect("backup key should be cached"); + assert_eq!(backup_keys.backup_version.unwrap(), "6"); + assert!(backup_keys.decryption_key.is_some()); + } async fn get_store( name: &str, passphrase: Option<&str>, diff --git a/testing/data/storage/alice/README.md b/testing/data/storage/alice/README.md new file mode 100644 index 00000000000..315cddc2163 --- /dev/null +++ b/testing/data/storage/alice/README.md @@ -0,0 +1,8 @@ +This database was built using a developer build of EXA using a localhost synapse. +Contains several olm sessions, megolm sessions, identities. +Used as a snapshot to test migrations to futures versions. +The database file is exported using the Android Studio `Device Explorer` tool (in `files/sessions/xxx`). +In order to get the passphrase a breakpoint must be added in the ClientBuilder to get it. +Passphrase: `/rCia2fYAJ+twCZ1Xm2mxFCYcmJdyzkdJjwtgXsziWpYS/UeNxnixuSieuwZXm+x1VsJHmWplH+QIQBZpEGZtC9/S/l8xK+WOCesmET0o6yJ/KP73ofDtjBlnNpPwuHLKFpyTbyicpCgQ4UT+5EUBuJ08TY9Ujdf1D13k5kr5tSZUefDKKCuG1fCRqlU8ByRas1PMQsZxT2W8t7QgBrQiiGmhpo/OTi4hfx97GOxncKcxTzppiYQNoHs/f15+XXQD7/oiCcqRIuUlXNsU6hRpFGmbYx2Pi1eyQViQCtB5dAEiSD0N8U81wXYnpynuTPtnL+hfnOJIn7Sy7mkERQeKg` + +Username is @alice:localhost \ No newline at end of file diff --git a/testing/data/storage/alice/matrix-sdk-crypto.sqlite3 b/testing/data/storage/alice/matrix-sdk-crypto.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..51a53912671716b4312f75a1a72f45f98b515031 GIT binary patch literal 249856 zcmeFa1z46#yEaUBhkybS(n=VlbSQ#|2nZt5NP~0G!WR)eF*6b64CT{Gvy@$xY{WGc zZ!1L0rz)t(4U%s!*HX4p_M^;pSwERpGQ*`iNWYTmFRdV1BIPVGRnkksl9vkq`~Me# zs&qYRts)1}>UODFiAlw&S@GEgS&1>dbF&L_V)Bym@=~+2_?O@1#-t~e{CJrdmme3C zUy_p)lbZPJi}bxid_6;b4g781q3iZnmrN3(v>gg5e`}g8B%1bGJwrJoL*u~2r zIK;oRXGo+$fN!KhP?u1Hps>Kemfyq=`iYbILNt`U>Gq=ek0{Jigf^!}>%_fHA@ z4^0JyW3u8hlm11D@9OHMW`Aw@Pv87( zy-~eT--yuv4B(XP4EnB-L1Vdp!8fTmCp9-NU*O{Tc}50w*ub9v%UQH*CZ*+XEGp6% z77XN}71i$V^!}A#F?PTDv}u)86KO3oGtsJmMkE?t zJufLgKQ*g&-e2Bm{YCe`eD&SY{u(C#Nj&{hBPT5>t9i?RF5TkR@K4QVYC z6VYm|MjZqxFgK}BK~i3R-k)D<{zU_$KY#VpYTvK>BijEHHJH}&W3mhK|E~hy!a-9? zE5PLM^!&3>F?N4`*Cbj^T1!t)v`Vp2$?S~Gn3T9YM$Wgt>VH-4$HzYv`xEBBQc2Fj zMqNtFTkr4W`cYqu-H%rqRZ=RAs8@Aq6evIUdm_wB%Kh_e)xQe%=dXSWSHNrj5#)c; z;zyd0vv5%T&k+4{gE4l0ey3Kd^bOGtjS8eD(w?dLB|rWu|5flGAO8;tZKM33!TqDL z7`q>@R3a6nv@CQ)D;qOgVp3shLQ>3k+`he2`Y!MH&it=M{^+H@eNHWmexPnq)mmO! zOGihv+Px8oZzaC{tMFG4eqQ{oYxz;bFFVF>W&bei4}<-EuhUYlvD!cT=O0qCd;FtD z3poob1t~3eoxk(f??vst{Z*`LAuFxr@_#XeVkrOOQ@Ot)@asZ9o#MZj z0|Vrwv|^n8PN!ct&4~P^-11fWGSXUI97LZEV+_p4#;FKhXC8t7U0OG{}*JN%tZf2kj%{#RJYWJ}VC zISADHHrn}*1Akia`>;dmuXg(}82Mu)F0l9?#`^xfz&w8xF|w#?D&9Ektd?)I(Dzc` z|495bF#f3G$FTFC53v8#=C4!9FGkQ7tt6zhJe&O;u;0arvHSi)GD%cgOI7v92s*tm zy-@703ZxhQjAHh?-v^_A&PD%{zzX9s3jTFOEoWgYCZ*+|`gc;L7qU|?6zA=vjNq11 zQEaq`yvQw_nFh#gpoW1YM)MU7Wo!&yv|$Fug>+J`mmOfN<)9-t-^(^RR^5Ica8*HML zzc@=#QR%AitX#<)Nr_ffK3R0|nE1H-gp`>4?DV9pUq^t5hAEE9cQ(LU4>=6%7UIfv@MdN2foAR=2+p64KXQLD~cg)F+!}5S>Imy#kf2#9*fP;ziK+h zuk`WhjrHz28IFpcsMmF>>LV{%C0(8ND-&d8=cgv8CN$m)=lz#Ltp=y@9A^RqzLU@k z3v3xzGn-Os> zfy;93n_^vRGQPC5ZiZ9Xj;Zv{y`6tT(OjboV{oGa7Rjbyz5;eLPH)lku1gZQdw6bw zRrJ9@g>n)$RY8Jz6i#P-Pdzb(c7J08HMx8&Z-Tqpc!ha|QWSf8W)LM>kd5ms zftQr$Sg{9=E9c^2Ukt`m%$4?*vror9LpFb-%1c(lsydCg{@HA`q;cx(Z=3bZPQtQ$ zTsKg}YPLDV6(>-ufcI^w)q09@2nzx`V`>k2-gVr?s_tQmJp$-%r!YYUM~KU0i%?Ws z$l{O+wu)mu`bp9wA6j8L9s#W|zBGb9bKs{2?&Tp$7#GItue;$U8n75EGw9M^*xocO zV>nNCWLu{O=gUaw)%N^xoA7JU#CFlp)wmNDVT83YsKI_IOw7cgxOl~IR=&Nd1%}}y zv%#Ep*lEHPGK&H(4aZe!oKy7h?1=$A#F%;p;Cja9^2X!L}IxDC7*`#xQv~cgQw+=_!8HnZ4Lu| zAdPj+FN#Uw13osDSHQk_OjT}yNnUtEcu!&-W;Dm}6b!-b=uRnZXx))Sb*Q*>R|2!U zs|uE5oT4>e`%t(C*cFlC92kjtM%@a_C3yW&R=n07_Y80Y`@}KYIg}2*%0ImXwXMr( zhIe$+iSc+E=p@?(JFPG|2hR&|pgAQS;i7{v<*hK>v&0co3vf15lvsYmXj4qUyLRDN zkV~BoV`LEOYTZ20o8ID?S7GBbsE!K2mrE{<% ziecHm3kGR9;y_nN+{Q&~KSs+5(YCmsl18Z4;go?*hi=Ihdf9HgWSbA;XFSeZ6ou1O z4mZJ06#_aJAC1bX=M6g39o)9V&@`NjZqBs+)zt~(#aT;rbAEFT^MiZ!q6e?W-b}jl zVD`heaLF7`^)N&WwV?qFiNS1j8r9orj~8jKbkdi0rkI}Pg?VlmToSKphuLNp?DU3W z3+`iTAB8rU;70tX6O`r5E6kYJC>LZZ6M`_DKKF$c&!(pRtqc9EP;ZXuEl^)6i2Op@ zd2~r1P0ZCUz&u4X1k;=Q;ecOC4rZ02u9-!0i92>N{)ggdD{ox&qGb+nv@@Ns?4o^+ zV?{Fz4U@9%QY+c#&{K{P0OB@O;`8D-j?hGFJnb(8AB0lVMe3BF4NY+ zj%*e=gMl7x+GBvMMwE-VVVVoZsBrXxVJ&;o_10k{PBrOJj0w#;%i@uhwiPzFz!_-_ z6LIBmY61=!Q|T!`(~biM`hem@uk+}g_Gj8vay&N4pO$LLv9p`A*3 zHe*p#)0Wv-$}t(o#n(B)?f9!c^1nAQ>!XjQ9%3>6+B%q65@mrms_ih_5KEh5p(LJ! zn5aAW6VXRW#vI1<3(1OARYq+aaV)_Zm4GhT+|~{^I@mCk)#IfJKK0`8Y$UVKO8R#_ z!{!f7Uz^(m8>q;}N@-Z8(6tV+Q=ZuZUkp95rjv{w-d$$0z2@NExl5I}ghW79U`Nkx zF`fN8gmA6KCnnrC#NW@~+cVU^OHfRpXQ+3_UnhMLRRe6Z!j4KjvB5xiiT$m5x{&sfdXgSltVsb?Hr)>7O5QB$r_|h!ziFLcX`Rg}7w1s)6m}5)!!Zl>h7I zny97-Vt6^$=zCypZV^q~(2gN;jP)!`V#s|#y>%0Y!6SOn15-zSJUqgRjy4tN`1(vi z4?HOpBu_JDr{S3I$viTuoF3YrL)luVmcHzsu0`2}7nosyxev{7Gz8Z?F(p?8Z{vDN z<5-|{Q_R)FfMjP(Q8YJ;W+J#!z?v3Qqrr*ho$*`&d$q9D2RGuniUjb}yBs8L!Yu7@ z=I?dvtd8QTKHf=Vcs}2rrsOMI2(GB%UB_3xAHtuF+PG$cT>+G96egy-v6rl5(ya}n0r+W1v)@_+ycG{R>b0=(#C~C=?hlJ7pkKt61bzgh_UBNEl2u179By$Ewt(obj z&`}nJ#wm#~Gn~s|q`crBCl@ah21@@D z%yy=nFJo!J8`v1<&WZ<=iDA7h9y{h*_7OK`C;O2jm-ozM$9iFEN2;(J{lnPPpLFo( zm4G`j`S@zb;Bu^v6qC@YvHW)PSG_1bF}ZO?q-s-eJYTmC&Gt@-oH9cX|HZ1PxW96ewTs~OmmZaI!XDa(&M%~oPAF=Vs5|XLwDxqUQ6lG zdGXt=3TwXJ89aK^(TWicV~1oMNj~BA>q%L$?VK085TED9S+f3WxMCH@C5LT^f_2x- z2t`qEuB%+bUHt%y#+8!+c-8@vbUABv&jDA$T4A(5Zs~A|c0aa6_Qlt(I<6R75frDw ziO^-ZVqa>&wS%MO;URdcnqtN|zJ|sH-BZ}qS63MW+u)6%0gm`_((xf@&&FVUgc z(=~9z3HOzvoH#W;nNwzyo0JQt@rH6X`c!0Iv^=Ne24RmrR<}@7m*qU#SAC!!)z(V>6txOdjWwOHLbf+&Y-ix1J?EGAV_8B; zHs@W}Vu(cui(KI> zv>l~gl8(nYg5SRb5^6vxHjwi=nL z1a_DQE>&_Se0~9i8;enXUAS5@4y(O5u{?mF&fmzr$VZ!CmLMB+a?rYD`Znk{VW%*$`!sCP3gwCs(&;zrSCky?FO z^;b?f-^2skT)e2H!7h}S$Y7VM1!rZ?@yf9@%P=n2Omm_JU8>ESJQxQo2(e(cUOd>#PjHoDkF{bf{D-4B_7v<5r%*JcuIG_jRKziF_V7Bo?lntfKCPlxez0c z2kCU<8XDs!y_TFS%#l&U=^<8Pf?O5xZ0Jh~!>tR~WESr4Q~y5p-qZ(+L>+40NN4mL z=XrB%wyWtpzvX5_%_gmER{qIHJN{X@Y_46sWTn>B$LaIBD~#&;H@Rvz!M;TAZ%}Ar zjm@32>ACL(7zO8Hf}>PsAni4Us~Qgtu*8Uwxkq20=2oyT)j&gA%uy~N0PZ-_ES(Pg z#M!_XN1eDDznp3;kL{+|6}Pob8Mik>n@cgjoEF5?Ern1^F2mk%azcFvs=C6mdm{F_ z60Ch_=q6-k~IPr$~}qJHl^`g@mYe;dVx5o#H=)4iZQUnp4gx6 zjs_hYoa&IO5r%0!7(6Gf9C0!(nZn=23sIV8c9;p>`IJ`$rYfOYM-WK&3D>6HsB;lB z$B3xF)ZT108FL*|vgv0JBm_}Jt&gJ0-!8+-r>#W84;*62^pMR|PA0ACuE9 z+}N?$CP}@OF$QR~r^JI9bfYl96GuHtqVP16s7=8mPr|Fh&j%9Nw*`#bDHckMp_|0u zY!}%=Owqtp4|>fmKQ8dC;@Wo*Udu9PY-cDE>mKMQtV2_}y5d za?RBww;c|e;aVTAqD|4G05>sO9M_wnPLqr^?F6Q{kA(g%Q`Hyo)b^JC=qHA1zaB(k z7Uip_wr_YT`8rA&1y0%>({QXOrZ`cT+AyPDIx)@cD+F0dy&XHdS(=TBdjuQ5(@~Ar z&&~!!T$|eXm5S52PjZ`kkW=tjRvyT}O_}!0&;2mpyq7Z8MloEg`Fj-C6R@W*mM7-- zD4+u@OCu>@4Hx)sw~E6nUmAA011@Q@172v@0V55V0>||3fdev@uIB8D&tgE1BnD)6 zCZ4;UaHR{K=ed3!Bj!eCn?NacS8F|Sz>DERBG3y74D6$kf}zG?IxNw^*t(8&x=f#Q z;^>y^+1h<^3o@a^5M;Bn)9UC{gJIkm65MOwD00+tR2z*OglZp6CIlM zxwizis<8Y`ydGj8D#%t|H{^x{zZzluRcV1)xzpjrqleYygl3qlLA++Ucxx^DOYf#8 zY1mAekaWR(;PrcDsF-5|YJpw+^^$yRIdInA6sCT2*`yz51+lr#l| zm)8Qez*!7e>PY_FIh^P~(I&W%>M>Umqg621oV=NxKGf-G%MJx`*qW$E$J@eRk^lnC zsnZ0EX^omnJWb|;J^3kTXwHjxAx|`}$1!k`!~F1)#jYDQQ`0CV^z-m0CJm1@F{Dom zzCGUpTl@hMu+(uvoMAEpJIw5vY|iIOVw!0Wu0$R!#zXHcHa1%`ly67kv!fOnJ@4D4 z(GBZ#y2q+vSP9k!<76K>y45j)`~_nJ{p_$P6-Pze3r0Uy0#4lq2S<+JzIEfAs7qA#OeQ++i}>ihJ_~h zadg6>`eWfjW>w6D4T*cgFFq1a%n${u_ks+~FthHrJan-9ntQOM=kAX$XHa{Tfs#l7vO zPOR#4iNh94tcXm+{FZd5{cN$W1L?^(8PTU{)mk~bPCAZsGSX=#ifI!HrfH@jQpauZ zr9B;b3Le;CvSRB>vIsnnZe^x&Ub9=EP`^przBR@QkU(9KX z;p(igjz7Z~MRzedsui7Tv!N3c%v4Q;5O~q^|^iD$BW)|PgyT5D0D@ssr{lTgXd>58@j%E-9Bp1kumG* zhMaS|yK3lHuT3|dPid))dz{?&XZ5t?wNpENkO5- zl}o~9+bmn2VIkk;&Zds7YOIVV<+`uXcIkRE;?2P@a|uDA-z~+83kv;i4MfaXJW%#` zQ;$FH|CQ2|)+uk~EWkzOG0K@rUzA312Eh1#gWm{c5F#K%K!|`40U-iH1cV3(5fCCE zL_mmu5P^R$1O&IN>b;lg?@M->KC|z$-mh=VIydWEaov2tg>bpS)z`PoxK-GA|6iYW z=j(;G#VgyCoG;c@(%zqYxTSoJ#Yu$~?S_jbx6U2ec>k~IUN+_St>P@vBsZI-buME^ zU6qx>yxf})Z*TEi>SGetc>g~;Y@5u$ptho&cb4al^B6TjWn5!E z&O8>X;dmfXwTJJB_2yNH-t9KO={>0N{=azW#1TbJq>W0BYi@5c+_w9m`5J{eBfn~1 ze4W%mee2B4jraen-RF&;->vVS;@IP#inYB~^xx6?UVMD!7Txhj-$}1cIn{Xo|2wL6 zeMvI9ll zx@0@t8~gdyLkGK>(_`KYw;NS(y5yvqd;gP0AM5Yzcwz81`Qn4@;ClBlBRrlS+0plg zsgmsFk5@Kq7?e0uBK~hUdr5e0h+V!`qzNsM#GKS%tdC0XM$&C9-rH)#l#~8dlW8nq z%aUBV2gi!MS{k+?OX#62nUPa4z*rQs1SF*ek%c6Ejl>jpviXLQ13buq9KA_=aVV^a zlp`Ka!=F#2y^X+zFtUmIS(}@*PQd{eTov~h(`S22FeZRCog-IpX-CqV29cFELqr@e zLW+4-j1^g754zwE*^K1hJ;3A=@}vaEHE#2(2*B;Umbs**jgx7DiN%dvwq@vNM7prE zn=;wNC;E_KHjWbC_rcu+vMNtwzrQRAb7Q5*M_ZSSnqC2Hcz+ho*;bm7pm)uTbmm() z6i#hUT4K=f9_6U1hP{=2I+7l@ps#3e z1I#xjS+UNB4eyVjxEsjee1$J5`07V88k_0ItpW3K!V2@{iOd+{x}ujnxtkwRn`cGH zS45NSchg23GkJ!K5pk_053tSykL1ci$U|F*$$c=qdw{)NCe`3hf+eXonXY&rkAnf2 z<%aV%cp|AR;3}(GmP&LWMfZ)B6gFiNx2J@00ttr?EJ^%bhM{(e=_C+8^%BSZSX|I1 z*qb`o2J<^^f^g$rhW#SWl+EQUd1>SE+B7D=@FSLTi8^v6=rs!fIp2(TG*qA^?m(= zaj=}iZ7(D3>Kd`pG0VdJ^lCXxkHf zJ2Kj>x zcEJ_8&H>v98%(jm=5!JgztHT22$8TdZ>@bzL2D@F&i(B8QfW{F|&jR$`+_awkVPr zV6?*7bZhYaM;4wFYF9apS*Q6rI^m&hj{AcDVrI!>2=kcF(Zp{M<-K5weTvuLH(wI=sS>j<^JW`LO zDyss^v9evHhCbP(+&P4EVt83#kEP~#T*lg-VUKfD{dk&Zl12{<(ZHbcT%I|nK$NNl z$G37*3|9G3fd1T$)dX9@@HttG)cJjM{>x37^aqmoxgYDgi^VX4`!Q$Km$zXgO(U21 zqf>e}ONki|HpQ?^?5PlEb?s2W9icsq= z85r(dq>0h7xrNHz{o7!swFkSgYBHo3Qv?Zd|kpiBvaQb0a`fVA8m;j zfpnFl{2fPxmS)R}Wa5fNDZ841EgrtXC;N|L&4 zuPz@8Dw9)t5{!v#>1MW|i!1k~H}0j3YccF+Lg87SIPA_#p2xIEV5iZFnQ5iDm83dO zRfJH@jlGH#r{qr8xN!dMu3lme4F+>}n@LP~yl}kD&;g_B8iwpv80;(Bm{0ryWn1@_ zUa{m+@v%kwckVgv&R(1D!Yu z*nnes7%GoN;{5JB=97y)>1g)Sdhg&_&6rY5b95^Bu39N6JZWmSD3?p zYkeK9u(yMVdI&aq+n4Eg7r5eq@-En|VW1=5GOeJyny(#}CiUStu1%TCX7iv?oJ#N8 z9s5(50G8yHu;bvqnVuP{z5$ruO%HdSnU^-Tvtf8lr!5EeR>FCC2@F!Ezr2lViw7>4 zZqHEJC)xwkimAeL4_l8!9S$}|c;}QDV5mqp>_|7}NsG;}UMo^paGvEnhT|hOUCeaH zHpPgjpk`Prh0E@GxSbS$ab<>{Q7kkVTPxhz;sf?AFQT*UnFQCSnP7?=H9FTd$A{Tt ziIW(E_dz<|YNeI5N1Z`l78WEIb-^C)g5$=6eb|_&!M!RMaJ-WXR$_v^A4f_PQ?b^7 zI{=2$eYSCTPY!FIz)CM*ff?$>i1=XaFNmDnvi&^PdU&*fS6J~X1uDG2}pM8g$s*znV ztE6cd;r>FX7X&L=nx>id6%E1>6(?GKZ%{{`x17!t+z(qEn9o09lQ*VCQ1Z3-V#R&2 zt5noUuV1Bw!=-tI=PC~p#llj{G<;SuW7qi@6BV&ESD!-GS9BNbd^NS8ibEqRsPbug z-9YS%!)YU3#@=mp16@YXM4YIgJk#w6$SZ3*zMoIcceTR4;A9S}YH`9_7L#onJ$5;U zDN~bUa@|_thEx%g7wuET{4o@d<0`1=en#~rPn?RTR~%R82zYo_B~7uF8x|hpCO5-k zSy&1#YLv0$PR!KgSY|yBT4b>v9a+h9=FlT_`nT@b(G@Sa2}LlrTti3n3}HyzW|0P* z)Cl7_v-2$17*p_==$`RtC{k-h6yBKTy4cW@ zH+1gIJ+G&EQZS{g&gFAVffjfDnQIg$M}N+Pf9sY(M?Vqc=X&l5fu4 zxhr+V)?=4$jca=CSlbt$CtF1?P!<%beb(_}>Fo7aW~ClEo!e{P;JcSwX&QyS4$2H` zxX{1bIX5Lip-}M;dELj{xo_q3GAOJ>qT!^goPn}3k{91PTKsl$zx9fOLZ+X3mWyTw zI~2EGd~Mds_HWh?D_lJ3aDR^m1Amd)s5dti1ck;fS?T9gtI=fJJxh&~-mlx}z1beU z?XL2)BE{Srw+mBWH?IHR*t6%9yx7F@ZlT4!j|HW&?#E86J%8A^{vYNbHT1wr9g)qG1`dBe zI4%5y+la;e?;ad@a`Nk!O+L=)-MIdr)v4W%o4sC*->0t-WjMrj;o@C$tEaR|YNF8p zLq_iX-JKfO|Gll|&y4#tAa!6`)o1r@(KqI0On&M2X=;xf7vsIhi44eZT>rm4F5+=W z{DipybF((8#n1j=BvHFyfwb~%>5JJ7MVEJ$G_LAVffjfDi#80zw3Y2nZ1nA|OP72ng=Ar`$Lh zH^@a&eu4A6Z6ZU*b+u@wH}c8t_ffS4m&ck0A*k{G|96EN@BdG!uJkjo?O=1GXzTIq z2eV2#sqafyo&DnIla8%Tu4u{FHQxXKu2AFs|L+Pl-v9ruP~-jo?+P{E|NpK~=Q(B8k1gN(T^JTvLfNxqp&_3()Sp?c>8N&zDz+L zSv>Vv>E1jxA8S;|kvd4)_AMiSEY~2DZ8>>LqxG<_$fQSC(yjWJHOFQXoDamuBy1#u z%mo9>FjE!R5|uD0ikzrzq!jTy+x&bSiX~TYOgC2YCb2+3;6B2tpG6B!{3IJ{x|Nt~ zTx(MC{aTR@J>IGuYja6E{bGdc=43H`B@=cy)|L68M&5w@q+{h_co9U-&|PwzMua+8 zR^mf3Y1unkXseO&b%~X4(hMNw@Kvs%Cu%gvtL{&tb~yiiN*37z4R4&#$0&<(*EZR# z;Z!O)(yK`oTBqm2<`-gbTP(|w@UsrZBsWszH({DTX=wEw6g+WDCDfm=Ea_TGzR^(e zQUUWfx(hBTlgBua#Pb>w!tRl=hv<%uIHJU7T32I#A9H+_PsIq*!%V5|?MOB9+@=ud zAtrpLJLWe>e;rmd53_pWfNzs-q*@)ttUOl5^TfAfZRZS@UPFf0M>K?ClMhK#Ct`wA zlgY!+!jY~e9(ZrXs~hlCz=FF?ZMI>G8UcBVac%gN>{wQK+EBzao@BDkm@10_h7@28 zH9dzhV!fR3C9=pGpVU>vFf|L0b;LUv^Qdhy?3U~L>Z|u4ae9+L;B0KjD${nyoTixG zs*wb?hEF6QU+aT_t}dXF@o3oAxj7glFGH*C7tnsmL*7lY=$B@g?@m(LAX163s3#WM z(eiK2QWAS$W@}1vNFiMo>YwLO$X%Bg+D=_a%AAlXO;UzKqv zjl}2&s-}3s2hOwDAUC@db?Us67u3`I%dpqP-Ajbf-Rn!LMYA$0A9&)7~97~hVYDR;!Mc45c>g+ymp~I-h0tU zMvx9V+@5gG<;7#nFx$Naee@xQH6_%GaZW_L0_U1^#0`IzI6yG=@PawSYz>}jkPf&A zLv+m)^SU>e?~rEi;9lB>ZZg`97)&RRd9xvb+(Pk2V{%$24Jmun?W)dqQDHGx#cZ8&Wrc#ie7FdNrS*pQK$i`fMf?}(UQ zVid0Xu*TgurV+agcY4-Y7#YkN($zd3_FF`c6h9( z=k1HBfz5f{<{~wWk9WjEr*uV2(y+G?w^m1tayBKWZ~-IpYXxRT>StA;fzP4$ zu=Vb!ZH;@Op_KboUXM(3OfF(%oX^WI@^9|rZ_i36^(rp6r-}OEx(51N>J{|u+B5~5 zvvIE@`a5&4##s8t4in7GlsBcPT@CbOc$|vV1L80%gxr2R0pB+POM^VIFQHjwL7qcL zKI-6(B>`DdYRiaPn%R_2bX_`EL@775C=T_#c#<-Y<325_tEh{2?v*@Y{;53POXFlg z8(M5BlLivV8-3ZAUON+0Lwk~E&1aiv(}?xn1o9Xu$zQQ7f`x(^DHDhUkJQFceGO4D#osYdkF-<#I{4E#rym~7GlW4zE`)2(L-^W_%YQf=X+!pJ*Ba7Ht8ytBr7S=`OQ3Uhxs z-0^5HXNlQjA)V<;4GiC*5lSpL1GB>U!E$z5BZ=N1Hg^MWv}7vU;+-}hAVl7_dAB5X z51-`4@W!DM8?EwjuRAl(XdA;EoNkezN|$(=n!}`5XTp4W9k0}6S@CR4>Wb?IG~;DC zXTbv(=5faZw-iv&u}tpu?Cj3OFhM`mjE`b+SS_n9iK#vjxZrG?<}Fdk6WTlHNR!k% z8XtlP{dGo2Ljj&kYVg7ZII75l!FyAWgG|YnuqP`S! zT{2#Irzhfcb4^OMgZ{pODLycXN>eGSyqei%2m|eLNA@kNaUo5YD163TyEOX9HjE02 z$C@A-dLZ8Nk?pMEoHDOHk9ylAtc>k$j91DUOhhMCTD$xB(SQ#yg@cKDhRHH^9`kGh z4V5cV<6D4z;h5bskV$SP4!KgV_sPBK5E~n#Wj=G=V`T>n%%VR};dOUe?lg(fn=xLl z7jF4FGO90yrFBWgF-m-g05{k8s#fu?H3z7{^y)Y*uNBTOWM}=BDPod(&lyvEHv9!RhwsSAZoIMBz?Q zs7E;k*pB-xa5uH;g_&D;jXNLf?J3YXx$H-(s?+&Io=t*apOCdh8k=Ybrn3|gu2FkEMJA~`XCFc!1Yg+==3F zXrd)HD&SONM`o8AmOhck8tXg++aGp@WZ^^#y?DJkw(@aAXEOU(T65fUiQ8tHFr93{ z3x`r%$`@?Yc}+X4;5D0mZV?DGbY#;{IuqqfeoWg#b(lYfV3)gvsh}(14W6Z_VEtcQ zd5j3x{>Lb9{I|R36ACOuK!|`40U-iH1cV3(5fCCEL_mmu5CI_qLInOz5fE@U=FAtD z_b`(Gq}zP1wDmzx{nt*mgYPGOP;|T7aAf{n*$$2C|K|sHaqbxQ7HBZIy7S4<9tYkn z&}=6Uw`0bKFFv$=Y2LX0|GU-DKb`+)Aw2)@-_&LPKNNe2xwznwwjp{IN}G-@zu0ZA z(U2ag?GMdJGtUSwdZ%_VHr-HeNJ!*GwNn#}->78eYO2hjhnrbAx84pG(&zY zcy{sgSYw_g@*GR!yWyo^c5G>IWF^-Ha59C{;?>=;)=X8Ug_=8FYhhTIG7nbsS&Xx- z<4ti^jBW40=3bm)JJPOHO)OtgkyD_%aKw&B+u&A?qbO<>IVZQQw2%{>!!R+QGsk@R zc9agLOM0QEoD-Uh5~Cfd#vA@@#HZFdO>oLmhjW=j47@CYahWI3RdW9K1qQanzSh+A zDW(}>MGA(rG1pbn%|czgyIE)TRw1F(;xx8)*Rt-ysntEPc+9g%j*G>#&cmBloM$|R zRb4oP`k2dpgFD(&%)w$E5;;-1RWJoi1%?G>lsj~fVp{`cTJZfjt^yIF1`Iak?C)Bx zM;yjM4?ZNimm}tebk&LS#5QL!R=cZ@wkjT5De}r{E{L7dmg5ZMSX}Pl%154yb8ef5 z!Z^2V%|m&%is^8ra*8h2^;Bi)cc_WLGM-AKoyl3u&lxco*oMdY5!1oBTvdCRjhWrB zxLr53Y}}0DVrmdBh*7XP_!34%1XIVaIYB%OE6v0$9MmFc!^0Z5V#I~a`Ptr9_$;L( zQ^57PDjm-1A8H@PiRWpm&C7eCj;rmST*+)=&Z+rrc;F_-x!UEJ(A-156X#y{a`N{c z<|o8pSQICaU+~SNAhJ4KafY;yjCplBxk5j{I!nK>LW4N5DRt7ylaAMcjr1&f;0ZE`1 zW8^rOK0Ae=*HV^yA=GD#c9WKzC9bi;j$~9xl;KcW3g?!eVOBRI^cOs}mTOG2S+E~h zTMeo62YQ)+hf%|?_7Tf1oh0b$D_eGESlu_{(#T|74pZVB^9OTXu4`Q2WcyLZ&{f*+ zW>Getc;i@CJl5c3@@!p<%QeNlJS|_`Q7`12`~pfQ+HhqFU@5wU*p;KSA$-- z&;$=P;`?BrA472jgXv0PPg5T5n1Ne7R*kWG%LIdpDB69x;c~7X-@=qgLiEOqA`izC zPhPgZgs)EY#T_Z!l)wq6L=hg!guB_86od1=KWDaaQlz6OcBP~e)rWZ03j>ST!WE+X z1#7LapuImDdJIz%W2zdj|GQMxDs^`09rKIMy&t+xb2wwEaH*w+k)OHhfwy~lR4>n5 z@a5O3N<5&IEzx`KuAs}fhFJpFJhoDtvHW>MWk1kY53;~`Io$3|M>>je<(Qzu7(Ah7 zO~kG=?-QQkMVitGDZ^y?-$6TXdf$pfEVAVlqiDtZysCjMPhkdqnZ$r!Q5uD_Tu*sv zM7>tz(;=)FJPn3eqRg~7D&IA~Emy-ockYG@f#g~lYdBM+QEnI(=tD*mYwd>vX;@jA zhL=+KB3L}xo8zA5Sk}7>&y&ldt{X8|j!rquf>M6s6QrHF7Id9}?_naGW#H1$!xYQf zhvTkP59e4svZCZ)X@S#}<}gG2Dg)zoD{S^bb)1zICMikqz)(-B^0cgg`F1|)@^Ie` zpQ8+zpkEr`NoW(h;`aDz-$9jDyul(5l4Kb(BU5qP1@)r1;^#%^>NHu!$8Ky&aJ<=3 zyb8x>8#cKwg*bjNCjy_brB7i;&3r?8%>8tXv!)Lc$oXjCYOHn-Jm|=0Z*$#QHqpCx zt02B7ooSFePP)X9UepKI+UGFhYM6N^;X^5^!*pU}OBmv}%ekoa+<`XeN6ltPW+>8^ zw}MZZzS@=M8;o=MElrDko#^S;@F9#fe5NmtF2WGksC+JWNq1|3X_A#(P902aUP*~3 zxns5z)B7G==>e2ED4JkkDGhx|qYoZuQ>7(^EV^AE>-ESqFlTbuNnpQXws<V<2vv6vf!Gx~Vml{A|a>^GrrjlmExEGYE!sKjR(jPPV~SeqT$7YjTU zx?nD!6@G!rykNZ6rbkCh=h1g3x=1keYMK9P_%kF=2{jB4@-_P6VR$MIb;Yw-UjqNa znzCM}6*e&kuI^BTQ?3|Vq0Yc+z$%{Ch}m))+63rXmIYo!_%uOPG1HJUAL-4Dx3bmU z^wQyNahwD&M*nLA8;bi3+jSL66ERXbpSm6}!)@R8&TTRX+B4?wwe9J(>oBj41@=3l zDkHfAllul9&_=P3nJOq?QMM&7TNy~>y`uZh>C)5Gw}rSi8(UJ|j7sv+wwVTT}Yi0J?Rd{=m?eJ#jy8JpJi+&Cf z92UA)aN^%4%!w8Z2qyaDIoWCDm3&ZqLQ)|%Sf=LClLTy=heUa_CGNPJ_r{Pc>OpuBN6$Ej`9^XCm;YbC@%h zFst6r!W=*9yaN}FF{glF9A{*0bHwvfF)q43%@-V0_yP}7=u~FRZI?6MT5{p}X)v37 z(O#^VAVBLgy;0{*9tKA(guN>(9)^w460H7A9HVK961&gA+PWAW(*ZM$@x&G9B}{ny zW@nrYz=9UIp@*G@SQCTKxn(@tGF`HbsF5!Nxn8fA78`PDDPl<$!4`HQZ!ug-iuI2# zzL2!jwGh*Flw- zgj|e=2#zL&2H|9yBO%_|h8gx#7-o2>_a#k7kr3^|ik6tv8bh2(0Jv`_=p8;pa^Sj5 zR2%z@iPQ&M!D+GwVzAjjS_Bh}>3wfyI%$vzkobyCt;2dhZN}z0JA6%T&%Wjkn-dH#cj%TS2VsdXMp%euW1~1e z%JGuTMl$9mxFv#(Hdtv;MlTxXl1?>mFn;ST!jml0XttXS<=3$shLv#S_?$jog@>Vx zv90VWcwD!%i4iS)n;qH>ntTD)HI3$wL}Not9$?b_%h z({BcrFeA*y#W1W%$9^yNeH#Q6pWF1%IZZ5a=F<`_nJKX07TVdB3)2S zMA=5UKxwY>R;33Dt>o`1X(^smidM)|^i|lb@Ktdbiwge}A|OOSh=33QAp$}Kga`-` z5F#K%;NJxSUx~m#(X_WSX71l~{(k1x_(^Xb1iovymR%gMX+vzgPD=+gIl0s$Zc~|i zZH$-A%u>B!dG|HnK7CW0`{nI`Er~6boK=`w+FiM^mfzjIpV(5H~wP3DJNX@6#h{yUmEr=KCz(dstb0L9X5TkP8&TX+tYKi zNAIBNu>%4}HWQQoN3|H~oq5(!w#>ce?b?CUFC|)R%^%QtS)S#K6P-@F&Xjh#Dc4xb z+OOko^$>fmZ0O-+ZNIemhHd+KO*&&ywEXFWy%A@kS3Z4RFzAEt(ugsk0VAKd^hvv^ z8mzqA;N4i$a=R+GLvs6N8*9l@pIOm8;mFXAsk>{^9@R`}*T4IRsgb4V-4Nd2v@UC0 zLTfV}W1ZZ{=2xCpZW@t#Qoi-Rxl?s~XLpkmEh?T8_*&*4)#9vs%XFXAP~X<}1Lv>G zHJ{dZcg3bYyLQY;eNyA8x1#+~>3>v9okQrST$3cLr>~9#ZvEmbV>Wm2s5Qz_Vi8JR zj>Z?Bogvj&%kR7{z}@`*j3{TrYpLOH@^|c5Rs1;^^VZ&MY1VH3z>z0I6(s*rEyuzK zs%)R-svA4)t7fIH@sDcx-P))4KdR+-%MxNfjA_~5 z;qtq=v>)gH{c-kR<4J(u&j0&<`k&Ief0r~Tlv9X+5CI_qLIi{e2oVq>AVffjfDi#8 z0{^ceAmEVyZnmfK{J-DbKB@?6`rTYjG_Kg-0r9K#a36p_9-ffU6@-1lUP-C}{HIBx7@O%~jlrhK7f^NFgG9qM}06z3I`&iBXqD|-oK2@#tR{258t!Ly#z}Vw4Wxc8}@C+3*O*; z7%#u?5Kp${7#Z?XZ}O)Go6~!{MsXL??m!tQoOZ|UP%>Pr$ZdVv3Db6~3&O-?Y-&lC?Tj2ejo{gb=&x7S z)<{;9OwZF9Hh9&}x>tlIF8N@Ke?h2cP^3*7&ylpV$DTwoWOp0)BA<8(PcT#`BlN0u zgf(feS1~~cj5*@fXfIGO{aoV4C4O3Pl``QRiNXkqUk!`U1l%1%@TNdsvaI zK33(Cn0zi1U&~nWNH8sPbis)dK5#mK)$a z&1*pS{;Y^iZONUVYr`EtPpHsZdft_AesdI6E&S|F3gx}9vPUN@Yta+)Br|X>OWR*? zQsgVtH_^kVX!6w`k&V6yuS|LEGMrQn##eLh`dn ze0~Sd`_UA8a62@{0gw3rV4#*1W1GjBonu&y)TZRidutf=Wsr|@!3)uIFWaynNhu&w zhFiF|iQz=JI9>0KWp+vkzIVhbuWp7t2+Z)ZAW1y*VYolUbqy*#T0wN)+G)DPPg;69 z1}B?gvo|KYheURbr>Gm~t*vMcEu>k8V@*GL?L|F|??5T$VrDc3)n_}oVYDe9X$mOy zp%Bxob%;5B`P9x_K2!Rxb64)Y{vm}0ReX};W<)f;b)p;J`}MTtF@E$-D;Wn(`C!0A zdc!1;h z7Fm-LSL{gSqga4h77)6in#q!ECpDw%G$p6d~TPl^~X72#hT&0EK6Dt{a|MCUvXWUOu^V;^JajpU;Z z^Ft!ziNrxN%0!+ISeb|Uv2@^h7{SL)>-rSrIO{m!uqKr|#79NW;Y8=?!a6)Hq4e%N zpU?qI|9Aj^upQyU37>o8c4cD*zOlwJ5smkaCHX~9f?te&o&P7JVkpKp{~2-*fau+S z6ZwJgM?gS8KtMo1KtMo1KtMo1KtMo1KtMo1KtSL>4FS;>2=&U}dbHENA1~=NRQ0qUTR#Ki`bkVmu_jyP97_txUBchMThfr z-^3^1__hA;7wBtzLoY0S!{&ssO0G6`vtAv0eg3sZ2fe5J-wi51dH2`)f5W<7yeX(y}T4wtETK|{4P@+(;@Frtd{Lz@!hsXEXZN6HO zX7I(iYTd}^GyMCu|F!=AXOsC#iuqzP3OnVM$0?mxGE&&7SgPXlpJre|UjYFD0RaI4 z0RaI40Re&kI|ww}NlS$c>hZGjl;Z|{tGK=D7p8n4n_o3pQdPP~Yk`XqPc zf4}=etS5%#acS)#SCOvc1Xo}8aH)C{+M@f$=5bx|2yO)NoGD2zn&FrZ*Tq(G5pj`0 zRR?$M=khZbr6!7ANx4Y$+C;uYH0qBvB-uz_{6LjUk4cKsSZSuzFOutLOL5GaJ1Q>Y zQ?ZyrtC(WQAOl~`)fEZN&34>EGN^~$7$t{hI~mcpzkYxF?Uk4aEg2`(AyyOjO{i(w#F8@WzA9b3e?inc~fmM@jVf&yc-@+E;> z-olRftiJqYE4G?)WwDhjieDNrx*Ii{$g{eh|G(-qJBT2*`7aPR_Y(cy?7#f}Uxog1 zJkR#p&u%C1rMP_EYgHWA@5W(}cMaa?aQSjb7ujw+7!8YkDdP-0RqKYS4Wbuw&1|ye zA!E-2%5W;k2G{;T9rt~x?C%tPKCSYjQ)g{SF7KYkVYLBVJ^f&XK|!9y%@$IWTIbnM zl|yZZY=(rt?>=YCzB|j8J~@bzCmZ)o`!X_B{d?&@a~Lbk@{Vya<*{Z9c-Gj|S`0}q ztisDm>`0&w@I8jcbcFNHU2$K@SHeyz0}n&8aZNv&>$q2PYPe(K61HpqAAvWuui~~C z5boEen3-B&Vjh%0W!K_#RClgUFE@^;XIROE3~II(VQu_(u{NQTKVUB#=)XrS-0(FR z+f30MnTWS_Tu(k>!o4OlOz>bpAXi5>MY^S^@wmS5EUsQZSH+?*o^83T340~~5WEX! zr(?3g0DpU2_QwQet|ouM?Og5~d5n>nc+`=09o}pqDMI?DnSz7ytGP4n?~MrAKXLI7 zw;|)-1%FNc`D~Zr;Wrs#JO7C*Tj6dJw|9(|)OYd5Y@?`O8xnRT80RofpBrLE869UF zc1CEq;YBR2`tsGyXfEXTg}u0`o{VquT&{kvhNpkXObFKDWP0zIT02D-+^*oV`wA;n z?z*^!RulQK`esK7ifg;;qvPQy>E;1bpDdA^HMfta{K@Oj9{!M8++~WxfM>fCJ^zWg zjrR)xR?_I(D%~n`B;+y18g~!JA)u2~O3cO`HmD*2bP#?tyvt zhsZeFuZJ|=cc7MEa6XXc`Oz_k0?%U9v{>jk<4gYro=m+S@4lCdp8x-~{(qh8|6BPE zK<)tezmxxjUIhdM1Ox;G1Ox;G1Ox;G1Ox;G1Ox;G1Ox>BuOc8?|NpbG{jc@^KbzC~ zwf_HS({;br|Nm^J>DT)IpUv<5TL1sEnG_{LE5|P?^Hn^QZz~T})>GQ2lp^}A@JB#E zKtMo1KtMo1KtMo1KtMo1KtMp?e-{Eta+1wn6R((;M+av5W%{LiClsgJ1$x^T1=+`x zrB&5cq}SH;bSiZ8^2)YzhzpMNF37%O9ur_w9bc1{W>xDGSXu69)wgG_>{8#rqFzo> z*Y*5#oEeM|g3tDN%llVtzVex0p#&&sS~$7mlb+pN;Mber&=l?7fIUUq>_K3UcA z!S)roeeI)4`r7qOzhYj~E5ND7D=aKBx4;n@!MK7I1*ax(3`YcgV- z`a~p2|Bd!bt^LCa3VQXesjW(_jxR1MNR7-W%&O}Z6CRrpQXFRPTk4Pm{6YQpIckzljomMmuXkwRcq_xlx7}XlMxwYlOGkGUF8?%?HJ`5ACjLW`8V1x zu_@~tXk8Z)?PZ=2nvhW8Q&?1(Q&p5vXcJgb-7B{=D%;#VJ}j_rTvc_F#NTM&Dmc_D z-6=3QDk3<)s=UI>JIyXN(62Njt1`~p&ps}qSD<%zbyaAQZH8l#_}^&X#xJ`xGCtDN z!OGvdHZ7zgv?SEd-`mf;D%!rH$UDqCr=X9$XI;fRwAgurYXWL)UUO+%VKtMo1KtMo1KtMo1KtMo1 zKtMo1;6DQaVg3Ip)lhh^)->@IhZ zQcsggT{bIf_eZzHM>*|(Rt8_ZFvZ_&li{V3ZK}WRlYW!AWuk24rC96UQx0f`YR}pI z-@m6`d@M#OVQ@bjHLv75>Yr4ZhoTKza2b=kw6{^kUISd>Tk4-x;JRdXlsfLHV?KuR zwe&l15L+v7$1X2Jl5c*$gJGEl`naotSJieoyyB)y0H$GNEnjZ`F#t1g3(s-QqPrH> z;6^l__~Rq?Vw6gat}f=J8sZw(>*16G4kw1=As#rkFTn*2^5xs^CjhZc5`Eam7|g>b zzKH%ns%!>(n1W_)OvVoiILi;K+heFMRy$^6V`XM7KOTh($!=B{7>9da*b#s;s(yIZ zz}rtJ8rVqljrP-|um^X;EpS820rxSYEG7#psQNQqOfO+~-vYF73|FuV2PniUOv0u5 zPFk|g0dbc6-PGQg$v%$NV3ef+wzML(O2}Nx8dVnsT;GveY z5jHeA^QPTI5sy{zv@6JFasdGW0RaI40RaI40RaI40RaI4f&X~~{G~(0L&Vd! z$fsA#yxuNz-uW+vBb~1=)R^~s|8se(E=E4{du(u`Rl0A@U6(o6pMLO}oppE7-hH`a zcAmO=Z?g8SQ?_ru&oED1s`8I^&g_}~GtK=~@1jM+8}!%2XTEO!HursH*ya|OYSV>l znx9KecolG^Zo%g3^K(y}D@$5D;^8$*b%W^lp@q@UE)VW|(M|au?YwUq^m>@`=njiB zZ;g5UIq&qBwl_tGkH;pS3tJkr^W^9E(ovFM%3~)!cx>+THZ;0c>e(6Bc6ur*$Xapl zWX;DJ12-%Ei|rVH>t8Qk9PUtKv*OnLwJskvkE&WR{rF&yXL^BR6Z(C)tN1UrGil{o zKc@*DHFiBR>v+cdeJ|Y)yAyXkRGD3^RCecnRqlI*f3clA2M(NFGd{tm!>xCZ6LSyi z)~?(!|s^+M_()JCU6?KRFSb(@WVh#T z(z_{#x+wP3Z*g6=V&9@cGc7YU6bFAPD0{p=NcJD?{HY&$n=hG{`fW(=u)OB)?z+qpkE?s;U|)J4IIing{-Tkyp|YQoZ`vMTpwuM|J4 zzP7hk+D|$}K194)X=d4+j+ZZe+?OQb@Jw&feiNyw&zDWH8aZU#Z_irxD{A~qozZ)X z(%XbC)jh}O$bVbB!DoJ_?j|K=O(7F+q}F;%{iB(aYn}}4y{*UM)`>4H2AJ;ADO8v+ zChAzJ$Dzo~h~%x;CJqz(*t^)_(WLM1+8nHho}2h#wDq{kbG2vMyAM8N@b%^c$$w3n z%J%Wys^x|ltZ_Ep8UFZbch9L4);Uewr2alJy{%`7`%(#Cky7%1rj$*#?3$HNYfddc zy!XKAH^ENJ99$<}iCaJE(kV5WxDw-CqV<1C2_G?qbuw=ye0Y4|2p%}N6yprt9D5|- zW*tsph7%8H9E-tzIAqOZ5-;*t$&V5kj1TP`F{hUb&pA_2fo;5~p5I zl6i{f$F3!qi+jP=80ze)>VglRZ0G{jI_pX7Hg~~O9-#R%lqX(V;|mTbyY{c}!cIN3 z;;n`Y`=6B`#&aq6P{whTr3I^kLMYJ-b$9pUZH+gk3GhYD_A}%N}^H>yGK(7-oV4!8nQ$mXS(W-v#%5duiZl zudH}Hu);(h8HyLl?C%C%Vw67*!d!%vO$k_nVU{6sl1&&_PM~htYFqMT$;CL;L}1REeFM zk)^m9850>n3jF9`D>2!?kf)NaBNFTJdoL{UqfM9NH(V^^39Co2UWZ43o{yuQuHloS zOGJb@4t6gI#H%69WP2Ql!$MyT+(xtLG*Ar;F6nQIvpvhz z5>5EaCw=j=S8r8MoHfKx3A)OLMmhG-g1vnM=;=QSuu9t&KNE{_mX=u6!2m;x^L?-} zEIc;V1+V1$bRj4M-D(&?bFjU_ktdd}#)Whm^bOV6qSAz?D)f|z;dGdL47ABz>XaON zVmP(htfr@|7S32-purd!qY>&^%PzK8;#*X>v23`PE8Tn!efp9KgKkrIUB3+PBCNGk z!p36NFl>-3>rX_s6jIN*ZjIXX`UgFAa7M1c0Idq9igAqko7DLt4rvnB-&HVuK!2X5 zIvRtUa5S~SCy8FWnjJo(RCkOe$Or2O;CBltI^_CH+T=)M93G^Lb-=3dFl?`9c&)?f zMjS8HCitK6IwBSOr1|X!3{-RGFD_%RGOnobu+~{gAq_Zg!uWnz&(fJ5!3NR?Aj5jXP#Nc*K_>r2R-P9jvqN(apUiQ0!yE^fip-k%x#En`nj1$MbKBjn} zZHI*w;r8Z14R~Cxi*aIxxLAv8f&Cdzhwu$87MMmKC?dUWqU}y%L8(bH$z(r9t2@z& zr@H|1cpIgJ$!es4F$uA8o+M9^EjHnir8WuRbtAr%U}0DtW=3P!fC1F%1lClwYtq7A zKe~bPiP0%FsGQraq4c?IXKlq{<)X+D4U@x39tb8v4tE0O?eG z^5`w+>=0QKn!$6DTmQ7|XmOVQMOQsS#Q&^v*z-p3W7d@?#o;&ICyw!Wd16n)9b;wr zYhSN#*)lw1o>baDp{uPmew6KfDXHZFeek1BF2AmmGiT0ugO_!Qy7*E4uwQHkr zDyjocNEIfDS5)JMu2^(=e++G`*RpN2BTJvG!dP)k3CF>tPK=^sMDtQaLp9bV)bW7i zEqHI&$0Gw53_X0X)-XI5hp{8ihGK4%$B7~g^~IaW3=+UrjEYajKz)8b1(Wk^soj%O ze6FVh-ojCy{A}gufy<3-aKEv<9qz{AY+MLVM#?ab57X0H@II3!U29E`ALM3<)f!B8 z@3gV5cOITcq+xs>&Zn1Pa!*VxqKH4Ou+5^UmZ1XH`*-L`NPfo>3w*A0$57Rd*rkh^ z&R7-^&E79#VmwCFU}Rrg%u}cPv|vqYqGJp{c%T`8$@+Ao!;FUM80AVxN2-#8*XNV6 zpWwX}>y$8gENsB53WnNK+_S(ttXGr6TZL|M^iW3W+>R9IB9`Z3aZcP+vi8MK+; zyP9>iLErvln2VTJfuX6fzL+WN;gjgaQ>yoCV5vnPry?pjSKYFaneZ5XGcVMqt-jMd zmx9>#ybPR;t-vd745R_$K~6j&(`Kq^#Y_wBk{o&Mn#CgI%Gj_%Q%eG{vwng{kj{v111v zb9%xsqPm2E_(Y+PCvIf%yH!N=m`&$S*rr(|M=0JfWwfNkcH=SG&xzyy-WXp=Wp`q& z7z1~l2xB_+3G>d8rg99HF?bp;iG?ay)fra`v+yH55I1cx!HK$V`-8PuDNd(K8=UE^ z-Nih#6YMc8L5ioekKx}Jm6ZDuCj2RRjp!pKBCl~VA@YAVUiq8km5(=e_}a^j0?l`% z%iPB%F$qm9Gmg|U*VRE=F~QpIj2&W(n}|@{^uw#Py8I|ZI~FKY%y`3D)+HEU$RBTD znjLNUI*Gq{zz~~=o5fw)F^0zT!}sY7!s8g_hZzAXtO6$DLODHfVRSw%v`>zu_8n%M zTXhV;r9AC?Tw1NI`~Z5xL7djc@?_pRyi#1Vk&gDEoOcYyr~n&$l*8|x=n*5CupiQr zYxtGfehfcmC0)q`&hKZJ;6VtMC>5ko$E~_7n(2l6x}vSRxEC$IEEHGudo*A>ed|1b z{)CAhbeRbhV>b(hxp-eiLL7{9W_Z&t0sFh+X%WEe=tJ4I%VT(C5GF(?CNY@q(TiW$ zVy1+B2L0rWcm%`sw!b%9+Gm%KkyS-1DLqK7)ct5tWo{52ImeKo_G7CHZ=RJ{?uN5D zYMwY!&EB_bE8}hsrP&$I%sAMMAu)(9`H0Pm^2I`TmKEz+akwz%wh_GrCdpx0EO8d) z*u@oh#At^ce((-s#PaLC8mZV|9FFFY3yTYCF~yU^jm;QbjnPG^IPZdYrM_6xLD?t#K(!L7U0s5k|=>1=?a~AqN7nZ0Rdndzo|8G?gQRqY9Yf7SN87 z`;sk8R&Qj5u&cK})5Ua)jyUV$$q*W-#OCkV(atTs(ALSppMY-^$7@GZOR~l&2|I#+ zRm+J=4yR)bv}2si!0fITo#lIDZ6$qTFrj*dHfv{jM(QI`wfn}|8E>W9W0fzi1Y@*c zjkzqA^$yX;l+K!H)^Nj`jG#hn?(eDpRalmZPZa8GZ!O2TA4k2w~k|^VyEPBg$ zTrNk8qSBwu5Bz%npG>S6|2N`PfYBg^>b(@a&sD|jjCLvU*c6=}U_y-eeuP97eX28@ z!SW8r(y%mCfykZ0P7^GUp^Bd~v1$N~brPeJtGp@G4_wJpOj6P?((8${^;B^ogLRfP z!+sYg%l5UZ4!{T}4=lAN49hW4mKkLWeX3`;YZ@-PccF1-Q1{skwidjrXeh(^;Ch_5 zO~myAz`lzCliUXOOFt-Nx62Zoam6q-7t@#{G0cvwedIiaRq8ZN~i~ zUVKy54lU|V1)_>7j86JU8Eb?3)z#8}XS?E)U#J{r$>E`ggB&m3OkdiGVZC#lFw%(f z3}jTTkjRaW#CGLuzM7+3-$usP8G^GMNA*bsJESPZU2Lokc0gO01x{vY;jnsnfoBbt zHZY8S$68k$HsQeZaR@H*RU23*Q>Puo(AuNu;Am3HJaUOc+BI%0^fd@yn?7ORqe}G*$f-{!Z*idUt!aYdO97c+{K;M~P67{uS6WOUE;Zj@pkKca;} zR`eZUj~0XAfx0dod{jG(Nz|h~UtvaEH4ZjVt>q-&@i-wpfE4x^_k3B2UclfQUOgt8 zV4kcdOD)3ZuDokk_l`_eU&4~GtRwFlYuT^8k}Rg1v&J~Vmd=v&_A$-9!w4fCS^+J} z_})n>O-)lxl~Fp*+K)bc+z}r`lmgqcuS?ivLk&-HEIhPx6ZREJJ2&B2QvpV3Q|)hr z`4bKHhFwm^?JR7sWf)H7&$}?vlP=Ek6Vp5OYcEDo?$8~+_&2$;=vk!BnLLR+C4#Zk z1xvd|1YwgZ7PlL~DZ$re^s}$7Jvqq7%SN_vyAWSXit(hQI}YosF?^4Ti#&R_9ClSG zv8%%!i`!#ovOX@SpV!vKTFqMbcV^Db8Vd8Onf}8SAYR zhQp2R@WQgOC|@+ga*%^ibLHOHr_IE3lS9h|`0RiYZulzAUmVe6m3EddnQ`nyxCeH& z&13a3D-`Ebayxdxm#Si1%2lLBQ?1D!=c_Ce=-cD9od^%(UnLmF_45eWf$ZpH7Awo- zFt16CoH~b}UdA*_mP8LD86?AcVp%6#)yAPhQ6XciB9bezihjEj6T^DO<9i$(zBQ#F z9j7`l=PwXdt?gpsZS^&?=(lSyGrmtA?#42dnsKg}s5h50h3}w$oT%a3fhf*cEY4#M zu_7lJKclN~H7==}Msg@tN|fZ=Xkc7dF)I6zRzy+)rvHOFV0o#Mn0e6-?#mL83#`?=6y?5|MGnm znKc#HWqPBv6U+I1*i=mqebK!E8|7pxE4mq(yLPcALiZVloBhftKQ@@AMAoRfiM}I< zvp6jfW`;>MBF6zmGI1q1_Q-KWuzJMcEM{<$^n@VETIx}C5Vl=P8sD4Gp*j*VE-pCrDM8(RgFMa+6 z9>*$SP!UELcPJ#qoTVFZ{)#mJlQ#{bNj4ZoJCj=;Y4F-10Lz>;*sDlSV%ibaK^&m_ z7x#3)+FC_rEbhRJaug3^vC&t*qPUy|P7g-l=5Dpv;ftM;8dUOiA=;90(^w&!I$rH- z=UFdqNc)kuQ8tc=rH?2(#FB1x1|-67eWziP=707+y+69%G;c`yR&6zp?Dz zhp*aV#gaWRUn{eLBsxOeg}5%KMnl;1N}LH2&%!HnhU0GD^P~`4q$9O3&&L*HBG~M8 ztp4xaW*r1Iln&7FW$}4RGE|S`6@sp|l6&vd#FyuOVHn)38I^ z7th@>#LZO}X9D$V-HnMJ2UD0LdV$nC*+Nb|hNhG#=3$SE2R`|v#pE#%#@AAi&2a(J z5g1lS;(b(2co#`W^0C1Cfb!nFb0)s#(TjkY zhKam;Gj2Q5fV+z@Py@GISpGf1+G?iqxoILo*^F6U&RPRZ93-@k}t!rFz`aqX% z3&Ir^O~b`FcevAvoIDAGL$&dn5kb6y-34|yVcnCtc|^KpDsLDTEKb0W(u6lKCL|Zn zlS{EmjSAiJrqWMj%{pQ+HZfj_-Z z!BviWDA#-(%fm@8)94g49QNy^8G%7Pcma}`%%8@mk(18T^Ctv3<7r}=gCR+DX8~pv znAqe~xY@MqVhk!xl%fL7{A^VNR&>wCgK&K18jE#bigK_#A=*I9B!;o|t)4#i5ie8q z+z87tO!u;87GBg4V(UyWFQc`4e=JNS0UWnh?SmElDD)JJN@H0&O+5mwj*d9!BHI%O z)HAUoLBty>%w;CYT%ax@X%iG5j;wBma_?$j5vtsM2N{OD8fFj6Nyy(tV! z#T8@JL!=zh#@TK)G-rx(wLi|vkPG&4BAJf96O&Xhw1#rrD8L7^3eFEa!>uYqTKF~P zIa$e2T~4eXGXTf%>ou(Fgh|~JvB5H@*bQsr8bYd=-LLs!xD_$}-Ib#PPJUsy5o@H0 zoQ%fZbbOb_a8+!q!|ht7G}kzss+C|w-%lhAm-^|Lg=30yoni;Mv;^KUuu771Rylp> zEz=$SNy!VtL%QQRAec2T;EfTLM)-%jlybg55~|BLw`H8 z6=0YLel{g{s`kyLT3d-hs{~HlX>=xPoNr~tk2#tFok*)g?6s^!OF=&vg3CxlpW9=L zo_TwE?s0#X9b4=@a7!0oBwX3}L9|AYL~g`$m{_Wa*Ji!&Ak^9{+DX+a zyl}(VDD3H8V1w_LZkX<#tcBkKEEtc=69egzJ4N%r>sh+2FT>6fI?&!c3DNw;K>GFK z0W{Knpvs!Ud=kB)u8sb609S2_89zfXNV}#zU36~;hUNn@#P4_-%VKh&9<@BwC7M?s z#Z33^jLGdr#&v;Wbda5*vTc_$QCf$u>iA8jyEK#EUOYCTU*5+djqDN)UbNPd5-s81 z?M0~_v}vL?%JBmOqnW*)L}}rEoyem)n^4TwbY{(UT^Nr~F+{WuG#B4ADb83KMT}SS zaO|k5%!IlD=S*<2AG`Q1q+^a%6Hc)@n2LvG#-0uI+=aY-LKGI}aPYS!@j7)3}mX|WDkJP);;QM3zVkp-{l5fD!%4mRrQLOEf9;YEtnyCtBAT3FB_Y@4 z*xWqRf|%-0?XRTd>$hu(jK9(^^hVpf|0;JScCefng8Ru_=@2I&Z7aYoKim%DoWSp1*xs|cof6Ju8s+y)#2qdA|0xo| z3yXH~-rD$5%u;L}UGIZyIQe=MOQCErof^(*LXw#8CJ%x_K=q*fcz%VrhERaqoXKjwh5*uue#aYoh%EeUuD#q1! zoJq&qBI$tvrgl zgH6qoBRjrhYX63hlCk&^ibYO#WhB1uykKiM?q~ceKBsnbmmxEX%EPS~NuI|7u1`CroIDsm5}Qt5ra|g9a$9oR;EuLZNcai@;&(_5kDF+uu&pD6^m^M z!t>OkkTMJ^#>x)3%8_>nn;6KM-5u2CGF5p&COyl^0Spf|GYusU+c7QBoY*bFJx>*9 zBDEGrEc#=53h8)e1ryrNglsB9x;4c0LOgdf!L94ag@b7TV8lmnjWytod8V7nGU4&r}4aY zXB1}l_F^mh`{1iHhw{UD=T)?%k(`(FhJn~oK;B$JR=VCUw}T2x(h~-x=J!-ss#TqN@>O-eFj-~NbvxJy#tH$uI1Nst| z%`!3s{|0T{p}&F~CcE4${TQPCqN@R7Y_y6>~e_aV9aFL|k_g z-($FEig_9oX(MZqzIdIFTTPfNiF>x$YR<*1!d}MUtDLTzQMP;EeB5NN6Ripks|*pN zRi|X6x?@x~yv-yDj#L|vuG0;NwPH+ya81W7DIXt7I61X}jVwl+A3bMx6n`2Vp-o@7 z%teX~Op;Gi@S`gs8p%BOgLThU?xw+b3v3MN&`vb8=Olk`elT1gV|6=*_}eqEcJ^Zl zd#)_FCS8Mwougk5wZd~>tgNGTZWH;Jc+xGKN$)Wp#bCWXbI~5W=udN8^kR>@!tf?4 z%rHwydZhtKyq zbu}!^;C2bE>h5%i$4TWlV1Ot6OsVV_-S#a;lK&D#LN)Q%{^; z>ToO7N`I^J0LkBXWQF__p4u6WAwk}1u@X*kG#e*OVo$L6eTh zxPBO2Om7>mLB1H3Re;BC=B!mtvAkR^UC4$E{VquKNo+EO&CMm-L9P3g->pNnBh*y4a$9Ev%m24im+8~w~N!EAmwCXTdlq`wT- zM~KE0V#!=%?@JDx;L7>eNP5^}4-RhX!~?l;ic#@avkKSz=_D;u?c6cW(pFuH47rS~ z*os@pUEN$=?7A@`*HDz}w#0FKPn?rwJg&tK4_?y7sK1Y=t}LU*7~plMK$A}N^hKW9 z*lmQBy$SzJ$~IR4BTITK^7>UqY(b38U7@JAqAPc-<#~yQyY@EnvJ+~R>)6xmj4p|({$K}rL*T5TItxq1bAJO?f=d= z98{X#z};bb>OHY-K$u-uOpc_3UGC(ng|>{k_M%zk%S=REt5EPYsV;Y+vIlwFY+sVm z4V+fzG(;BH12rWn;Re+>jPQ%{ld-I@;7n2}L-wS+O>p~scKNi2YLzW5HbXs@Y%`J7 z+{Da|y*n{SoG@cqdRT@j{Q{0lCl=6!_LB8JP`oYF>=e%0V6+Br`hh{+F>?S`shE}b zYfnXPa8w*;$Lzi7F^|LtU`+Q4%u8mRP46emUO!<77Y49xl$->4YD)V*tEVh;7t}p4 zz2cnR<~rNqyqV%vi;f0vIE3>(=)f? zvkb24NijH9Qp+Ez8Mu?*pUk~dREu{P?T+H2qY)=VCtyec(RmrKjURkNxIUKDQr)`+ zB_SQ$(bmnUI>wjlOHO#x+ntTwMT;Vfb8Zr$YrS&8qn?<^T|G-nX!#*FSYJVZxQ&w< znC3#L&JgLV7X0W{iikeGI?)eSkhi!*a#Mrl(0S}g#^h*1HV`Ar`Qs0+jwNDPBW4zN zU{SQH9X1uyL*{ZZEIyB3un`Xn?D?Z;#557Fm5Z=ElESbQWD0!1`rv_omw+I;%-=~tXEKDG-k)v>USZ1eP*XeaZ?qoW@Y575rNgQc-5J#Hb;cim16xWT{_nX>}seC z;@TD?da(z7^mfBw?#aRvwPFIcnX$8v1hh;NLwYbPFTnFmzr-4BE6Je0FcW8Ex+Z&? z*cUInQ_^vyj8r}tm%QnLtx|nL>3JB<-F9f>JXJXsyGxn5;e%2MF7?L1WSs2i$|YlN zuV8Y$MU{^+%sw$dN06+z)o3_>eTF~m!(!vYvi8xecG59jhe>55t~Vv)pjcsKAKvg) zo}n;Dx7HiiIFGqxfSLtH<(BEz02!o^HcW8|(!fJSoCqqg#_`xtytOu|3r7nVk8#3; z$(H7ug41!mdF?Wae}IMEsvOorM=;4+Hi$nVLi&PX1r3t}eonPS{W-UC*I_BG)gsR|A>-eg{Kh~|12PjsfKiMDn>p!T1c&_`oaBJurd%y_t(%tCy3y;Qpavb=oT z0A&Wy11KPnKdO0=~mUss9%|z>6qR)PQ#*IK!b@| zCwo1NaV^7Mk7z9MVYxS#*^vvplkquQ%^;zdDG?i*rldvxo9m04dY)jCB znawW5Lhgi*VAc}NI6h%$%)|2(E86BgU2++g*XlBtK2@d@&LYC|xgkxQ&i97;-nDF0 z>W1~59nEn-0{0{7sn8|l)3895>MX`0HxHse1-An+E*2LJFv3w%zF$y1zDbiPrg0lU z86N84S|M87;h8s=pzqR^W?+g6R`IXV}F77+M_!0-$imK9@ziaN|| zj7>i=G9Y*V(7dNZr`ipU{W2@Q4%yFQ1{?oYH2F7&sBdLPndUFwoe0_a!&lB^(TH&y zRQgH8DTkd*tGY1vzb`8!>~Yb~fK0ce+|4~XS&7j(qANys#lvhv=HBD-&KOe3&HA%* z8Zf99Z8CV#j_fdr;eLVpmx1ZosU;lrtUzmTvQrC*nDg0WVGa;yJM!my*j5_e0b{s0 z%?xun(XNU4eu{S}R@XFg2H!u)9$UT9T7qTvj^wN3@mxLC;4on}FXxOp z7xyXlW|G_)jIv;MZ^pb}oe12`Q(;x{F*YTTe03mL7WX5_->b+wKdAl;GF}^#w0=1U zv_Hv*d$CcIseeDd9Eb4%Je#$;L8|=;JaCCtz{1%S)IT)?eK3l~UANrFSPm$U; za_THJ6%PVEDn0dzNZFroNSdnOh~tNYQpoui!YK3mKGCNA!^^k=sfB?-a<~UOCHbJZr?sPwZpP{qz7a_XRj zUKkg}9+%bTVN)qkFvpm1im?76 z+IM%F|188Be-?g&IZBJOEW#}Av<<)+Be`h$&@?}sGP1_fet6YOrDsJyR*u7rv7(sr zzAwwc=@dL_M<|Xr&>Wom%3;Deg^i(M1a?USUW>*{tI=%QU|v$gl5meGop!?DI(!c_ z<)*b_7F0($SlLXQd|)~m$TIXXYh7QwiuUNw(F+q*CrUoS+kx5X7Mnh4$dt0%nPZ)4 za)o_snDmD7?*>h2CUiQVGa=%0TPrX+I)rBb;8;O{o|*e&LOI5>^#= zRk3D2gd^I?nskkoIyjn5rGLkMUAodsYIF>TLRvJWMY9||{%rIR>~8vaZa-)JTe&eG zD)#JISMxm_i#Oi2FtJ-QZ0wmR#eWj!TbX>R_is4uOC@em>$%~oj_oM;9;}I@zh3dd zcbmEfSx&!ot}V~N8fPrvGcI44TDfY+y89JFne{dk^-p{Y?ntpWyK1$^q%bA@M%F@n zf`?NSMVJ{;#_-rFDSF~&Yf%zTC^1A%ddj+3(s)mC$`6}5(S;@uxQ9|bA~7U|Kz@^} z$8$~Iu{Z$JJ@73(gdXz-^FkQiubZ-I-5Y7euC(A}C+@M2b@q^zuwu(Eh0V(i{mSrSkqx+NJTD7C4C=WzKh`BRVMot>)@F?zNK=ou$$&MOP}7O zUs6uTy_Z?fa2*wlRbC$WScZja5($1BQg6%cjCa|Rmi6@vzE$k#Ck;m~e4&LA!C2Y} zmr84N@z#$KaF2EztHy@6^yX~9P0?oDC5+kmSZ*AOW!!f=j%6al6-U!F>~uIWO89<+ zV{;YTz^gDEP_xO?3ru2Vi+6e+xT=zlk1?VOccB;-)nZa8`@beBL6qOACZ#Zi+^fKn z+^~2t`sD~(Y@>sOr#QV}T?)tQ^ZKb5iczD_za+R7qehSW7;euZcn?0r;B;U&wl;{4 zxhPx$M4hn=m2-Gn36k-jc*P9^QavO}Pwu)pp-T z+djCEMk9>L>Mw?a4y2dojBC&^TT(eymdrwy8wVcXC%3@Rdnf7Oq910ac9-E3a^_lw zn%&B=)1Q>PF^!J?TPY26G?U&kmT0uF>~6zQ?w)q&Big3Cp_?hgaC98bDKovbaco}_ zSyV$+wlm>gNa}#OD$KA~dSj-Y0=}u?wh7*N;zu7XyyCW*VsFg1HztC7l8Wi)b=n{F z#QVSL;Z}NPPy&0ItA=;3^%$jQUDL(PiAd}$Zn2gU5mUyW5!0!dVXC|5yXvIO{;A%i zzT@z#TQ39i)(u(ME8wwJ+x|(C|Ad%+3yCtuOBE`zTQnNOXdA3($ih$k9%!wu?#EC( zQzB8A7t|T^y7uft-neH;vgUF%BWneYC-=oIA8lsH*PU>&FLp&EChvD!!0LRLhU%rc0L`PZB#)}LvT9;fq z1jCw`67JBjFc-sUl~nAvF*L$J6~@qYPI`!|ODVV&bIDvj5c70shJn#&cBF@`Bo-r! zbZDlBt_ip@Ta0eNpxROEnn^ik%KBv3_-#YV}JGed|%1Xi?Eu{>YC64Y))Wh)BJV~^< zg8NV#aa_O5~&mq}B-z?%uX|pP!0lEJBP7v8_Gl> zE6BNoj4%!R`X}ROb&r0^)aC>$mN}TtB`0Z26g?2Wm&|>VjC(`|H+o~bs|cG@`P!Ta z^d?4gS?+C>%PqjEMpIAD+h7nMlwB1aFR6sF2KW#x+P-z()G$)2mKV;&-EM^7Cuwvf z{~g6A5-9X|e>p|Gm7}@24oCO9L+)9KZQ|G%7@Wp%S&m`0?E(Y1i_p-kr-vaH7154k zi}~Cfz9-qmCv?ZnjC9=b5wT`~eX`fD$437kEzum!g$DmP3%ABlwAixi^={Jm))SWl zanZ9WOi8han55)TG0CBdHnN$*KvFg$!wzCbXQ1@x2>5EMrx8Q?{yJ75oh&5IRDs`fmS9oJ9 z4%c9XW(YCRs>G=R(QyxBSw75EV!FGZmLyf<7KIOK%xklCy0BOsgwGWiYm4JDPT`&M zxnNw)7wdbz{1}hWSlB zIr&p88W=M{u9D!RY|&C(-Nl2lip(vgt5oZ7BU6^y<~(8Fp(LY38TPW6(eI5*C3wr} z6aF_AtK;xkG~Kd;|Ba$8IhVv*{JCZ(e>#m;hf)pN;lE%!xFb}sPxmJOxmTY4-zSmOUnG|{-?Rbwl_E0Wv=x~vk2K#Q0NAYMa=GE`; zEQ6Xfhf!NT7n+OXk{mv*lE<}rOl)#hr^pL?cECCAT$<=68YI5LWGfC6CRK2HOkCbE zOP{6BHTL*OF`J!m!O0n0Obd0{ODl8e&^*(KYSl2+iUNW+8*j2$^0X0=Z~ZJO3l@uZ z&n&^65-c+9VkR3cTEcmRukCmhDBPk>#K?{737J0Wrc{E%QeVt!^v`d{MtCd&b~jbw z{2vLHWfGqUpN`L>6^OT38xd`aTRH(8d{@Oyj0zt6qIYTDUA1tP{d%7!8P{7k zyB%wH_@Bi0xmboUi=IX?+|A;?0$eu313PhjTh{q?qCxrsbv(9{!lNvV@1=_)sW|3O z@V;eYBZr>&y*=@m+N4e&9McU`B8}u7S^x40nJNz5u9sx7{8=VCx#D3$0K;iAo$w}} zc4dd64V!Z?yAu}{6LG#lA&te>ejG`ZP4NuuVL|5j8LNjc*7)h1!kHeT*KA~g-5LxF z(KCe2I38ieFlfbVFH04gt%Y-J@ati3#v1P^hllSw;=4R&Nw?9b#&Vx4_WPx>>ozaZ z(|&xg_$5F25Rc!jGkJ(lMLymor{HQJHWV<(SOH^S2zGR>!>m|eH#*Ri?l{De5l<=T zE*e(Q&G+H7C%fbln=FS56VX=Yjd`_X$ySVW@v_CF0*q{+7HwV~@@eJ8*j1XwdU16$ znPiGJZt+1hOqa!FZ+z+73v1$g;Dm7nM&xQrrijL*`l=R;s1y2bsb1uX>*S`Lj*N^~ z28I1O4qnA6&uth|VnJBA<&EQE(H5eqDmD0#ha0J>^s`pcswZ9Y*weT5xEl6Z?Brh#6|w z7KnQmEIvQcj1Q8msNCipE`-G>l}3u@9$)1;Q=IEKAn&W6lZDY%xYA^9k4wGUVTiA2 z%hXfcW02yiC?coR>Gn~F`83g7$7)As1@5j9%^Oa|Ok>gWe*5vnEd}em$flf7>~G=S zt00`jyEGbC_&7Pf`s&t_E9T>)aXiMEr84w5R;OcJcQNm#-?^6I_){GNxpPD!o7~&X z9I_g-{Nsr2e$h!JGilMMOmrrPxr! z0tyI%9SaK5QA9yOLBSS#?;3mWz4zXt@v`^cjmDPPTlD?yJY$^mkYJw6RuF$rli# zi+#ZE(6$=-78s`cm7&H;E}V1GiqFpc@fA^8n-9ynS&v)mNMSuFq|+%}AW|kVme+04 zP)9;LwH%Bxq2FrhlucU@4K{`9*1(Fr2msZtr3HO8xyOJ?npT5@z+WtDt{xMmDUaBm zC*`_YASQW)cJ|eRWaR5wd04wELHpX4h;O0~@pYKdeQfucP}AbR(fng*ix8W3T3sin zjJ6liv)Y>CY?JeEOLo7$<`LQ55NoC#7CMrq8v(W&QCkPn%CVS9I^PV>$YNx;wc)Ik z_;1ciV)aTT%a_cIa=G<-(9ZFx=WVMWT{5R+@S-+0ra66QN4)`hUi}efi*Y)TY zhWY%={K(C`7VCH(uUC1-a1gFhaOmZm4O_C zu_7ZX<@Al!_E0SQPsD4efHLo(Nu{;L3RAE>L+c{g*bkOxZ=06Lnf5Ig^r+O?9;ICM z#!72CwofWq1<&VIV7|hI>_SSQs1tS4U-M(F^&`E0D5C$OxKksH$5?`wi}f{vbLImb zO!L=S8-)2y3!U!gq#?fg?n%|-*QlyGQB-7?-0i6wA@(sCh-t zpDq~Rc&2S(nrqR*E>4${thg)gSG-&hw)%Mw-7JEdKCFU(Zb2W6Nc@sk@dS9xV)cue zJa2SKMU-yHx!QH369+FKA#sz1~rFu|@TCOb^!R zK|0pGR`cvO-7&)lt5(zTSncxfV%w@uRZPYOTeMK`I`Pan?+Usb)(9T&VEo5@oXj-8 zRA6X=b%Lc%<@eUON@ROfsg+M7OERH}EyHxRR001KjU>xnFRBMsPfLfGq0ZeCRcbc z#oR3|Iu)mC7PjGDOgBv{UPb#eTWM{57_q&Lo(9Q{3%V$JlgZ5CXE-Q;)84$yBM zbhRpns#A=uZcwjK z4BYYord`TvS$U4VoLz`$bYQyM*{$E{dsqDkDU4yhhLL}cRX$fm%WR>{6zICXvn{UF zS53~U!fvoW1`q0ix49P7(Y0SH1PhvJj6vvG>TeXo5TVp-MjOBpH;+?BS8Adgj6AyjEkXB^fYBuN#@` zX`HT=fY>{m_yE&JNaSh*w$WoIb(z;ot)UV^W9qP!XVK|#&BNLGX6t70);jNJ-$O&| zYf%74;7<4+3lIxe=r&ek3RGs}(;mmzMtyamoo<eYp<^! zFb1@Oj{59n2`*yq{)Ku5;6jikfQ!C0shtgy^U9ikA84hW^_lunjkK}RmAIl+*l|x8 zDFM^Gk)7f2>5WF3vW4Fm$l@Q?_8o+uv!f5^$9FY&>Zja}4Vvp*?>r_%;Dnc+*S2JM zdyaojh-uW(uV`v=BqNR0sS-E^{#6+9Kqb82--BTD%XDOE1%&&+$DbgsTlv>WhfJ+? z*Y!Frs5vjI<31g{HQQUe(%4oB`G8jWuZ`3SLAl#YmuqW$V;zg~Rj*`PQ%uVmCV=`K6Vs?XerTNxD2ACPuxRSQ}=!GLnKNObl zchxy->Wz|_*Iqr(P4cIfs-kybVcxgfzqW1%>uCv)Y;gp)Yp-t+Duz>v7?$y_o2FC1 ztP_!15Z2EpF=rSz#h!S3MRd;bAmQiMt1*9 z*7#CHgXVzoRs+J_!>PMNihrwoNrWc)>SDXz2q5V#-7u(EaFA>2xkD67*%;AZ8)`)X zJaP=1xIkCT^tO+$*0}3s9;5xC!*!j?B*31TMLTMAQH|`Msg(l@K&2sCAI7=mxHg6` z0TQO8ifeaU{gqP&+?|%vl_fmKI*FclzaxD1jm(~UpWOFK& zLnFPL^7{1*%R)Enm{W47w>CRzY!7fT%o*;JCS-bRLY#Y6x^FHe&$`nk$4$rL3gJP{ z*Q~xwXMvdk7d%DB2&k69{!jMRt+HC%uPJXn9mrW^xJ|?2n!!2AxS@WWF8faz*jk5AA5{jMKqqDjB)3mOlzK~O^_aH9X=6Y#$ zlndONsEKhdTAhPcf9asRg`?1-#~KsPs$ONM*m&nTG}7*ZmZf>$soq*XC?z!-V*JdT z9%L9>T>oIC5zIP>GxlEwMdJc>vLTQB8CxNdO@k|A4pvnx9duzJcp=)+`M=?IjS4pX z+n$6(0j^#$J-dXa&AoaKUoJbZ{GQkFH%{J2NCI^!WeWZzr1L#+cee=q+H$?Gx{}wt_ zv}{S+s@?juw-2tSHOak9wbTh)wn%3Ii%c8PUdOs(09I%9W8?Uto6+5i1cz%`8)rQ+ z3deWW0*n?80nE{*psw0lXk;PeOG{D5=k}$wprQ8Kb?DT*l(yv4&4$i;lpL;Ud9Hz-_|Pcr z8Q73FK4Wir6O+MlQ(L;ZX?6ovbfs0#Agsq~Kk{w#EKMIByY;K5Q)NM6S)P(Z`cJ%m z$<2p~?MN!>o*(+1Z3?8FX84BSDgB(PEe5F6IK+oC5VM|)JcB{6X*pT%W>6e>g1k-` z8+GI*3&YLPt_7_+2G>ieT`!NK^|g?B~=La)VN?iDNnpWXTtiI#K}Rb+Me0HS)-ztdVx`v(61!QbH^v%pP#(0CV*<931^B_(0Y=TVO0jZER zj5d6V^?Dl(xK+x##!88^n zv3=gg=566<&t((FX*s0=efvU}&DoXyn;kLE|EAW(P5AGhU;j|x9}4_Kfqy9Q4+Z|A z!2g33nApqIB4R;Bx8bKIb}_!P=c`GVFZX_5y4%LDfAlY`X5K$a95>zi;K{%tIj?i9 z|HBoYrKu)Y&DC`dow{c@vgdWIOpX@CJ6EjEf%-~O&xE%4Wb1T3Q@67?ES=Xb(})5b z`2p3?pS`S&OW&Ui=d-#lQQpe==}rOMpOKpC-&HHJI&f~9g}Xl&w-*?cK6pG=y7KXH z+$){b&Nfn-WnnyK;|#wRKYu=F%p-Van>*n_fsoCQ4ryc)qYi4xAXm*z;hQT+DcNjZ z+|gIZ1N<8rj^qL3!*)&d$+ucFtqa%dUV3R!G|UO$hJCeck~rfWF_7$b>y0-MVvPE| zxgB2MR5KkZp&_KB{-To(+UQgltl0)uCLDp^SI~wCt43MvqOAN&Ip~BDE_ff{XRQ?g zK@{;IJJXU?{vbKIN3iDS)FW3(i=2Bj*0r7>3%nfEIGZ5Zm@1JR0H~{K?H1cu+pHnp zI958gv+n0+r|NUR{&n#5??RY0I$liYy2f;XC^NzRD?(R<9Yl6$%XFXc#T@NVuh^E# zF97#SYdo2&1}Rtb5hEZ>r=k;updC3<_^sz!@Xeh#)u$3_c7@zSahHu|5@4zhj5lB! z&ZbgpLXc@XMMAv_r*L4rkd|Sp^A+8C;j90W-p#FSFFTyx8QPiA$IGl|Y=(!g0ok^T z!7fv--|6q8{T~q_Iu*%@;b|z$o=M8>T6o90Q0<+@~4Zpf;4Xs#s&_gaMrbdc zdg>7NDTsP~1|A#64DLu#vy(+*cufuZd|o=v!Oyg74UZz=*oSb3%30(?1AVq&b_?~W zAL|^E50Ux=XRjbfrz)h!XmtXFc&I}i3G$rPb-*S-BOsprpk?v4`rI95`Q`^&mC*y` zkjCZ?)SL)=eKm7|d%yTIGm&mG{U`e&$g+V z4g5GRZPUJbuyCCx%o(knfCTxFUEY=BZgtoEUT$YWxT5neA?>`B8;91GBv8 z9LlZ|1v}lMUcA9t@?HS9fj-gF|QPh%~ zl+FV0CAo#_a7hDRa*h^u*OLw`>rH{i-Vokn`%`})EGAP>uj#waS-DbGuqy@ zn2vX|r0l!Ct{4gGKLtX!2QX%adn}577m52l6wZMfca>Qf_1<48@yj(M2}}pdIt=Qe zPZrev*aBADfD$N8i?0f8?nLW;U}ltsRY7~LO!kR$tzIIun2u%g(i>>eJciofgE~A- zAp-ND3pV6z2kq*{(RY!S$Ld8XjRdoyAz-UMh+~~j?WqAVnm^poa>98Y;mJt;LEUPg zUkhr!S0u=Y<>);IV@ANHa~jbI4y|iJY?Zn=Qw-?|!?ac-vn&;%8KeF5SkmHh#&yI! zK0aMvtm`wSlTdlEwJtgq(7bT|J=8$bTh2$OV+D?O(!4?>A@wK+w||HJC(CJZ!J3G} zozf*Mk@Qg#bMRKjOY6I{4(9{isA$F5W?B)`y_i;IX9Mi|$}w;ye>cGWrhz%&uZ5oEmVvHcP7e&c=?xi3 zDm@OgFPASy6id+8BE04dOZ`i;6QV(JL|AYAI|0D8eQAws3cu#FpWaI+iK0dt>;g1M z%fMK!*s4VLzt8_x7ftx@pI`q_;2#S7LxF!N@DBz4p}_yW6qw{;ZV|Dt#DI-8vVOe! zb*WeC{ujT-I^F0z*E4STxUZ4NADMjnGJ1B<|D1noSxE1RdDnpA@(<|s=)UySLTecBv-S_mNZYT$%@B7mYJdY zSlgtVuD8_VD3Ahq_qy7|A~tg4vsA|u?Lf}T2rXnB=6%aSg6nZQS+BcZ*A9sc(%K}S zs3hC=<+ZS>KKL2`8FxG{ApY^JEKYO=V(l$BlIpK11)cczP5%FlyEx;hcq_ydCVU5& z-6+WTZXa2cOq(imw%|xH!Oq(&s)4)ScyzX_V#ZSLmDGOYE^kow z#-DY^qOZ2~=vPXYs_45HBmJS1HXi!aoeAI8I6Es)??+d2?}I0Pb^v_fUd?@^lQd};3j9B1Cf=II24`HsP<9*P-{o5UIrN`G5a#KymmUXI|6qm zbEF2?KgYxj2Yq@u?JV4p@4aU}%ev~fTmU`Zhb=6y<6~-zLW@>WJe7%u_Q+g83Y4)#9 zHepE2E@p5&v~;-&MrGI*T^p3uw+RpjeD@by6zngKlIyEd1ntwKrooco4Z3p&uHV}d zX8Xp8Yo>`VW+zstVII&@6GE!$T*-2cNVX5C4$WuNUnhU_=!X6pS3w5uz(hB#D+4dj z0YsA#;Yk~jrWJyi`M5Ox_=s$b8dw%eUhM|&&hYY=EfRYq)o^2|mF4{)=B75$ZVgi6 zbhUn#f%$x1pIVm8^`P9WfMKuh>oMuu-`jB`+s--W-k`k&7v+*ef)~~Cu1*XSZJwH zRJUHYTUOET#y!m}Xv#5xr+$s4mXb8}XS|9mqo%0+T9j#D-;7v4ARj=Uvoe~Bo*+hb zo$%oiowToL=BAI{AU1GH|Oy>6FrY@jW~3TjlZ;@zgWZp0vO z6QKI2-b{0054vC021#5OgLJS3TPvJ;f#cr;+Y4rJ5Ex*&(!3VecRPK_u*KPqX^YA^G#^fd|kN zWfOFb68}1Y+78ljNK_xhMfB$^VFD6;2%3xxaF6QNBC;_}OxkpXmoBVuY5};dTV(8`WjW% zBNZfA9vrMa1ry^~+7J|AGry-NRMc>@@PFk9=z0LJx)@i3L7uj(hX%|mkdzY_Yfq$P zv(e`Ft(9^9H#2cHv3BH-|LNDnEK9BeJW{Pj>^|meF)_2A$vm&CcZ<)Bosd6fh5wx; z$GSPciHg1bAG`?*qo>w9t*oUN*8Ri7HMbZ5WA(C`31}|TCD%Sk>5QgTH94Y$Q$fQR z3`uDjkvoVbo$OsKrz}=ws!0e2a46$k%V?Zqmt9D&O1Mx$7amw8ImA#4oiI={7U{aL zE|g}&03<0-2it46UnQ`s*pB8p>OgDyB;#P{ZWaAfiY<43Gel-P!%u^5UjnhKZ?wv# zwEj%e>oV=KAln%5;TiXGk(Abjw8+Z4QY*d2a0m9v)r+PWt|7VXLSs`i(6E^d{F2Bl z;x|KQjjEgjQEfx2(+;IMJL+pNx~nB7+3Add@_VUX9sHq-TG|S_T+ugR;^LVK?aKPV z(s_CS92+g%Y|JCD1P2RhogW^_&CVq?EJtfyQK1KA+m#9KZN)1V6Gyb_h<2aXvj{D5X3Z0s^%5;8P$ooo z8)|4X{Pkdv=7AdI5q~JDyzMNI#drWbM~TOTu$+oQ%4hg*~!R;h*TL zsUsBM9Ei%)jnrbjidqv0tcbcfAX4j16wgF z?fCjOF`UyQ(R!H`P%EQM21R6AZxYWkcf*CDp*mFwYK=A13eN=CaIZHT%zOG1)NEE( z3ra*8;kuC?I@%;5mpMKNDiQoQnV#^P`^@AO#9a<>`B?pkE~kAF?Bzx?&L0f_AjieC z3oW{6r&+3Kd4$erdi{{mkM+}`1~K~NZ>O7$n)k`HEX>O2KW9SA5C+^}!*c$JXyxO9 zD%|Jm10DToffa4|eGib-wH=E!(hs1hj8=XNd|}PIJjlM+9}9nu{@%{5VfNr!4%AJ~ z=PR(zx*EL=-`n#vkp4!;yhG{8=s~MiL7pancf zLUdfZo)^KW|0o1vbqnn&txbh&YUqNW4~jm6D1~yBJG7`o5DY}) zO_9+BNYGu)s|g`T4bm$>nJ&@ty9HF;hf?oLiP7!su;_}_oU?i19bIUs4XOUA9?f*D zR_g{}2h~h3kl-5{?rYgC79I~TO>TQIyTM)VVAdyi+Li(=Jt@3J4aD`?8vTtpvM$=Hj=+VRVuyu%hD$WKTUhIsQWVmBTWd^=mW4ZG5%XzHLF(^Rvmfl8%jTG_19m+Vo#EFE zWanIIhlXY-`W5E1f$QN@=-!Ay2selG+-@4y9*nL%h|vN>c}-iYRe8~4{(Z!cCK2ol zN2_ye_)Di~28Lx_JRqmA9yQm!za{(p}H2*EEkvnhN(6`+$Y zl=}XvsowN&V@o{9p39NCWS0Uw@Bmf5LJ~okJ4AX7HKsMz;^rCPxA^n1!+KvwhZ*3Ck3 zPV>k%w_YB6VywQAI82wD1^Pree{0hU2C)7;$g)}Q3x4uHtK zbUhoUz187L>~W8AFkV~MOD01=PuZ+>txd50EY`a9pthd+*}z%fVRY&HT-X#u`I1O2 zM=UK1(>x=-wLQpRdlIpt^nKBKQ(Y-o1_s>12#oUZ)<>Iqkc_OB>a1XCBXXEIr1xZX zN;W{iR_65Mxdogg&iETi)}I`;sVjXbbhRSKsKh29X;{zv(dM+eON;+?XN?2aQS3DuLf>i z2?%t;BrMb0F2!KWy^Z}Fm9gov2IXHYeSjZFF1{}D(iR?wBxtHw$^CRc>6M~blAR?2|~8Nn6Ew& ztF+}Tw+NkE=$Oqc7L-p(rcYnp!WLRvCZ&(I^wu>~yl?}{ZzHr>TZ$#o5n^d$_O~DxZ zxtbrwe2oUwLy|1egYAZt)#(GU`!kvEmKfn({Og>%&X-4f z&*+>jR>)S1<1*5MnZ#+lL#<5Ln7C3}>w%wTv|2pMQ(rSR-@YFNKG%ZQ!u)=Ogp}MhZ7R_QuI>tF`M)>RgMeTy>a0!Hfq9{o^xioCo15)1DNx>O1b_YW z>mLgI-$H@Wp#>~K)w0i7yDBF7Pq$_5suYWiZCZ8V;IYetJFT121EV#&UO7#U!$kZVp}- zz#h1sP0Z*i{(Gh7?1IzVV%g@=?ncwywIV-;?xxPvbSbFmfz>m#rijt3k|6&xvR>H_ zwqpL@*g9jgZkn@|z3ACF0*k%Oz7u0_jnMEkEb-Sq5$yd-OZc1cu|NCv7&=u^9jcozttAgV^wQkOT+M48h12`p9L~=v2Iufybjq$i8^TEd{yJK) zJw4Ie`np=8qrR7J%|IK1G$SeAp>;vM3Q6n5^j~YAM~u#wZcAv>v{_~L!Ie#!(>2Wg zyhhQEPQ3a>pkDW7-oMrXP={#4g*4{!$Pug|T~5w6576#vEdwn)S^qkXk4?po8A5z- zHapLOYWj`9vUMTd@@rbxg4dnXh2reH;HohhTIj@C^Tru)W;F!lDoJ0;vD6EEX>?q= zPuHTz-W|2IMpNB$(bF&Rd>-X!AmT@Rvp`yCq( zX0g<{fPcMa`7P*#<_L0I13ju`IN|hC3^clHkSRW-RRhz++M&9hRbe3XG@|jt^g5W2 ze5nOzFLYt^d>lbZc{sweyG6dgy`ACJ^}A2$MigY~9<}8-@(W3Wb<*%Us9BpwmKE7?@63yMxg}PwPduDEv9U;?|(C8-(MVCb#8g9jn}Hps=f1@SGP&C zGXev51$1dXFK$@Ggi>bz!IW*&k7~&M#<~@BJ`I&w;@q{LX3_a?M0A{&hDK>wR7wXZ z^r1;Ny|*xgxC(CQeB@#cX5t-9{kZyWW5bi*Ko*3*FKCJ8tzn+kMZc`+r0%A$jyW|; zYidbr{YDP0OAvPFH?8l65nBwv916gdoCBr(E4ph(0ZzFitvt%;KqD6nX2(Q5uHM<# zkq-gz->F)WCS_>yj6s--fQ@XVQzpj21EC(Fr;RIWay_K(D`L95kyaFd{zg{jVpF3V z%URyJvR`|sd=5J?Twi*n@Y4@S&*Xd>4$PJD7(vlUe8YaZ2Dk|jAE7j!N69xAjcM!9 zM=uhQxQC9BJiQlB`IgZ*mkfAT`q-4jpxfF3ECQ*X$@&E zBNA8B{U8NCOW!!)tH)8f=AVZI+LocYde?jt26QD>0py|)v$UodAi$?W8*cQrxcn+Ju88Z{N5DC1rpzI(@a}) zPz9UyMFWS{L#b!#An2w|Vc^6NNPafTDvXv61W=0Uuv3-REc>qnAn`krS6YE6!6%)_ z9%$dFM{`}QMu{`zIT^|_9xxnjh_$b)tKR1l4j5^nA1+~-__2sS-F>SROw_Ri?JsY@ z@*6RdWqki=SN|X=I}}O)tY!d8Y{8 zLKGceg0o$t>eh}%R#%on*B>IeE1AaPhSg}Q23nr3ch;k>iL~o}29G{#X9sH?11^(E z?Vw{>POQG*}2Hxp#k~Z@q@{T#O3mwgvPNj<4y&Ap7<}*qz#d^Eg77Hl@>TL)*0x3 z-Z*Hk(@LuV492U<7}hHzJKA8JCj3|9RL=R*A20IOHXQzO%ZPb5k}B`%J*?TAG1Pjf43;60$er)~rOpWWGQA%gLNtxEF_LVTH3lfm`)lFFHWDPc8AlGT9 z3tT>=Kf-C{>DNoo-Sn`qCe!Sm6+D6imvzLY1azNRTPvEUYC{twa7;AM{HWVP>w+-> z!#nZ(7d`K;%O1uB-8SuYWc-KRRuc$-P%A6?^yj;ybks~!igT!{YFSSA6Jq$Gfhl~v zmev%9@_zsc?~>V)1};ZSBeZWYI=hT(UZdaVLTCM9tpx!OV@%Q(`Y=~+(Px0h zm(`mdE%m7y*sQ1!8@bLAAH(ciE8QnF#j2sc(hcsickNaw^#^pQ$QTD320811dvRMC?jUj!J)2YDn7v|y?5`|*jv9PX_oDvh{i>$)ah7VOUpN_ z(j5fRY&sqOs>iMBL*f&)o4X{{57I|hd%d?w(yGST3_zERwkAh|W0ChQymNdnt?sK8 z#aaCa{jk#5BpqovAjrp_z+C@!ne;C#gfp`fdB@AruGpf!mRjkcAtm*+7&}xc{gKG& zc!aK`g0oVMz`pkPf{Ukg!qx*D{XE_o`VFq8^Y!%7mgWAWk?HQp^ibM3j^aFC-7(AP zk9tXy`?l1Unho_O30H`j?!zcdBF=ul6!LI1pDy|2=y`HPNet`nuyF{+VNQA2aat2> z%4$l5uG&x^XsDX$`8*u44twzO5B5V`rf3Uh_onVQA{tMVog!%P1154#kK(n+tx9>_ zuLR9c>kk9%Vh+6iwUukd209tT;6~%!V~4#HDYyr#CWpjIpG)=g^F#pe z@U9_lY$nG__8Umxk>Tl$%$iHd~A*B)S>iu`AJ3OwT^e zNzh#PK)!h*U%3YQGDy$LkdaWZdL7MdpIpXXkKB@YcA4(vFPWsNX@T16(g8ZG)ZpCs z6v*>CRNkQT?(t=P+4yy)96I=(&ws*uysa(uxw?9mjgnB?g^V83?@^Ri6rvSOY(Pkj zW|mrrk9s(0pnG*dJ&NK(?kUg`>Kx+6h*d7t2hTc$bBi%^P06#t7;MymWNmc|&okob z?`k#FtnQjcQ3o5-IwKvy&$%Y5iks@gFyq*4UKlZ|A0FkQJ4qM3;e57%1~Q(n#&!;h zh0~MqWIyVnE5dY4_Y)D3BXD_Dy}a6tvqiJY^wYRRZEK*wg>gYe0`t<#po+{SUsyL^4K0X-(@3^;9yGbB$$*VjPens22<{=KZa4mHnC z=m&KU7EIUFviMIY9U7S-GdSK{4Jj5} z9`0Or^U>&B-8I+ivi#^LE%Bop3T*fjmEDeF4{gwjdA-tG`d&4zz%v$aN4*^eU=ENKk%cq#!c}nb5a#C zH|O=He4iSg@cVJ$SY~M?0bb5z&%YMk))>a2fn!77c-ewvQKR$jsvdfkI>@b`_LoY? zU!yvmwz`h?FJ$=2eY<^}zu?%;t519ENtpTJS;VIocQg8i@7x|&tLd6y#m}z^h~Jsl zaB^%T?*+NeW3!$X`}q39reH=+!H|4mEGy4Fsa%87Zat=Lqfh z)5M5oCECNCC%PTTvnMG{Y!^dXZqw?9Xb!4r#E_+xp#9d@_?W<@16v=Bnp?YlWs0 z{Lbcc+gs^-D&yVIT3ss zgVVxv-bBBJ>Ul#f;3<7@*9g1Nip^tccF)lBY>u)oa!}c41+iZX`?fQQ%ZaGtP)O6G zit^^;`Xv-0GWxL(_f06Lze?zM$NGrs-OhSZ6nXwq8jsUmco|?W^U`!Qzn&Ms z@4Sb>*{CORdTN6mT!>Kq9-#f5<6z}OpZ-2odKX8;hBsq1BO}ThJ=Fjjj%sF^-bK^$ z=~NK={9TmymU*^5xK!hz8=X69R{7?7T3*N8z^OA~S`Urp>0h-v53BI2mPR1y^Dz>8 zEHR^(dFwXqtcZztqMvGNQ*+k;qYJ5nIZ$a=E)K-kRL+ZUS=?re`cU^Rbl6%IZJh#v zM_Xa&mtr~|M?`nmdt<*c+Wm~8m9637oha6jcbu*fY0F<1`=d&Ob+mvcS7w}JNYyhZ zc9ui%_6rjkqK7n&1YwXtUg~zS4opqjO16yQXa3Y0J2OrPJPdF3%q;a{4X@I7iGt5uMjWD;7VeOHsCVF8-HE}KSRdlB^4#Wq3Z`9Q$KDusU-jKD7 zw9YD4KTV$^{9%Cs{B_ZA#6Wv)Y$X0Zq2hX7b)7NFqsNDt;{1Nbwiz9LR+WZtTRZ4j zFU-|lA7@=}kYcI1l{0je>_!)@?cRkOUog=Awb8<`WhL1YZlVIe_~@pQIrx%sfro3Y z1-5u-n_==|xO$G@>IJM<*Z+o|{pewG_?h5Ihh9|=X#CCYsr}kb7VDRsZ93oX+pD<6 zLlVmV2kUi+#&5;p>2y1Cv$C20PukyFcaxJdysUg2eG6-C1>?Hrm>vz(a`*mtC9_-T zs?l)cIrCkmRZZ~Hj%!kIyv}r~ZMZD9`r#DMVI_|v<7W1fo5gh1Xg0Wrom@ZS^>Ac- zVdHoZ^xwLko!dP*xzYE7ciwtWNGv?q?Z61LnRY#H|CQ0+`O3=7jK%)}Ou(!Tpr8&8 zPKF2fb2$JCE2V`;rj{s~)v%>`8y}$Iz~$1Hl5`!-rdvCmI@oAWV^fV9-FuiDBZXj% z#x_C7ZegrmR6;*)^4)FvGd@haQhg180bI%hY()E7H6cIHT7Lo`7FZw$u1?YTCT0b- z!GfXpGR~s#0dWTG)a-8ht%lYm>RAsRqnZKbzleg3bJY}@T1OgXxl8@@si2M)Vq;&E zr-c!E*+0ac##63!LlFCen3fGuj;31dt#_@tPhF1k%&^jmsAL3nxxU#3@bZZ|*hD`Y z5y)fOQU*1C)2NI!JN|5KPSKs3`7;NW40hL;mPR9$Q}y$VgDGh$;U%ME^}c3Z4NlN) zD{Ux{-b=d;NCVOp6TI_vfWtfWs-g)3`vqzk-CG}Mf34#nqok zxm9JYAnTv>5iK>f59`3)p{2%J()l!owGXy2B21^;wY?=oncWwvH$P4O~~{=DDO^+_Y! z+Zj6U^WWl=1%u~#+y9!@Z}-Gs4%=24ALE!~zINKKz}t=5Oi1+{=k5LA3Pk(9WlvoY?r8?E?0yik7J6P^TL*MQ?B`(1zCh}^2>E%Q9K#Sdp9hl4}EZ%zPmVAb%e?VW$n+qx$?1%qzD_~j6FEM+Ek^gXIKhr z_^jb^!Ig|oPb;;{oCZ-r-D{U;i@1x$eA>-;#imX)rR8(q?R6*sky+5bG086umYUWi z*50_YIHfJ2fR@sbZVz8y)jR)YHeSJf%WI2gx{Uq9Z`2-q)bVsI>}QKiU1$|h zj9ueF0TG&h)d7OeC9P7I3_RQwg*4bDAflbt=f*i`MN2I(*H69lDZwU3M}st>wx&Ax z>srTh?Xf!xs$*KmX<9}mbtO(@Ct6?^*7i62trxsY1MQ6SCWZYqbi+nFt&67x>r!kd z6WDdjk}{JJA4KQ7UK(wN_XM=fq7^6S%a^8lQ9##|Ts(CoGmrWkohnSK2>k?>2C~I} zD{eGj+K6=>uf0Ld|E=7+JNBrn10{OEC@!Q57x(6#2XU-|UnL z|NZmp9}4{6Oo7?e0j=8VK6_EmDFMEn-u*VaN%HY(KGtPAjeR>X@sHlU?E2>8 z|GWoTlg@kr&tgMEm$=^AY(Ntk&Ep=H3)8xySW&W^G|5>t*;e}uj! zYX=Cj{u)6Ow<4sW71YR1I5&H-KEKg1uD4Eb1l5!_4DunVj{)HRD?|SgL&#+@hIL*Q zv--_G9RfZWl&yIoWGZq>rHZqy-mtH5rliU#vbM({ zed&t>vA;qId-gakhqbdffZ>0E5f)RbPpu@Xk@7og8o zc>0B28z{JsSj}F|wud0Esa12=yf_BD=xS0i0rR^{bMkALD-+nvO0Q^&S!X{Ql;?Qq zn?qqU%}LkGlIBgzmtyA{rFEr)%Q$poSKMh#I78dh^GY<-Vkgb>#DG%2$ri|#{RQeh zG$IctHMx3bPmQ#-hHJ<0%{H=m9KxgiUP{Xv;(v_p`O<}F$V z>*~Oo5AeVp*uPds+!{7AfpotO;KZ|oh{RuA6E)n3Hqy;nw_M6=tPNb6RRd8mz;GYO z_Zpxf)gAaOT>u)TkYU{`z#8&QC~l;hSCeUwtO;pH1eT#`!|dB|JO@bee`0 zkkQ{Y)(KLt(iM{u19hc%sO}qHD9L(A#yyrWhNZqHH-!;Km#$=&)!xOnNjKlx4RdQC zCX*UKgLybjyIdjUZl*U$v${1cn$K6md(!1>_yAH%s^QijsTyrGT%-39g8iqKCfg%C z2btm(oQ;zW+mwt8WWAaJnQhLra-k0IJT%8)y_}DdJKfvoF;+ookK($a5tCYP}tB(@?jEMmMmUPk4xAz zXQ=JA&}s&OhP}GzroU+U-qyQ*Q*G~_nyEzu_NiWXPU@t4L5z4PIVc~rnSoFo@d`?J z*6=>87?;$ck9XN}hFT#kb0|1Rh`(!h_i6>=YU!o3b7T4tMi`CvcIDVp>sLoB{L<_w ztmK`v zLodM2xasE;bkVf5U1@!4ILNh$UfUYs)bo96z%ir8o;Z&h-~r>2|2dJ-oJzOpb_tkx zr!j;ie;019?ZAh)QR5rm_%RJe!!V$4F#{9a3K*R{Cu2P+J4wc@Y%2lcK}Jpbav|4a z5Uo2rb0r50zoP}sKP*#ivzk-e&gC$4oCb#{^U2#@8c`1lkOWfRKOa{{^^y5gc<+zP z+R-*6vMM%iD?_~C^T!hhXhmA2R^=8eqg82@Vda%-)k_u$VljK6;ffYIQ8_rQOOSgY ziZYaG4nwNR^P25$*VLx9-e<5ijBA+}qp<@uxTl8H!;0)-5{tX@#fIjl&U#xck4IN) z4NXvhxsTTVqCFwSrl#DC*VFnwjl(ew8*EYDH)z#Wql$P|^RDr_+|98oVfx>6GZH<1 zMR&9AOzKq;tmSdEhaU5hi3~~7>fU#^aZyH1|-|iVhy7EVJ~;^q>IjRy(7t(zfAFHqkx9SZWn`C8bHZ>(d}ngI1i z^1A!_;AlOtral1yScaP#_cqCF58rIf^r7}3%CvfOy^7_THKU!^O|fz>74qm4Q?FO*d`oIwLJh;|gJoZZXlR823ADc9)C*!SBTqI$<0(H8tyLt0gp zJt7@>p3QSl_}s;gp;*7^Hl6h_m{JhkXxpttQeh1XtkYgsN%QvCjh;|&63p0SuI)_{ zTVvTidNSZ|Zphf9RQ+7iw2Y=U*7ugIX-(Mz!Tjtx)j=)WhA_jA_VD_d)|A$s+;VZ+ z*WJIeeo)-h!AEC%0^7%0jiiNB0C1LY&>6FS%(D-?hAE(r1rxLTjd$I$#j^-|rF znuhB}_fGUaO^@y9jxjekV+}Mg$byU;&9$6sOS>qRcb>*Z9mq2CqCJS_*Q<$yzA;Bi zUZQ$$v5qep|1SiBII@P(?C6{J#FQD(09Kwt)?#SL`A zdm3SU|7yL4#;w!IBGe2ybtmf*#r=jpj$ledG}fMo%Lr@svlF^Uv(f5Y_kAY!#jMh8oQ8n&nVLUU`rkdGxt7@4k2I?3Sd5uLM+OsLvc>-je(kdDyz1KlA;&j>` zb9sWLT-M`GxI;gCh_J(jBmyT4GePIF81Ynq(f;5->vSMok>YI*+S4pUmwNQ_@WSu8 zK_USl4Djtu?OfgHZM5ll4gq+ntG`2Xv#`5nwGBKG#aI;ut8(6%3?7iDiBcr~Wu@lVT()v{HCGv!ZS z|J~;;(Z%`R#s|y&_YY<#XJ(~lq?@izOzqhy7FM(y=`}vG|n*7DC!IKxiE_;%{$$vOTcI%`Oo$WQn6PM~%1%2_4 zu39Su{XEv7H=g8YT_XMLhn`rO@%3s06lff+9t!C|yINrIH0z^#HL$4L;y77#>1E!& z3!YS#HkWRN7|qk*&Y-T~YGr6K^|7g0^?G9G4iw2`JGkzFco#5@xnqb0_Vwz?OA(yYe~JZ+#De??(e#}z?ZX6u(eSd}N7WO*Dc z)16c$($2rr|0?t9pn`=pP|BND9;;o%kTajz?15Nrhr(a-= z^fGm|wUM?D3h%?dwcL#IR)}!Q-li}LexXy}Z0*f3np0oowM5N;E(qxy9u{e-kBywu zwY5TO3L1H}haY8p1}^ODNUXx4oZ4Devs6KJbV~Qyd8w623;(K3jZF*bK{hs%^3V*! z_p_*9vrA*Ld~29vVLwzY#Cj%LdxrC#v36E^StJ+xu#8VHZ6xYr!=ymCKs^M$clA5F#x12|9ujF zs*4%4N^XQjJm+cMp|ek2n`Rt)Ca@p=imlxj*H$}PLdSESF6AQqGg#%XdBp-;bjBe= z_o~w9#JF2|$#s!4g<$abF$W(F1*!L5h`z=E?h@f*uTwR6_BdHe@-XTj|h~ zZ1-tx_~#<7ld+;BG8*cyB4*BsnBq$yZs)N9OtUcBdA-7cI2y?=aHxxRd$2n^>1KGa zRKx2>=UR2)fssZwG`S8Flj|+2qUkjfCDU;-Q8%gcWMCc z(?l1I?^{&o6ZI~GGyIRNmd>WYhnwXy5(~GuV5c|gBA!fP?T^;EJ`_ItX?C?He|MSj zv@`aDBCK~;Dq8W)trqhbM-h5)9Bd$mD{ER`B;&Tw+4psXdQX1f2AF#e7`tip(HrKh@9n6-up?y?a2r-&k-F3Pc1$~K- zW8VQ09#og9KGCi+M$zj^d(rT}L*;t65hHcWFLKV1f2FFVQ;NoOUYd_23@dFVdR2xwbmy8SJcI!KeD63kzGK zABI59w}_$1?pjqVpz?r<0I);7<+)=mC=1mL_XNF)hFg|6aLQP#iFGy9vP^P9pK2}+ znugv4Um=5u?9}znCE^@e*%#E%CCVEwIoc7d&n~UanHG7;ifI?uHdVVDXmkQrc4CRa z2n;AOy=bhiNhQ$k^9TPAX!nVI%qPoD>}mcf{y*@Ae`8W?fC;Lq9iIBlxl3EbZwndo z-~Mq&R_mcfNU~n0^)2pPMI+)G>+yhr1G_?}ai)*qLlUGHP{Lv)>#9v>EwC6;miJ~x z48#sx;Vxhha%1ur7q~kjQKO>6Q3)>qF)rn0Z6r+RdTJrvj;7JaY`pfS=t{IhDq)Fe zXN~WVzT0hzAEu=1O=Sz1@+b$6Uy=U941U|8i~dYP;TrIx1G<=4GKg%niUz5FmCkgo z=+`)yke)u49SBEopwZ59VAjE}HyyhIiGX&xMAYHw2;*Hg~b}$Q?{9Hnp7U) z3mcpmeHo|vRj|YMh$31TVdq&R(#7Ey6GL~ zDV|R|OipIhUhQaHj%10ijtJ$=nmDDCbjJfxebB|0Rk8f~Ts~hQT^B!!%2?N};Ok30R zpj?h?JD$u0w)6kPjPfa=gKgz7eG0BO&{UU>(p4Y+`UTtd)UU8RjSQ)+fCFao=YXSc ztW;mG9e80l!Z^t-w6eil8yVL+M%&A1V}s8rfd-_*`OyQ5l5Mo4MYMs;_!?V4Z}MtWj2#*N2>I%2!KT>v0&+gdu2s9% z^mnJoNWX{ad71uL*O^s`nZ|qP=v4(Eb-X+4g-x>r6Cj$dg^}i5JG5lwI?!lxu{vq} z^OP-3JmD{j$?m~HSM+nHC29UP`YRMk7*rzA@I4PTHm=g z$DCn$-u5yV?Q?H=l`4OySkTaEXC!cQ&OR20dw8*-4LMP^u%n*>WO^ovT>}R?zvlloY+JoYB>} zTpk8_7;V+w);Y0D6Q;4Mq$et9A#*<#Y-cgBy%R%!ERvyB>FwH9YMoHnTp5qXuNkgc zk);S0{es$>+R=&{4|iSct9AC8)*Mwa4r%+T7Fzn7aV%cv*OKZ*+!645*q2rOyV3@J zKi`_)w(FU3LbvF%Z^P0VK;n6VQpMv(`W9!L`>b=ePIy&8z(?tJ+j@-f&V~_@;d$0c z24@lc!E9<=!po7ZNRdx#t#)Ls?6S`HW!FyW5@nj~#C3fujn!PGXLc~q-QXmKylC5^ zeUGdR9<#+X7#YzW8C;ZvfxD&GF~$YSUrp!ZwTXtHy4tsW0XF(+mqeuPYz4n|L{HXq zK^l+7lgJFfoTGLH)WZ@lWI3CA*ywE&?P?cia<@N%d=H(sv6MR*dRE$pG*@j6W6;Ge zZ0c1B*OF>L+KmaE30m4g|TA`odaeD}nt|P0tLR z$x~Zxw?-XL(iwxCH5D(Iz_Na}#ro}{wx}e$V-Ua&_+fQzEu-*;G$)J2AB3AuYTdui zgY4E_(hzG`Y6N*#5|?4-S|H>ZJ9J15QI4j*YeQxL)fyz*RYO?lxii@cV)W&pHy5ANBx zYe!Gqn4wU0pG|%JKpw_S{8`foc0T!qI3A=U#Ohm()Y45Y@$=zec;bvH0 zI^RTf!4VzEAb*nY!$fD&?*))M4Rg2msGtjtopidfkNzrUkPOb0Y7gt~B(`u}PG;6L;F zUqOMft~MsKzRH>*YoqJ})^F%p)*<}<&ZZScmiL;I5j?i!KkwH2_wR0Wtrc-6`rXk; z-z~w*XRc^7=x}!DGheb@8*Kbxf0c(_yY|{$eCDYSxqs?${@({qto9~F7HpEYIePYH zsJ>@*%|moem*{a)y`-Lle^J3CyKsur=x_|hM+%U8=?j+hR=h=HB+Nhs{(-(dsXgiR zR@TTMoV;&Z9Rc5O(DUlLNqjk~F<#-TuQ*Fp=^ zH9VCj;u>EPfA}LG{JT&y&8wQ&zrEIWD~t+WJD_zh%??AU>~SeqL+c#fTjIf;?2Q(f zrpF)x$2Ce%rb#LyIfmC>RzojbLK`e81a4_i3g5dNh2Hs4%2ard@E^dZ#!_KfPv?xy zxu9zx?(p&XA-WV^qXij|gRn>oYsH|Txy3aX%m)kMlq@n0QmF8-D{70|@`RP1(25f1 zb27iXH_(I(#$6QLl`ON0UOE!o2(paUw+OTL&5)y?(A9#P?2e1U%r!Kfkl8F%}jsu3A1gUmd7(me2oHBvgME!N`s*P+F^E zwAi!0Fw(jg%0V}!_g*B^C5e7K#u8lPY^Rycw z$%9zH6kukWx@Ku2(3oXS^Qj}1ILR&5j}Dxefqv|4U{4-0!0$agIO!hI@%}g%`6uHH)g4d_VBWkw zKJ~ROI$2|>Mzcr5-_wTvpfx5lz`Oo&T(dCf=uY^67xk`pfgF&2*p3aI`l0O?QQoGr zkwozHtah>Bg3T~F-#{?Smxil31(btGt#m5}2JS0#_^aSxgH-r58^Rpc%6tKZHF{tO zg-PMl+#3p_Q0A4r_7LlF)n-%_X0fbyE|sl7Rt9@QfB zHM+hg=EKcA+0VHZ)CWUFTt(A^jiXWCXqXxC^dnA1AYJRUwXU4FqFGs$hg=QPyi zWSIVSgysQMsUK;xhEUwn)?l5j|1Z}iMia7hD3UFhj-PkD3g0+xa@j{{b;lk78de}k zj|0a#<}&)r?Rq}9z}2xHuP*j_7V++F?byR7ru}?!?B%)-Hjn%6{Yz5H*THFlfBDbP zQ>WACMW@?3*Z3`=KuqI_Kl=_#XjpC6yY8d^3k^jJ_#DS5-L%&E0x1?9opS3*&jE#V z_2^lQu;v_FglHxXLW$y;dg~CN1t1ABSvq3rM^_!H-_hTXaLxw(<%lvk)Ge%WafIPP zl#O-|fXEAjI(QWF@zkf*Iv#IA0Dr8Ms9vbh^%Wicjl5gQla(Xhif*eHfOo!Pzae6r% zUO~qT=HoKdDx?eQXF91FIJ?)H_C@s`(Tae=1fKe|V@A^r#`X-0f}Q&J2k6WpANSr` z#x=Kw^}-RH-?Kt2n67noH$#^bVA>Jr`5*bSs#KE6AwlIAO3;qleyD(zzBt4)^c5I! zjmxLSz+dWnwU)d{*3=|?^Q*dQ&tFT!+VRWm{^OdT2^cVmtP5I``i{L$+m=+Q*ixR?RJ%32i7kq@~Um(y$VsMRjLi z&NnTBTn)$fJKQZgGsz=eQ@nI0o4B4;t~`+RkNRDtfPs9*;JzMY59fAqCwKELPg`l) zkdM~pPORi%KYp6vGbGMzr$*Xy{~qlQ^(F*qS+_{Y_N*jRq6|K>x`}_kNJ9NAhO>T- zGY!5jG_@~Ty(qvb;PBO~QnkAlMQSzx95F^bwJ{zzvjiCEn2W9@>o+3M`k|D&7n9k? z7Og9YDE&xCd?ECnS*1fdjB(Pheo)V5-L==Cg>nn1r=GzG?~iD<*L>+GJ&7m8;RE1> z4s~UjD@nnD`^S0pPU?%EI?En>7|0^G;ypjngwo7>LE9nrymCVmX#&`(JUGHocVR5g z{=o_2d}>Nkf=SZ;xzj&!s96p9XS{t7YrM%O|D=&a0wUS7V|ZZjYZ>MCrGN zF)g|l&FE4@f8-BF8*Jn3pPp&#$}jpkYhYEvm76q`l#t;b$v(EWtp-OTOmnb3^$Qs@ zSjWG-RdMEdKGxk26+R3DZIY$fIc;yx1|RQ~+aKmUpd)^LbSbirZx1(({g*!SEL>kV z()6nr=^?oi;AFBS^xh4#u})7b+UtO)K9BXXHR>qDH__#It%!;%l4|W5oKdH8iyxbM zH!AQrAtz|=xk`s;QI$My_QZaLb9?natgi7s3uW-&voAlk~_qT~n3hi06>%UM( zy9vqJ<;`2U3N)G9wT%{+X^R0}q)&cM1L@mV7Mi~`z;h$mmkU+sEI*3u2k3mJ0|dVQ z&sF=as;uxV>;6%fa<}g_*iRQ-!nLynzQqY^-AE+kh6s#Ft9z=U4XX#(v7JAXn!ybc z3^pD%J+Ei^8S}L15=e!s3D5nc7i7Zp)NZXwfduZrn;lw?~~&eTsU3}ecI`lG6ON0SIeUl#xwj8oox!KFV>2z zfMEPpPE!+`VA7Ak*7r2EqNPT)MbKZ=@YbuoQXgkirT?scvfhvZ@6FI}>fkfn=&rRX z4!Tx6tDas|)wPOXJFV;lOa9f8in20>W{GgYdadd1lSD2jY&1?6l6$t-sE{~Mj8xwm zgDM2*u@_?b%!I=qS3qutYLZ7yw9u%Q-mSC+$l+E4eB7Lw<$b%F$%yPtCz3*J5Q(9Y zc~ImN-L<+h^(lBTrfJdTndOFvIr|P23QsAN>C8m+6o>3aP9Xqv=C0+?M=kObB-VcNM=b^P9 zJ2OKg)xMu5vWh8s zUlaQLnk&uJVXnsNe<;KDZeTZNyTaEi!nqU#ijyx6rwxUsXBO1lBAVBv8bxdNT2QE$ z-V|eRCuaB5<_h`DARBY6SP>@QRA?>Nm-I6K(okBQ%d#Kd>MT_E2Xkk$%2TtVjBWY};kzS1rg5t%XvUuS4#b5>o?t*Mb&MNa!qt<+ytuyt^?5DEw;6 zR*A8(@b(?I(n0#xj{6JK(PWu1QXfiViRnwK^PWBQm&vc&Pfls`mTvTY2Io9QbKBz< z89ckKWzjV6a39ESN7`R{X>O(9%tnar%0xZsq$lk>suL7wQm&eP6|FAcyFvdNIF?|* zYFr>*$!*-2Q)STWe>d#i0zo`cx^5A@<9cLgn8ukZqs4gg0Gb8HmPm36;!(z@KUAU( zPo6-sP#0{$c@6KNeYHTL`==+BjVATc@g&@>sXEz7`&*(Z_G@V`PE?-=UOo#<*Nnc- zb~5#E;$H8wT|Qx&kh9t?jjP&*_2Al!y7+8t)!qN#pKvn`D;HCme*V{69Z4*VF5 z)?Aj}5qIbtI%KLT8M~t!)_9DUuzWlHJlmnlO_=pql-3rvHIT2rmjs`ErZyS_|0pDt zMQm~BOUJqOao5a_n(o4E&g#da;GPyM;E^yWKr_z<1Ml0*k ziCoVqwpvs$2Oe0eE4~G4x&qSO$0lzoR^c*#Hb%2=KyO2aPX)W{OpG;(bP6i`Wgja{ z)!|~+Tvd-TK*2xRWc2mawc70CN;jQu{kSeXu3viVP7zJ^wKo~N+X6YdJjTA_ zxV)uS^hk}RU zmR+GBYln2nJCKey9o+!L*6#Lw5SxqnlHsPyrqjbx%o#b1RNDDyU!B}Nob^*E&d0tG zD09rTzV+M>4Py1-S^whnfi+{U3$35Up!y0>@u6mr(W%rXM z5AC9t4c##AK;Mi%d&IO@^L~%x*6;VnOxbyA@WkrV$M!tY?QDbpLN(E3&X2^%!Wxm( zsRfpKqLwvtr;ZlsJd}v#K7f1t32Lpt37sCNaW2XHGO7k_d%R>d*#EUH8Ryadr3qIR zq6%MUduv>x#&q}8i5A9dxUGw&bjpB8nA(i#dhd~hKQpzzEoPdC=Yq1hBrCEA)y&XK zw4f%oMo2GMk?I8R0dYB6KiMPGKLrK*16AfTHe$2KrhJ!(t)C^W3e_C*R8=FVTlR-ad5uy#!=ACKEq3tpqtC)WmSl9 zX;RUmg$vbatS8QCNxVD^^?Y6v0`yCsDnshqIZ$Jvxiy2>s@Gj;=@HtAr#$Wy+q)MN z+8(PpmZkG)qILCNez6)CS`*Z9?P#JCG_)9w1N_cI>#gC}ONA)~n8uvmbnvg9*cS)& zMip)I)ZxL7>c(2+qzOP^jZNihNCGDG%aJYw!pD38TilK zIeAql-LFagftu#k)RQ$&O-t(LikhOrA)Xsap1(n_j9D^e0e@?EPZwv_y%_&>xMR5p z#@vs>cmQ=CmnFR52lh$cvU*l21O|DPs#F}}Wti*ISLD11%VX*aGDzF0&s?m+4 z7Eb0nN>((tt}92)n_9_T*Nf7z4Ev77O)CW7KjcnSmr)9vOuxpz;J-t*iI&xMmkxW{ z(~3z`^?sNBN}-K(4@9d2eS2YeGQw;-YGiU5_;0gwaIEeDGi4E{B|%wrQ~irD=^0wm z(p5`41=-c$E_SgOyE`w!Jtwnnt%SPm>$1RSY$LsE;#=u+%|2Dx`uXPG)YLbX%@UXG|x#RpmQ{G)QKtE2*>A_<;b{(lf7RTcC|JucB+A zS}fy*ZnxL|9$IXvOg0WW&N6d^JC|S^B=FatUmHx?w&(u)W0*mcbziJLpJz~^O>`$ zSk9M5u3cZ|*kx3{X*;CI{O2jl+c{2-_%GBH$xWqt=tXTiy$!Uf8c>vp-;A!Ii@lQ& z+^6Mh(=NqP3+zkv_oDolc(RXQ`$6V9Wpxp{6_Ge{UDF#B1>jC|>tSjD4dZZ^(5Agk zI3XeLtZbTSfpu15h-qH=tOU%txOJ?C^^Sq_9yEyUiRt;B(*%{}TJG-V-=QP^3#_=m ztJVjQ!jF=pN~eRam8YqlHs>Fx*Fy^SYRxzHR4t5#0lT+k_i|)vro?$R&;@%e$n{uf zxXTE_!Q|qa>YCvKgHbS%64qEB+QU<;3n0-i>Hs270B>h&J~+!frka-?q!1E}(gSu2 z`8|_^GW)Afe~n7e!SV?5uR2n_4P^y1KhI;QC%M7ki#2UQE}CfKqQZq)783r7UKvba zH7mNVRh>1oNYens=qvoaTw7X^EpZdocXU8{wWQqLnq>Edy>2qUo7FY8DLHr@3|j0& zoQG>Ycs#kk=EmQoDZOco^c`?5&1XRmHA%fs(i!LtUeK+UI#W~YGJK$;{+j{agCcon z?lDGH#XRioLoqkW+h`M-@YL1X-nJ>h&aFCmV0aG)nagQXR`Y3PL(LCs**14^1H(c1 zu8dxF)vQ!Lw8*+&xzM|qRh!1Q+1!=lZt zjYpx;y$W{K$kzJUGcvlFUNtS)h8V0feJ}KTE)Xi?ZA@0hRGp<~QZ(##TgO0`Pr>Oq zljT604*=DQ<<;WSHuQz?ZQc>;{-UjU0bc4{o;@6)8^wC}WdBYU!~1xx^(NT{)VzmTFvjZbc^S-Bm*?CzYX`2#o> z*T~M9QLVb-%J*e;qqxu~(_`$R*wC*
&Ce|!piq{$Q72JzhYC@ zs{TQe$!%MkilsT9)v`L%n_F0CGMj=gJ;0Ug{M?wzEpQ4;rgUfvGhN4nW@!1p)KdO7 zRWyZyI&FyqS0ZnP`ab;;fz$n2mPygMnw0;ohdGPdI!0=AP+Vd5>}RCzShU8c>s6-$ zg_%^vBt0t{tD||MnzpZYn=kes0YRQm)tjp?l+FXF$+>x%i zA%%-p*H87dF-lLoF&D>~E5E}q{+{WMl+Edp ztevDq2I~0W(hdB8)B$zonuSkP6yKPpxj_xIkZXzx!BsRNMspgcb!Nax-UfK_mqLuR zrbC1_42Y&gsF_Z5fK^AjXj4i-q;I}gtsd;aLwY&rT^-$~H)kc&9B6yK7Et(UCc<|m zQhyLCFI&}TAX_qqZ9k{CH1K7IOf#tq4!V`d#^1`TA7esnF_?#RG@HM!=%HVzW;=xG zTP;T&@g5Ksh4tOUN9keLghzgZ3lHgbArmHEs0WE@W%a0e9^Hv-2G4BMLO+f!sU6}6 zI+=vvi~4NE*t-f=Zc(hD=4U4~(#`M)ZzxXMVbV0u(tv#VC>9h0bY zl|$joZ{EdP_hjEmTO8D;S`*KzLG9aVRRg+%vB5t#3$^UaRb{Q><$>q;>GxDEqL#>N-!?7*@|46N!J z+R!dBp%kgOS{0dGm|$FEeeagz%U)3WW?{+Bj$yCIZRhdLzWw` zhH0DoMq}(dbqzG8`2TEza~-MuwOH8#TANQtb9&^Y>G#?!{kc~@9PdN1wA(V5qG4W6 zp2#B*@|teXUsLd4PIbwb!cK!rOw~lgik7j7eKaal4+Gi55&G7oxZb8lwAWKSp@QM8 za|ap1Xs<_L)YofOK~F*hIP0EdsF?`&NIa|2Iu*t=k84#3WSrSVlX{uTj?Xo-nwo_0 zXJncAlq{!5y<#|T|A8{utIGrQcXaEr-eeT8=fhph8!bvUEr~xt?06_ZA&mQzJ_cy* zpj17vDIL~|zAAkdxDZr$Wp6Ti zM|G>%6F+FHe!(*~IY$q4A}~+EAT2HzEK%c>#ZJvdA5RL=(SlBZNV1+7de@J+yZ|6xCQ-eY!xwu>3C$6iyUaJy=6 zZrF6bU7?;Pm0c|Lxpy4QM*;#lbM1O*oP80Nv@ILm{Q=nk;~!6dNCfTr=+eRh+lM6n<)q&`buu^*Z$MMmpLQD8+&znY z@s@n<2aQcFA6Pekr|KvcU~!?|U(Ea!(|D^H1$DHzZiiSv^w}-K!Z12-19ZQ5cw!yc z@{Fcf+oV{Tq=8j1#b0=!=#^dh^`LnTJS$6TC$d1j^{n2Jum41&!s^-*=mMjkbaAe# zcd_iuc}&<)ba-E?p0f1X5@e0?u*zpG>q{y6Fy5#0mL0CZIppiIqwZw7!Lno05Ylhe zyElz@PG|LBvsxusHZ2g=rFQNPRz1zB$URnHquRyC^Mv2hQ8x33m}ZB+_p5+Ebr|TQ zBJH8;KYG(Slp*N7YrXi<^+yoD> zsf(9;x3evV)Lt*8t)==KO?wD7oZDT$7WdY-B3Qif?CQ2M)Z26(R3g>0Q3vhF(RS)y zoNMT7JsoO?Dw*$_C!4DN3a;dd4zHjMeYwM!k5ieFh&12snZHK~8d<#vqVg(;`!8^zfE_|kO zJ~0Gs{3&fcalO}ZunnpjIy68L>g9ms;o=&dzD z6*RwWPX2!FDp%6Zs#rAx4{yMcC-q`44HV@yo#}=e-Dj5oZ*J}gQ~pt0&pYaUE@rkd zTjy#aK4WlIfxQn2Yp9uNI#*C1;(2Wyz_Cp=DXm9g4Y!P}g68G+uA5a#Vp}E##FZy& zouXCdn)#Oo-h1@dq&FayyIwCg}20X1dg#p;_ zz*Pa=-H%BFATTEBI_OkIu!mo&^05<9T5Qm{E|^Btqa%Del+c`(cA*qnk+I38 zHNbK4aQ~!A+-)_j)Rn=xTW4bXiZk@f$N|x=D2O{QjdYv7Qe4(ky@7m;u2ZpfUp=mk z+x|yw9Di->q^tg#H>k2kBt~nE4-&s32PM*%qbzM@XoDEyjfg!c%=X`E5~oQtE7rmS z`lC*cX_|~sfeHTT*z0MFw)Oh}qc7@IjqJ9EeDM$f9ubRV+35u^4LtF*=^ zytHvMy{p3Jdvw|KB3{H5sB8T)TAt#G$ZgU`e?2Iq7rv+@F_~}nL@D1op&0+nubH7p z%dlF6z+Mntj}a+CHK)NpAXT#Sf<3X)@GVB5Ky(7ImJxI*ya zOBcQMXslzsLye{7TQs^wwDvWCXWugB zGTm)mYfw1cJcXpF&R87Qdyq|C%;yexTBAy5_p+p)LriLB1ai5iv-UUBg@M&{r!mV~ zgSout%GEFwUO~l?ML-mu;#ao%5#*(GWMLlk3*apk*xz#<9dj^8LxHm?<;JRR>1sWX zz)Wk_djrGztdubdZTi;o)>1FNy+=RN4#nKYe6LzTqb)Tj$JxptZQd-2eC>5i4{vK1 z&W&_cHtrr5w69>cA3DUvgmaeIp_6S=^|BQG%}QmLO@PE}`PA9^aQ*1P-LSX&y!A(5 znMz)5w607$O(7bp{ph3DJeQP}8MRQFACaaj<#Z}{3H0!3`?_3TvucPl5i$A|ZA6Ly z`!XvuPD@iXHKdd?#2Bkvtt-GN!(4T#WwYcuT2WS$0;(faUo-#N>ZzKRv`HM z%2;rh)n$;r=E4_E0;8jMbTtpcb3Gct4p3=ncOLLjA7Y@; zj4(a^*XdJdQgM_fA~&mHbfSKrZOXRFMP3K1?E{t(N{Qa(|GV9V{{L;hc^#51tSv@Z zI1J6ToPYi&{x_kC9Wl>2`5TOJ$sYd9rsS>44+dX|Ty!pILPhh&>^R<-;B9`KP~x8# zYX3hzx#8-;=f#F^+xNZe@f9oG=ePt9oOkbTcB*A!UH^3_YfYbg!!DPWXWV&mZ+FPL zMx`VFeRS0NfQ@fWZ8IV{J|Hcwjujs5I&BTo8VB8|1A9L1$Jt{miuO?l{PJmdMQ@_Z zx6+k-$l|YloLGNw38u-JZjxkm55+PCg?)$bou-(c=uwHf8dbu+V)mfGF1$6l3`|cR ziETCHlO0MtNq>au*af;sVP_}$oTWr@DhB}3uk)ccR7RKPQzrk^P z+*?BfO!3~WGQIJ8y+AhAdYrn|%;~6h_!TY`*wlmLhL4?|H{*%pQH67KuOB}yDcdJT zi}UCyF8zD`&4DsAcCd36n(`gW{Rq&M*ORo6zGt53;srQ^SHksYu4rFf?LCOqf6?s} z9c!q0fD3fz{r%2Boo3fcDS={{z?$y2OtZ{~rkZS_(@WO7tlZ=VcRJ z3DT&>0EA~?PcC#y!zH+;)zLajFZ<)|QNTYG)22W@@QA}G;uSn9M!STVg7y1Q$%Y16 zt68mSGKNC>FhqC#n9(Ng1}^f%uXQc8B9jSzDHB42?#7%O4bI7VgB5cc8%zT)+6`~h zDyzaRY^p=gdkphFqkUAt653qbp|S3I;$&>yh1Tk1g zJ8O1(eJY7PKdCKTa`)+0Kf$k5JdgR!Lucy6qVq25I4MKgXApjOd4@+0K!LAewVUed zS1yccYo$^q z)+wbKr2C?kHNznOZCv%eqEExn2;RKMY16AOPO**_iPdC#OqJ<3u};gKDr;C~U=}9! zB{<4Dli&1@+S?dTKbM`+TTgT2s=kD$K2&1?2++MGnBo(Tr)gDlNOQ`-Gk>LON9m}5 z+WcF)XF9kH!_V0HU@Mfr}g(x%{7Irhjg)SaX@7? zx-Itqk9?KmyIJ|>`GFO3!S99%p=}2HdB1g;E@PpNT*>G|LhQ*unvaZx^ zRvT?{t8i%#00}Z%XhBol(DT}vqj}V1a5~A-lx}3|HTLmthrtJNFuAIJr^RKKQwJ@j zt#TcNaeS__`Kx&_#i1j?eWmZ z60zp0<1Wp*Becg!Z>Uzx)k()Xw)FkdQ#Jc;h)C*%S&*!FiEDL zb<}+bdrrYeAN^j3{W0lr7sxp8)E}|mu?(WkS zmzUzHA`x)REt~RbnvvegzD_4v?j-w{)8g_>ZTw(Lje}}vS50QKfQp#f`Z=|2QUElW zt1YIYeixtOvaM{Y<`fAty#nvRkUR7?zb-^Yr_z8q)ZAY$BM;G)$9%G>q7Q?ebT*j) z&>(p1lKxIGIFB5K1`W}Q8j(XF-z}Ml@q7K5RWcp{x!=LMF(#TmaPGR)hgg0a{n^!r z_SfYIZdy2Dj=m0}!Ykg8W548Z@~xRr@jnx@ohZ+pXF)1CF7yd*31u%;2VeDy|LP zFup$)(Opx`JV}3-YhEIXZ;%FKr&U}B`P1$-TsH@>m)qg#R|DKQzOE*e z=wYY7D{5=BR`*3lK$&IhW&?YWw-MTLYaUj+9?_c|Vx<>OP_r?Os_ z?XHIr{VTz<$917N4WtZo!x@XFe8n7B$1}-S?8M(%*vaJ7p5d&xR!93~l+wvibeDkx znO&%OUcG4FAFKB|0AFRchBxmABVF_MgI;u}OxFCUpni;JN`}a3jWYPSm$lBnKD%~8 zk2+~d0WQC2qePYfyiT;NtSMQ3Eh2PZYYz#<=;37Tp~ttjm(mmb20Pg0X9{=q&e}~E z!KE7$3}>IoU8sONGn%I zi_IF2YF^}P3;LmnuRP<)+qAPtL+xt=HSV~TFN1hpw>CXczxe4;&-S&OA%+V~q3#OU zcL{`^9>X^9(^TK?Isisk7z>@CrL}a{)|bzoW4?>HN1E#hQ9rXwQ(N27!pfFPt$(R9 z*JLZ#0@_6FQC z8{IY~=l}ld^x9LdHQ(#H|+ah>;na$UwS}hC+}f-3O#5@f~K z)*}3lJLp9Ka98_kts`rH4+vF*^ah#{k)%o8H9D)ZHqfYvn$dbilu&N zq|r&dw36u$&w{;&!d|c1Qogy+QHu-m`UQXP076zC%z8?{j?3u;ChuYInshXx# z)k%6qrUp4^T^g3=3s*+k=m@({3)9Q|BC!)XFh>m z;Xqz~JGglhB^4TOk1t}UkwsJdERfHIOlJzh0r;4n_R&Lce|F|7nth=^tNN4^*3K_Z zABt#eydGpzRoUymS530}%WVcS+Z}Ahw8S<6`aLj;T|HSh#KOWLhTU{PD^-6mn9)`v zDZdYZM@>{}hZCOrrEoZUA;+qv0|w^S`|vva_(AIn!{Cp#u{4=)$&`44j6i=uZO@BZ zFg)|dM%M-ub+JxB>F?0dwkcug=4*O7h~1v3H+k@cKA|b*^wZD1bf!s8YtGge8#6x> zs@%uq4xo*frG|l^tTT@2?P+?Dh9_}tK;8OGBwQ52e096{+EaIX=9;I~FsdIX&qq8;*E<>)9cL2|xWunAWbUj{2Bkcx+cdp}| zroSTqi0;h=ibFL!c;^(|Vsa;2Hm?cnc>yLdM;EH~Fv2++VLeT@0GoB1;DN7(2b;8} zIgUxD9B!IkC7|Ojb=5c;??h*F1>y>sPvSzJ(}7+*c{$V_-!K&9oO;m3D4}(%cqLZo zH?6XyF$5=LpC;wYv+0NRx+C--lck3)OxH9lnURb{UPs8LdG+aMo7uh!PRwz?Dz=ml zlU{Y7(XRD^O}^@FtS=_=YZYW^k|vloGFxji#wO-Yr4a)7C+$!uaq__RE#n>q-s-N@ zJT>h-w4h%IpSWUDy(X2`j~z(M>8g9}Kqid>A-h+9)9Cq!_H4ktR#l9&-^6MzW=((E z=vJr7MbIb=z1JbPhSdV!)d8h-8Up-i)kdEiv*2a=kx($Zy;e8c`XKxcQ1qTA)1G7i zoOf0uO4{pt;qvI$7s;5G13H$I59-gr_8v=)M;vyP?1bjH+z6lSl};3Ax4&!dpt=?n zV2Rz8wKg6#_%ljqT5RKrjQ3Z(m4>xo2lh1Oo>JHAda?YSI%Zd_JJSCM%FoDEuNohG zq%-6+4MC}y)-9BLLClMGo}R=-xE9l}5X)Ss(n~G%y_jj}bpmS%%JYbTUOE^D#Fai* zVK-^FbX(`aw1<3`DZxIhPmOI8+C;HuOPXj$s4g44JB~T#w7b47Gq}iIQVW0hCBLR{ zo*H5<9?#~3zO%ax(1+sou*Ixu1%Tvh?(69fagUkfqMqY;)6^{kUH_YYpij*~t@ZKe z=O>zGqniWU>vm@h-Oi45k@@77q5TQPSTv2!0$`X~MmyT*Nnw!ToXYAGP3UTBnMvcj z^AFDGKt4kgu>7dE`z+^XPFMRNy-w8TX6DMC<|8#i%{|j^Nb7NC&^C+x*c09Z&OAS` zr@50huIIe^X{BL{GrYQ$+qz{;_ibV5vLXsM_<>s*R~9|OXqTvoZ8+IG|HJ>Hb2fT5ytc1NF&fZ!@v<_O-_I2 zUSczGthFysv^hM~CX}Z?+)xTzifWy;H9LGU21z~+`l5E$tv{qbA~Lo$s_XB_!GOLu zfIK7&>0TN>AC1P_=yo!TGT82`P+k`X=yb9A+TITunO+5Ju|F%(j8}+nBMmnYzQ?*4 z!jtc}49u^wP0Ho;E2LMY@h#Uj(BZIJI%%C&P4DRj*b&*7?GvxHr6Kf2?av6!pchpU zIOGo1x_X~SAN)y%=tFNj%W||;&sQLzg|0W~04+y#Y1d7Y3HInl4|ja=Q!II$FKjcs zU(tx}8fU?0IOCV=hI{gWo3u!M0i~B)#$5X zWjZ1p9540VtG5$$UaSWLOD909TWE$al(2aalwzbS#oBqmX)|e2(Tm15xG z`k_3JTpmvSXo zo7wG+@<#`PN`Zpdlo3u!8e6wR>tmp>-WKD03d_7>Risj3i+1Se6eRv`o%-#NuFuKn znCHBGCK8o!hnqgAyH^t``9sgm8i|V5(<;3o>sq*UNwHILhj3&`}I1MfI8^3rt)w`ev(08Q ze+;gqiAB>jEH)shxo$Y=lpkf-H8rUwA6y47-i5x0I%`LKa;@ZsFzXTaf1h@ngzXnK zQ4yP2n(2!5u4Y5MCR_VZp@u`+v96C=Pb1EJBoemAlOfj)VjRiuSiNtb&h=efOfRz~mLOTzt|i}T3IojF3MX0bgt z&-I#SO^DH zxM*=0^BYz_OP@;U*Fri~7i&bsif-3gFqE9SVxm~vFVOs+j+cRw9`n&BIT zy3DgmL{ahSTo>2xk?=q1Z8-~vQoi?r}RP$6j19dI} zPUh$ioI~9P_);Tvpm~h|tuL0QsYUz>Hb88DE!ock?9~Q*@k3dFf1|AQsC;D}Jgk7h zkvTPxh-W)}>Y~Zn;36*@fX{OxH*#=MA02zbz@N0$scbpjZB`4T1@L7aUCe4yTg&5< zQrUP_XH&u_RW@GFbF=fM~{WWN_)2QE;FRl|egvtDm9M<&9}M z(X1*PzYSMri94A7z4g5(E5h4HH+|!$Rpf&XFxSP0*$%*gz5Kfj)<$QXqaPwlX?bJv zS}mOQye)Qqh<+G=19XJ(7Bku5&FlDe)4Z5mdKgyCGhd!Wm$q&DgaS>e3l()NR`Uke zX;XvtZ~+Yc3;>Q2l{B(d{Ty9uuIqL5J957aHL^~xT;ZR;b-y%0wqYTxhw?EyeX%Ov z+=8N>3a*;n5!8y`G_Rh0-@FL;1c-N|81$ajubkeyz;E-k#&kRyi=x;K>310z`f>dE z(u@Q6q{5nPgC-hL8P7`xS%;piix)x2fq=J*dym@TN5}wgyU1u6Y)e8;t#D(~xCa!6EsX|24 z{)`TF;~QJ!(_>1pF~_vrCx~^eZ`eUEDJRfhZOUbrDhvMXHs=y`dTK{dQ}%OVTde?U z2Mx6}2M+m^p>eUK=RQXYsVwiJxtSROl-Ysyw7E~JwznZ2L^GrG!oNX03tAQ)$lhL% z%Hiw1&f80Si}=Ji4j2-z@!iqLGxWE6VJ|%|g}9zdEs7p{i!HdSJ(UOALx8ioR7yk3 z>3W%eH2)nvszG8ToOQVg+NxcwpEmc>3=?~~9s`C%c^>$zdvl(&s!HBs7{VPe`$13G zYn(=x*BhjbZZH<@T4`I;az&9zBIB!FLfolYb0u?!vaWvlaV?8euSN=SE(p5?TJDah z!H6bVeU{dg$_s-{)2OH@?8sU;>nN8Muw85u-6AN#*;11V=GJyAyDohO@wmOdgbaQ>$69VB*)t2^lgS(HH3$>fs-k8gVY=0fS?w93;Z3z5qghPnI&9gO zM#)}m=~4rvNb+=>5`o0|e$!i;si5_KgGuKTiD{cQNN=+dWUdkYbTE=>G}A0oU%0L_ z9DL;;{D0e63%l3+^`HO#LxKNL;6D`j4+Z{bP++W!16Mm!{kj#d7TCYu`Qmq8Pe`2~ zG^gXfkIVO84EePG?Af?h&&MQ>D%5?w`;l(7eq1!@W%sxD?1G978hp!l#MLPKF~e`| zUU)a_zqs1j-wKE5MHJrA!CKau9Vm9v4aU4HdF@;dP z*GsGW_3Ec9j@YOlGeWRgXLY_Nrut?H&39GF%S=GI@bNPj%B*q z<2#H^bmUr-_g-Lc_lGqgE3#gksqCK2-9YXp+R%uHk0eGw9!DCmhW#;6U=C?c62lV5 zh{WJ?3nXm+0x+D*hx_17o*rnfgfHR}Q7zEgz(?=GRzBDD&YI~CHzFEggGUx;%`bT4Ajbu=SCs$D(02ZiLtwWdvU zW37k_O~w3A#Up!IgtHPA4^}#nox*IlYoilkgtP*l6`XVeSK%iE3P49TE9N;xw0{As+73AEu??C3hUqxixCyn{U)vli2(wm$#1B(=s=R z`FCHk+TYva{;Z343eY+~DD-EhPV}XQEh$YG+v$Ko6gjLDg*gMB!iibHC$6@!scut5 zcbXT|6hB_QsT~FNx~Wq+d>x2X#I}hxnbO14;jY>jrd4FPWM<{$F0Z8#S>BC3vY-J7 z2?z9|t{!F8?Ao1!%Q|z8TR@QC#0*Vt@jP?H`zcRcWKXT3Ji@do0OT`WH>x2~#NRdD zU)y?U70E=_jCVSrVl!7HWP^rhR)##!)6(*rY`Lq036Wf`Ou%J+-OAZya=GfYKbcT& z`n5;8_Hm;*zEozS&NQk}GSdzZabH1)_@X-M%sIUkJ9IO+RA5o!xITqzZF?l@6O@}DN-9;H9u}m>njR#i=!!qAW-1NO#Oby*C~(bk zJHlF@*zl8@S4Z!AXlRbVu5xqjgl00eq`$;cc2Qrm-9xmmPkW7PVA5kQ^-bqXQ_&zR z>e1g}ZKLR{u87=wJr2!{*t|Aj2CdDEsio`P+Vtwy5hkL@=Z@)FJ_Z%CBeYF8k3C;C zfE!7z_SDg$!K7`MoM~0a zS-g>+H7x*-&0{9_`f5xQO&(kgUB9Az|Li7BYLbP;OrOSwb_0%)1L0V$ODTcO?$L6} zuN;w$g--NqtzQ?}_*fExn6`F{D7>b|GjEnqSs2&1mgmA4fWJ*3Ai4N3w~f62WX>0p|E^R0#s`HdMa zG&uMp0lgTIpmoXlo9J~|)o5+($tZg@wv4~O4}!pvcvMO1vn{Gychg!*g#89JcW$oR z<+!Bn)2bwLLDi2Y_k>G^TX)cB_dpJqyP?6SN~xiGk=9k8JLq6kCkS^gt5P>-Ovby$ zg>=(XFRSW)!)~TJXO*sdM7aBK%$gC;`+F0^a_L|m$oZ6(DzuxR5fs*!Xl^G;fV$Q) zYM~z-G*iEns-D`-Ao9V536 zUt>($ftgHtE~Go(Pa{ma?_?LN9)6KgL$t|7ztrGU>%&R?z19Fz{TE)!GV znJ0&Rea89!JA;p@jMZ2iR!dLQ*{n<6-u;twBGHTE`(Z{u;GnOzEjwsI>k@ul%Vk=@ zT?+^l9BZyavot)>Rzy>GI<$GF3;@3pgQDpr3-Rl*CeJDZT=U;H!_8vzeIVZDd@Sn{cD z8o?a3fQpHLAOrjPy%>Qqx&v!+X_W0Btv!OPWHZlo+T_RGWemz-DIAKyjzx=*83a&r zO_G{SSnaqs1h_#~w>Gt57nf3_jiM?S58<|W=Z=6Li))6GK=83na31^0x?+-dpFqH? zHjGM3sqX(W0zjp}bt4?u)Loh#&2`2|7~+#ZYG8$yXo87rozVd*56$`CGWTN({`a5% z{zHNPP~bll_zwmCLxKNL;QwzFnBttvrum|tE}17Q+PmFrZ(Vbw%hjM+7u@XQ1|*#Q zquPPFxqnR>JLT7Zf2gyysf)jUg8UWSyo1`)zN>aS<_S+U)e)<`Lba`N7Dw|RKnLi6 zQ+(8o@{?{=@gTQw{CKT*g>jiERrrp}JQa^=Wl^$6a^-N9Y$`404Z=0t-#QOh5{EN` z8k)MOecD&X6ozl}X4K1gVH?`J6v%3%pBfdc#{`z*(;e$<>iEv+SwONa4)b+)zBZSm z+@&V^OmlF)v&BmXobi0_#?|n`;oeluqztUqlN=o8uNqSer+kyQ7PsILkF|$}2d(>R zbdi2~6RF{G1}ExS1ub#2)lc;^tnvL`7sScDkDs_gGY0A+XjcXS{4wvW?99}^R;-U_ zI7&-h!?=ApqFF^d;A4&Nr*|FNG@$JDHz=_o%R<}S`!h?^ZRHc6->I>&|3o_8xV-YDJ%HTB)p6Cj zm(*BmSJx7a2bJWUcDlIkmnhsENA#*T2Qv2b_^y2YHGi(_K%g_;_6qm^gT1$o>N0!( zenF)q6j87P3=~1JMZ!WU1q4M!!azVploC|1yL0T$vBn;|#>*FOT8W7@ZW9t-y(f0 zp`~#x$hWc4-dGMP_Qq&*9?x@1!+a)cPEuWtq)zxjn%l;GT(N$-V?*gu_qq^%3z#1e z{CXBAJaPI`vwbqL>QlJ3%vaBQmC>;RQEZ0@f@|ux=Ek1gpUJ)^IRCv5<1O|qYRh-u zq0|}!ufq^+F+k$%g*JsVTtVyh9o8y((eYqD3rzQ&;? zOj$#qEztA1*#ykOS5s?RXj7O=k8WEhG zd`Jn%zz*y-L9EEt_bAOZNl!1RzsT>_=Qs@k|AiyQy`;R_>3D@?wm^sC*;M=++fdV} zFhR`M*s?V+3Od%cvjts1FG6jBgDq91HSeDZC}>oK4~Lx0?lXzQjdi9oiuFbBsGC#S2M4IjK=sa>|k z+V|G+T8Raq){!hdtd~?hAI#tCQHu@VB^G`?DUb_g>q?nsE|qo7pG#nmv`97_iw80? zT^`qi9#I9%p`sOd!7%7M!??m;q%8$u$N}5h8Lc}qjTIe$u=jN}G*$DP*jiT3E7I42 zQD{Ta)f-}t)1w$azN7tNdC<}ggo&%&czUYtm)3^j4E(@8sbQu@ST^d(RV_1wWBFXE zbt;ayzR}I5JaPaH{?5NMTkC5ahln@%bGTTR6`Q}2u9dWf?}bq~;H%wznm9&CLIt z#?-=X`Jn!X+YYFMrPY^wZi7;(jBW@9!m~Q%Jsfo1xwUZ`5T_me_~<0&H3e1LmH+^_ zezU7ZS@Rt|4MlQ4wlhrJn`~_PN(5%z0G&&S*I1*f)zIvJNKo5_>|D@`LMY`&Ew^+i z!WRb%@yj#%)C`IL!Svp<(4*9&8xTBSApCBm@uRM{cP!2@!+G){kKFmsQGmZj_HG+B zFkc4be#{|XR=b!=VaV3+?R2z*Z;fC0|E6nAtecr<@z4MM?|)$mjA?0Q84_YOB_PB! zEhaDQ#QxE1`s7T@OI*EUb){+#v#LJ&mf7FR@6`DsZ=RmB-_W9b&6YLp^xJiM&yn6a zK@NZQv|s%8jW+xFq0XE+Yj}5!ErkNU(1%#hP;_BAmofb{ z0fa@H9vyrz3)5_Y{;6a%ha1%!3_VExbV1X54sa+WeRg!phY2(X!JA3;wF&tGj#af z5w)8ULO}ain>y%b3oWwf-?TdR^`}L_)O;A3u?+MvA{RG(9K$!hggta8Y1UGcskXZ0 z0(4lOu2#XQ57Ye!uUN?RLv8_w)$4Lwa@O$)WToIh6SaVCMx zZcpl{>1R&2+Mxze#$ye)>O-C#IsUx|0`+fG&n^smwJ5kNLU&Xz z9P8E1_sgSD0#nUM-F;DB*F3eXH{3s{S!vMaAnX_q4ZhOfuxA$oeNJz&S| z0(flCCXEBh#XsBK8Q{cpVhgqmHFUg+ZdQWdpKB&|YT?vS7m6AkWQNuBuV$yIX11E^ zq<7$hYeWHEPw#>c9FAD-;sf*g`GcIHe~ikBVa<|hBkj=|m_?VK`e_bnzcj*y?I2#6 zmdC_qh3gfSbh?p=Xbkn$15dj4R89+I3owB{Td6Y8;8Z)kae;(j-1RS3qK>B3ge8AC z(0IbBYY-Z9A*X8tEsN zWFVs(tFnUPtoUtZQmD1DZHmU2Bpa7AkI}Hfc%-RSXpgH&)%o^PCq!|!CRGGVt51vk z5j-{OKkkIze+uIhKf-mT2%ospk9qBZ3@bgmL?u9szW!SEVbIVwBkjgJEK9gG;qjLq zr@uaZ+xzgbsF?GiD}r{N{PMwUOpxU-OV&EXv~7L!v43wUVX|Y^$njrB_h@&sYVsjhC)2XE>!P2^hMXlxR$4N5O{_8(3Zja-VeDulSbK^2jI$l8qtm>p*#ed zpKMU8UOlS3YglM?fE{tqwOYiCi>t7AxIt^{y@<~i19yVta;@pD-&`BvrB|0gkUmw& zsp7&;l*Y<@zcGC5It+_0KS(K50CNFJlgfd1{hFtPb@HE?+ON*7e_6D z{?xWy*T&Xp{s&LKeZk-H3^mC@l??8yGjAT5?L6~3F)|_rNizD~@vG3c^N&vi&}@BfOQ?251;U_FFU^i35?6eZ!Cg3 zziZn#O(3toe|x|Ii>GE$!BNY&VH@09!>NPV$2=xze-nVALnA$N+p`rti4hz_@3@h2q~wW7ZK!b6bTB(QYkH7Y+8F8gOK}aW`4_wHN850H5k%?3EFk-F~=$Tpn^`$o0bcXLE(MVez~n`CxJ z!?dlEE`-q7wgehFw?|HC)50#A|EG z1aUGEeQ+bqH_J47mQU1di-vi680V^!?HXa6fQw)$XNyM$7-_aAOEV?{9^vrI2M#L* zP%+Ik18PHAZj|S{#>e*IsFl4+Q-;joIEZ+2GF@JWbORNApaC<|C7x!FFsv6W)+pvAZpD zyMXCmAgjeD1nGYUMh_Ydko{ln4H)O$x=~|b8J+M9hN#zJ#a~68qnc`XBU*A&?Htjy zj)zkgu3`-=`J+Rq9#`m{p&z9XoO#J6Y&?dR$txSzS#L}CLtux(*`wTuCUo@Eghov5 zJ@h*54kho=nq`}{{u-zgt?3^~4vYzmUz*qs9#A^Gv`Jh4V3_z^o7&KrO!bc)7bI67n zNxcrWO!-rAJxdE2m`U{33#GMXKjHwRulCL8Jd?iVqWfj#cH?l#wz%#W#?q|sQc2^@ zb)^ymz14^;OwJqa_cGU}j``}OSH)+ZXa^K{<7yFR9WW@ZS|K;js_;4>Zd}TB((&L7 z%UDdqPSZkKMel}pO?m@vzp_v=6<&wlxkbWV~}*#iI8KNV^|CvzRK zTb0(w66Zc)ASoCx!}Yqjy(R8%XAPRXXlYA^IgJvGfb(l&Qb9$t9)`{l!4O!rFoXq74WE%i7=BYVt zD45E}b8`*xrxG)O#z2`B;M!p$8^XZJ*vuHK3FeL@W9}b!2JDbA)_BsZH%Q0h%cuLM z!m_z}T96i06t8$of7Imp6F#~>!05^GcTQW1rAk_ApQo2kSi&Iqa5h@U{$ss#vO)>- zCOD!#=*=oE4ysB#v9^1q4E+5TClr^Hg8$NZ{m=dXDido@^Zur*{ukc-{{MVxV?u2# zjlF;S5Yx)vI}J2V4XdABYxS)~8@xYm9hbFw*0EuYpO+6bnG*f+?tjzCY(u%8W5$*0 z7lb^%#gHrx6QV zmrYE7V3kpqk@u!kxNr(0BQi-sL(}iz)i(MLpaT50UtP$2H=f(GIhIW{FF3SIwf-4^ zbr}#_|8ion5|}5W&&nx2MsD|UfWUyK(AP#Cs&+3DP^LK6>3)=Ulhp;g-`V&#%Ek8W z3x&yV`OG*UZ!?z=z3N7HYP052CtY>u#kqpf*m=43c!ZGTXB^!O(f%UZ=iAp5YMj(d zjG4KEjyK`Ejn29NcAWtfHjy{1q#d9k4Bi@@9p~@O2ghYacYruDr85I-eXF^BTGEKi z00G#iGawWbt*mwEYecmeSTG^ZLBZKtR7wX@v>O1O&bhQT+0z4b!c$yI|O$I^P2xa^k;PG&g~2v z{xPi{t%fzrzE$fOeKF3Pk8}8-ZP`}ZmEPPqx+eq~qp!tu&eWxm4mZgw*ed{*PJjep z@Om?s2WA@2Z;-e1#8oef8ilt0;&snQ(M7Vs}*OvP(4n<+LV7vHW#RV&wp44;Yy-1lz1QlxjmVt-QEs)t0)NQjsZN zFpDC`%wB8Vn8!$`0RtTW<9V9uxJNsU=!Fn~aZmK}ARJv=UOhe~G)?!rA-9_OUwWP~ zb*z6`H0Kc0X}@o1(Qryp$K8*r6+PSRL+vsj_r&aeSZH29+q66PvywmjH!a#`IC|cw zN4N-&f9junCUp`j#zhcoEvccg1DsH#H{R_7`c!np4h+U1oYLru)hcR8Z6bs{74IAGo&#)w<@bYjhcN4*XJVrbs`BiZtrKQsYYt)6P?NA zwf|yH$hoG5{EBYIA{zVcb*i@=RxHOOV=0${-Y9eF-os|wWFFx zd9Ch`lwHv4WNgEo3hi~lXm4{7N$zD}q+K;2&i|?&k6APf%qZ~>lqQCx)MkJ1wF)N_^6%8cHf02%v ztl~N0{)8l5Ebgd{HF?WH9qLcDcNzBnr)wJ1h%iRi}&;)oL}s9COG>Ch2G!NDWJVJ614Gyn>mpz zU|nhhO-t>nMOE8kvi7>^2XH-cG|hO}Cr1bRL#yRY`NbcQ`d@u&pk3AK8_ht+flJth zJ)n`Hy@um~TfgZYj0Wwm&m+gRhkF0RngwE#LwXCm4a7mjauLh`lDm-+(z0^ zPLmxwunfbS4%dc8Wm+RWcXX@|W8d_sq8D{Eq-kmF)icgq2UjJ~dmdR+`%LVmHt6|& zy>yT6rc*7LDW0iLJ5t>nbV8C2BOuq*$}?A@{#F zTOkF1nXNVV-an2>K3pc}_not!o4I|8YTnjz)8O+{{+nhCP^nRn zVV_n@lUODE$k<4d7~(ki}VP4YVKy*DyaI*}MSk z-lp$$eTm;r(PP6%kJwCyk?eH3vf<&6VI9WU!q)9FgRfz@r!Vdvy4M~4O-bi|^9R=+DX7GCjLY@z-E_KB+vW^#84dbZvzzL2TgUni zY?B|spy?raKaM!#D7W?wH!E5Uhqb0>p(Ogs)uKS1J~qLK=4wQ0TnthMI^=cgYB{mM3ezb@%F&jBsZZTTkyi3o?tB#rXZtdQL7{P%jqJF0BI% z?#Gs_q@}HVS;9>Hh%RMr9AdD=H|5*o4fHyOOzth3kWS-xg#T0v`j(`}$fC3BskM$NLLS!+|g)h8Xc zsb$w0LBFqI5e1pZ*>XiaPz@>_?waYGjeWdTK0kfU%WkOgfksQ+&;0$=l*nOlE-q@m z4j54=0LGuR*%e}MGcTkCIojVj5a2CKo$21PUV?7AYHp$~waYBsA8SX#$3%o{FKwl3 zm9|OfguYBk)s@;ssj81HP{=5jFuDy-#!#g=SHk- zpLTU603O@W4<4^g^zmRqw{_Z67d$XuJ2QA-hC?Y`@4~NVA|>1O)D8OHZ|1_3Cx_@q z9C;i@bDKw;Alt;|*Pqsynnazm#di{5uClLOB7ZQFHk1mAEyOVoLZRg`U7~LbPFGu8wW|~mefn&Mpnb=2wjwfoeD6himEXSqmag`9B#l_%zd`pCiu~+ys&93ga4ZLZmgrhUxy}jb&Z`_N9k$GC zQ(jw4Y+_4WVY61((`Z29IfBx}9z^n6A=IaWEa7K$zM38dSvO>BLlX)mb8J1ewwTVg zjj&}|tKtjk&+4H%l2W0D;{a>0dr9co8=s!eNC$iLtghSaM;k)lw=UJ~q8a%@n=&0t z8bl`~n$k*pIc9a5egh3Ow?GIkg?{kJ>bHJT&GoSf^-Wz&xEW{08*KGYNfvr6OurYa zM6#OOAYF*kiO?9hH@Bds*EQv{XHE3B7rkVSLZ(qhPtXHhjIz>6O*J~Hty9f12~o-I zY8frvCzVPCo6s}aoPC|AMp581r2tFbT|`Hl8qgod0t}eG3Dq<*8i>|P82uL!HH

NhKTB6ZQzV#v?pewF#vj^O8YqH`^|~f- zR>XfZU7gDpvbEI7E}itls%4S(mW=y%vuJ;#)OE6Ew$K6_?WD`Dw%T^J2&|n+Jyh93 z1Y3Xh;g+zME`*URk&$E=f=Z4WNvBG;(o}I{5VrS&+FP7bGMPM}c{3gn$4NeVn3C1Es(I05y|vS+G>u62D$IumTWEG&>b8ppnUSfHsf!%OEdA0zZbAng-BNOMo82N6BY<0r%Yo$BJp=r5?Xc z={F#u4+_*nH)n@JxQLOHmOnp-mywh*rG*-;zexuyfi_Kde`*TPw)1 zQG@AS+Jujf4y>=6E%mCnslUI`VQfcpk6wPf^@1$v3kp7axekQ4Lq#qet78fPgvDeVud;eJR@4q+B3! zdy*NiA!W_gQ>w3vy<2tlL2EeVMaK?nagQ|Ef7M?ryWr5+5-n;Upf~Ix^rTNs-mu!4 zK7w`Qj0}+1&}=0O{q8zITa5+{bfngy4tmg_Ip_S>eS4$^2H9Cu#$e4U$n-&E->J6+ zN(XX$UL&r<0ND9AvCW;-_>?%j={+l$vOq60urxnSOL{Q$sTgA8-*)y@Dl|E?3In_FXLPv~ts zpsvJgo&|SsEt~Mr zY8$li1|8}dZFq2YDgQY5NC(-rHJbB_JJ9n3dAMGIF$nEs*fT+MG3(l$h+6#w{wPOV zyfh=X3jLSOQLa_Bm^P;rB{jXb*604RaEsdMb2qR0!L0w0?v&^VnYq63fC-Z{+rt)9 zxwVq!gy@?e_Fsp#ozWd1>4OoF!3*CmqmC&v=s?{R6-xwQUr z?ocAbo)kj84XNi&^BP@nV+xyaQzZ2mc=QYOqEwMc#=WSI=>^>r^jkSS@0`Jt+y&_nSfTe9H{RW9^P1ppBz=>RB6-!9?wtKE>k`8XU0F=bhuMGWS?KZKGbN}_->3^=6cZDN95@b5!0VxQyO ziUXR^GN?27XCeIHYetoUu-wSfVA-iH7b?mIA~R6bzHrT5*8F3HQq>F~v&m$1BG}?izbJEZr8f>5EXB$>u z>*{#)2+*;f@O`otqHkN9U`!@8>X^m)AAp`!4*fdK z=Ij-GTS{to9|Y|>Z=XUQcXfC=gTi==f*Kvx#Yn2yuMH)ThEW97>?>VN44qikRx>Jv zrfO&}`1EHhy&b4U9?`IRk0us^P3vh7ZResjHm>HHK~V%Pez_1mYFO+s9>yHBzhHWq zRBXX2t?4`v30McKo||b;O}D}smhQw1<*IeUrd{w()5j9F`0iWXE0n9EOGTk5{_(qh zw`a~1jb_)Y+EvyUdlSdb+!hd88ZZDS23S_JDNL5yOnuBs*O>e@%4hq*>$8?=Wz$fu z4!L^MkNlLe{k-XpLGxA*K6ctmYa27UpBONsYrssuaynXA!>al=))WiQrY*FrS*>)o ziJNpG0dKdeWvM#))6>nAWlhkDbRR7N8^UOPH@*!}?{N+331x}gP02OIPBhbJQ>J&u zuOi|{;>wX!jNIrneFfm486__92;hB7BzqS|yFfnN!C#j42=if`cWBGX$i;7i*?1ll z1@ap9habrR=Dfu)gROY{CwHsH&euo^S_p&x(t>pDD}Y*zXwnnuS%x5P(&Hfg=-vXg zcx3NXiK+hM+s(oTY;XEEAp*nr)Q1Xi@Gh)8;4-jC=hPlLXbvZ)HY?TCLQh(OZcmPt zkB(c~CKm3BvD~CfP3$Yh={unNZFIIDnlUA=3I9J@wuwVmg6Cg-p(8?#3_cF>Qs{-^ z4dbqO6ew(?=RGvMQnClEp~2$aW>pPeIZnq+QLz23w6}*&q#>k_wYN+Y4vRGI75ir8_yqG4KnE-rGaI9Me~|!jd6{!HZrVW-*{HLOuzdV#8hk?SOt?m)vZOTB6f^_K?|{F z#p5`C^@;6TmgH>5{IO=NZ9f~?ivu~5Oh9sfuT}&JH#SX=(EsOX>Fnan>W{iQny3ku z3_c0WiuwB4q)1B?mRpa@2;>V0x>mb#WAu__N8PecG&)@`H4g1pYEHXO2596nZHerK ziW;!E>y2zUWixxD$?}9YnGke!tcM-0brO3vGLG!iTj@%No>4UTqFCgJJ^(NbEtXja zRAr2PWy>?pJzq@Kkh+z-!(=?S)^UxrTX!pJsfEV%c7ROuRBN5DqaFc6j%^>;OHXS@ z`_(h$5MfwtNb+wcD}CouHpo7xYy@EF-5ZB_<<)fzF3pbcF$-IeldF+cl65zX)m%5a zrGpAzv3r<46g%L#3H)Ojd7m?% zSi+VvBbst9>1M5w;Vmq%YMjLvW{{(Seh$3hkAyP%?%1k_HnDRt^4i{Dqd;?=+|}HP z5OF-TTZcLNEunO_-h{P=Q76pu4a#n+A+eb?jP3#N^t=)7{#wUew$=znBcsE>f>!-C zvJUvo^`P5w-FM8@V;_y8#$a0>R`8^azR(K5sa>&#MVp7gz?de5ew{*lhnPlW-}Rn<{n^Ln^Sa+%x^I8-gk7hu-kVDz{ea|C>?(I)8N1GpD4e ztZoHT2e#MV4qDeWv0ww8N$F7owc1SRB@Hd=T&MstPqAI2NF%lZE8I)x3Zs1sJLpM4 z6k?z5S1`~-o~CPcxMKxU2NFWDQx8eW?#4&o75uF@itXXwvqNEvl1TjD9;p>YvIbfxu zU5bFOi#6QBFq>J%CA+c}YM^T`3OB7mko_cz3LTa{vZ94P3~b9;bZCqGX*pi{13-KI z8|=VGZs8~=We_~PCQ!Ki77*_S~nnBp_>DrQ4u7u_kHA23kQ9bZ3(@2Ta zxgE5}#P9q=+l-3R>^T?d5`JxQ6 zoXV`08ayB}BS%v!)zfxQn^=}V*SaG9Z$vR&57*`%9ke)7x633|YD7<3qsZ-ZiQ444 zZDg*m420eJ`nM&v)H_ovyfwUzBO%48%w&?75?hlINJ`gfooZ!K2x5|?kfq-S+QY*M z_3^8hBWv^s)0Z4q?TLrIBlJgTSy(nor{dfR@(uI6jc0F#X;K>N8lvB57F?0kR?UbG zGIcihtx*6QHru0M7oeLs%(87@fRe4LfYqCAX04^(={ee6KOG=4Jo#5WFQvsrG_!VP zzFrbpRLj6XwMoF`?1^AfLA>U3xLaqCW;M*R)A9oPt*#bVzz8q^fZAJcb-jne3XTC04M6@`P zKww=j{Y=A-f}`%Ed4u%aN9!$PTA8M|?2N^m9hgTDvIX|Ojv&%Aj@6%#JlROwlhO$< zuXm#?HBlS8(Kv6gLj$cY$dey5swH!|jp>_8Ffnu>k2$y)SFTqMGw%b zY)xz(+qxRz<1=H+p>H7reUl?%IpZPU>UpqURMN8Gaur&2>(t&2zc55QBJ)`3 zOy;TDT?4hg2YtWYQ?j+Ou(p_*RVkR|1-hB0L~_btPQ%rhnwm;c|D8~4y){qL!DJoG zcB;`kx>GWy{C_mkti{)Qax97iKhvl(rGY$aGd6l3Ox@Q6Gn{j70x=o#Y{o2K2CO%{*GUZQIbU;*xBYd*L1dS4+eB#kr={VdGk zA=MJ!3Y+P0w0Zw{$3!%3j%M~{jK@gZk4&iYcVib1k1X98z%X->nw?4b0t0aKd6?ED zWawi*lC|scYNLb4E##CV%|p%NOwhOox>CT7qs@us5bhPa?p>5e#;S}bn8FnOxH?kT@!Otj5CTH zt`3g*Nu8F9hF;QeTRfML%uKgkQusljCViRCF_U0 zC@`ju?JsLaI#Xo+ujx@!A3Szz{;qY~oC43!`V=f*sF1p^zIQVF)9k@p|C@OMTVVHS zBw@F9#zyKu(>PuBY*Jpca!Np?--8SQyR+=jw_*+c^flVmzD2_paG*nGN>`4$mkc1q zgsVn0>kzbi5?E0OjLfsCI@AlouBq(MP4{}}epQ~m4vF3}+~1n!(JIdl&%Z=hOLf!y zOnl|cNM-`=zI{M$$#4UQ^kG-Ljy?@C<>e$Pr)p|Xs6RY47~7#DPAK+J<05}UMbgmQ zvj)2J#wSwJv6Q>jIO2{i0~l^ZbGm9;*L8twkD5kkT!>L9eU~8U6ZhR(7az?Xzh!Wc z7L~U|09M1`yHQw60sOc%Ro9#w?dGwAPBn!c;Q)AIYJ|<=0~B9 zw$KH3uS5j)2;dMy!uk}`_Od!&$uC30lVU>~h38LnEMix$BIG;G&}5xp>u#pI8O^mY z`-v3__s*+VolWFL*0LXR{?Of0dYx61;f83vQ4oG3y^ki9)%uS4;%ir|&^neN@{RTe zRL-(zzQZ&uPfM9cb|pP6ky(MJ1bqhRX+urU)$tnOW#EnK*Qj9}f8`FO4^%!a=}UVu zT`3M<-j&g`Hr~B^cIQw)50f>wO1oZ#^|clc=q1az3Q|E{GIuo*Y1X} z^c=q*V#VuUqS)(NX|1<*`>=@pk<9;#ah`$MJpe!V0@OIsuY93W{pdSRVQDXOw~qSR z)`aCA#Hwy@+Za1@Rcn15v>}Vn(vPc8#ZLWnj6*Dq&rH?3#LE33{y5(L8Dn(C8>v}X zHnXX2MRYRuz~yy5(1|T^q88Vo@JY|gcZcoU>HBpuEQfxA8rq-vt`8*qnOYuu*bqUY z!61e5PVHO#qVZO)x^n5*|gHr z7@bUP5KM`3wFr*#9$~I1(j3$oO*_-fDaR)RXTtt%rU0c22r5 zQ2!dm)Gs3G20Z}0<}3N+(| zUjqWPuUT$lV~vR@`yZoK4N=^xR|7qYmw-;g5vo}x&1!0)tzi<0b5EAC?Ee4g{=c@n zr=AULsy$ud%@p_g0XknaR>v%>dU%gkk)*>lDu54KK%7rsKb# zLc0?XWrM~B)(9+}gOu!X?AHo;qTTny#3Y)E#|DHYTcAeg^;ZO*X{T?y1eU+XsJr;8 z*M)y2$I?_@n|c@5I2=#^GDKHpbUq2j95*!WY7dCD68XMbwS$iLV5UnM=Mryzr)iaR zAPsQo@4DTvSa)&(n<2;>>)_oM8COvA^t%aFH|AO#(;(0SUg}ohARQ{BXBFF*YtF51 zK*t<3{SA6?v@G3EoixE$`;x2cO3eyTcC1!ctD{%ebR93%M9Y$q{FBsh(i62kq}Z9P zoh6(*SVf1!yccXoW@fOBXsRdmTVo+vB^LBw-Z~4*zb|!QXN}FT4^H;b=nqqt`<#!Q z(jT7M-Wvi8@4(DXc5_ZLeD7vd{29`?oh|gs{?R&YLG($(U1akePH40KaTw%UtPMK! zms4SPf{H2i`{8TfAq*e9^`j6YueZQZAE<4s=h#7t4P7v#7@iatt)wp_7MWPXw{e_v ze1R;Ds_Lr={h~C2I+tDiYORiSDPyAjfe?q{V%;}-1FdVXH&JZ>3N7Q(xNcJo?#c#u zej6&I>c(2@eRKWrXQ7Al!u2huh)#wQi|o*0DiXL^FWwMabgMD4@Bv(K#Bikt1iWFy zyW{B$7;L1JUPGLy!bKp*cG9{U_JnX3tTfgm3K`neGoNKqCVV#!dfv&&E6cez$%XA= z!}#6+jXA9SJ}ug2W_bEChe;(VJI3>nB+r=t6pg?(pUBmv!o2^Q#zoKzFkAO~*Uao} zjm*S3X$yR&l{>}##- z^od5hlx`l-98xSDn8pC)Np$gV3okC{rEO?;Cbt_}Z6U*{9E*HjU8DLMIUIYmu8X$j z&w(1#yV*)X7Mf%<88_Ng-h@~iywTSOeEPKxXOp(ukq56CM-W?WyYcZ6npfYN2fyh- z6&69mQwrn8odxemfCG%T+7YYu2$H+QDq?2eV%^Q1o=3Gq^+qE;O}JMMeG9_ z0$>m=Zlx8;)vXKEEz8LdCSoS;VTE?k3#ScVU)G}jdS?#_hX?k}-wh-%qk{YmQ00DF z6_M;?(%VTJss&jT?^OVqUhZBW85*L03TsarPlREjHV{Na#qzeRPBg3Q+br3EJ5{3$ zbzK|iPTO())(u^mqvhRdfC8sCHS1_CkQUALuOCk7Oe}_axp7x|N2dxv(KauEvsP(uLd~ zO)77I4eu+b!x{Ot&X&|ZO!VE75zTdr4EQ{qC(Tr|6O%PBAGY9PjXH)}{b1H;UjT_; zmGX)QF^PHgv?sSKGP+9#Z6Nlw#w0^=-Kjr_)rtkD z+WTmgryYRG{Q3^@aV|T}Kd_${vvsT(qP+_MxAsw?=%^wE9KyJQpH|O_*_B5Uw%NS zP8#{MGoYOTx-z&Z!`{oEnHv_)SPRo|-f!%n!$}S0MzGwV7)uPyJRSE=qG(I2XxYu5 zWr+GMZR9?xj4PB{$lT@`u}8L^H||-dh(n8HT>)kU-*^C3y+L<%LZ4?CFMU zVYkBYZx{m))f^DrG?sLl`}U~BGULS>*0qfZ-xznNlbSfuTe8NW`uej;87oE@HOS}) za@SqQd~<`NB177v`XBi-QmayuA?+4c%)J*Kvo*yA!c0JA)+ZJ%jSU>imkac<3L*Wn zUW074JV2i#G`+Q6=GT*)L7huit=zaSd5R@;zOPe?JKD9Yv@6hiM={vH_lgTJ#fFge zos45SV8A~-&50m*r?9|3+)GEpsxuQxng?PJR*^RgeT?hT8y#p^I1k%40Zx%a*@j|5 zqx^qxMssHa_W!Uov-(y!LDzv&GpnaJK#=Qm0lhFqSvKf{d3~Gy?F$-ZZDY9#^kmfY zOamx`COTviV^oy9#Qsu9q^Xf^5!Sr?rlz&5iNV^8F&muC#U@*>Eo8fCKM8*w`RGlf z1?76%6c7&Ik|pM@CmoRVbG0GarGV6$KuJ2&ptu_(-C9()Z3b#$gi-1D3BjC+6wKGv z4jSyFeG!108wD!6tc&VrdBgb4)*ta!M&H6S%xHT_BSN@NxbRV5YxB=z&CcnqorAhT zz^f(gS$1qd-iA}lN{_5nbGdV4q57dY9TIsG`otkTO6Cc1d z?I_R2OJ6fk?GFvq+os6M^^nrIgR2!$*}0)by62+;09pJRUmrny+yR5KsV4(JAlIWg zle(|tRW*W2ZZ9Y`Ie$uwo?GcpS`ogDi2{Huzh61M^|58xV_m(Cl7_!@xu3o_)>GG3 zy+9fI-mhA$&h*zJ;2FszMB+YC=TR4(dsMe3ubrgBH8M2K)uekY%5DOg&56_K5o2F~6tL{}dV4jvSp@~fECW?4UcZ`7Vg^?L& zxZNiM@j64@Xs+Fp*3Kr{5U-u|XWP=goaU7*gpIoDM&(wEI;Qp5B_E|sDcOd|c)Pn6 zl`vBG=Aea~a2*?@v31am#jyGhSiHLd0{fFWOxEfy$j-aQScxgk$eG`%4>>hi-8>3q zL8j!Zn=Jpb@x+5FL7HFzJme!&tw^j8r}OpAedsc%wQXEAJD7^jI&oCukeydEKelBo zd?SaDDuOn~p7?Wjx;#c2Zoq{TF>Q1-PG9#_^|wHpUK=nWfA>m6sMl+?;S8sR`azUe zM!Vq|2Cm`f%Baqe;)ukdQl+th!&%n~EXc|f9gZ;TSt9}@qyf3!IYs-ElOLn)MJYtI z?%q)^U~B^x_^3^KMct$_&e5%_Ubn-%9AShhrL`idw0@@RPJLq&GX_Okd}zn%)sJdbJG#KB-i^9V6Upt^H-$i}X)P z@(b3Kau}Hx4QxB~(q9}d6-MOV=F7yReAL5WRP2xz*+S-x_>h;B5&ekFBUPNtQarOM zH2RBI&(L2zYq#p!%W$rs>eO%x%tkEFMx|*SWjb3;G~YYfyjMdFj*Ca-j|G_PXAz%f z8d^{n-3w~}f42Bdqf9Vh4{=(@?Dg44e|tmheW8u=M^;BJ*JA|-1LXAo^1u2$NU>T{ z8OOUeS0B3~Q#fnQbSs4pyw=|)to4LBI)4_Uati4)c7-2$Cy^MIsyA7>T{kKosT}Rv zP+N+#@+;PaBKOjpXp46|#6J>gM~r_rjN>WGIPB=i?%EZG%cleoWtrbs*V^GNo*Iog zZ|ij_MEO(sKIRQdnVIQ*Zy(<*U8$h)P4vjQA&feR^eu}9kL|R9G`tc=c9llDyyc`; zm2cY7R7)!s(+scbGy&&q8g?_13{~J+v!~G=9XY`s<#9V`IR86M1Hq z4tAzi1hwBy2O+IU(6*?MqD~oQ`Q$k4@4EmE^+ykBHP4MtZ*tBeyfva@XfYk_ri0b8 zLix~Ejw!oAp0_bfY&PrIQA|%gVE(1LFLNPRJq8UMu3 zvzs-lj!z>k7ZZ5@XicoyT`xfU;LoBuK*}Z53F_KYulky39nE7qmL+}6Td$)qUL$n2 z7oVM)Z9v;z(V3XaynP6JksbOY2+6+W=VxWWG)=0~9~(EeALQ@gFh^s&+M!rhv+a|a@?Ohkqb8V7tc`WaZr*DF=1!hyul3ffbW(~|Qvm#&)7 zO#k%M(hha&^wv5Wg?M~8%VE>vrn-oiNU2CVheF-RjHs+GQI^uL!hf6X06$RL` z6pGeI7k6E$+_aD88!6x8s6sdMYoL{_j1C_Zxzhccoh76k&PR8c<#qI`RxHn@HZ5Df z)v?mH2ISMX<$-7R2-}rpEpTzQXUUWFDpJo2A_iMMF#8|M_Z@&N-EFM}v>30XxyfF7 z*0P$>8+$X;z7ieH5Z~Oh9K2o1XE1ZHkt*! zajg`>VKZiRdv|T`j3O;AsM~HSSb#&})lKy-Kgr)ZQ(e=_X>5_wUTgtgy1OSB_0(&E zuz!C*F?Mk2o%CCo1lV>AMLcL4-M;{*AJ+{9V8XjX;@mKaH%l7LnQ9SRy{D&qq|N}8 ztJ{77pwX~`5vdxN51=zD|1Gp8&bYz;C%A>?_*da_ODBxpG+WCx(;EjEIs+)gL*CfF z6(w}H9hqBw;RHCtu^deN>Cpnmx2;41`gTnZ(?~@;%FBn^70$D4VYJj5LBxZ^FEDzk zJjvmnGJ;;@30fTn1Y?aF38^J>-Ldv>42;9YP}L}-xTvqa!z=UrFugQV1m0K7z{7n{ z(7uqM>^fjUN3iY-^l;|c?UDZB8s&|s%ma+3tyYr)3h9{0^zQz~TSc`n1 zsnulgomJz4HNq(fKoM)Bq4*0;kJOcH9X7YD$mahetUH8KywsD5dYG>oGIJt-bO6B@ zKl-F`IU4Irs)N0Tw2L7y{7VmFT`AaJ-QP(1`3p56ebWq2L$VEK*h-V2r>msRs)<8t(anDWwW5SeGSk|E^@_PMNrCm|K4iM2O zSh&syCO0!uUyjCwX7Y-y(BRLghI&9WbggOgYDl>}eYDxYpZhyFo8LayE-nNMMpM>tvRyN=$vl5^U_PM zWU8iT_JA_;Y)$Q|K=bLwPR+va7xH&YjSugoyYy-ELGR~dL)M`9%QT}|C6t^_her_T zul5*#Zko^+aeh)ORFi{rJ3omxu;uSwgYtFNVQOlJEO7FniK~z0RdX9 zkG+C20!fWZ^=j1!$b}T-`?HSZV{+qg5Qj^`tc`rk=+|^0#m2z1T@enI7y4zQfybcs zD@0;G@7~?M01-{ImQg10E^a}jo!LO@3#@da7t%OhdwK_w*<9DDj2|9NmmE?4hkcmj z^@t)G-vb9f4aF-2Sj0^&PUq#LjqcKS4N|w)ZBmmc+puK5i}cxzL4WFCV&9-nmPyTO zU<>}BG=vuL%;&Cd4k}nJ$;?VqTsoO)YS(h4Ze22v7|!s~yKt0SxQ30RqN>>T2PmT{ zG%^|z-q6^#uy_>cn$E2Fw2N*~W{ft6vR2A1+wtZX-TD+~jKhPP7`XF<8u-NBMp@Ji z-N~m%l6w^rqt^-j*^%rOqW{oOx3n^TCx(M?RTv1?0U@E2j#ubZ~ej7E^= zx=DF>YGi*5g<{`mLAnHWpM&PFH*Jq~8mW`r5sH6|`_aFOxF8`D^rAOpI;1;|>vRm$ zJt`XM*Je)s#9fkrLqL5v;7Iy30{0a4T+RhWI0V?Oy$zc->CMdd@Z2^N?R841PjZMe zI9<*i3)rG@c1&YLJ597gJ2ktS31Q@qK}CTQK_72JpQTlcrqr$6n0E0k;^{D74A!5~ zu~b+$p)j`Ro34@LzDq}=c*XTH+U&_EACyN^INj_MsnJ$09-vhaQN{wqXWEt;$`EqP zA`LgHk#=|j0%#;`w@x7U8!K1TqB4#SWJp|g=B!{CjG2Z^9ybE`D}4-T_~Cr@NzchN zgAH>TWIWX2=ti#_>EEVCwac>hQ{@%?@hg{x zb?uOj586on!lBOGz&|X^hVZs;m}WNVfagukj(u8#Kd7PcA)M2Je+Ci&h zHk#hR!JgIp6GLbScypjWhP8(v=QY29iJzueBjqnj`jkK)wsUZ8v|799Nwy?<^N67@ z@rtF|8b(HUDZ;z?1)1hIx>MR2w17IJjnssj{P0*7X%EJ2%JNctKm;bAwKYqNNbNg_ zRII{?UMPTpi9!6n^z-Vg;Z>XR(f4Jm8u=X8^Py&RiL|sT2leiB#>q^~>Qq>VvQ5Ik zZVA>i<1*`Rhq8TtIlSwR%E~;4}6un@-l@Y2Mqv|TP9`fr_%DMMs5m|IT} zREBC{81((y&$Og}SV0TEoTh7T{q(tdbtVpp8pYJZbjYDT$r|=0yO@R&Q+?Drueh3= zm-Kh&mL5mNwFRPdv$7sGiPo1kknhJJ-tZLaj44L_qF1Cl*#8iDdH`Cvr-d#z(8o~B z$}N8OC-Yy?2|?Wy4El?C}w~`PBU0p z(41u(<%Dyi81MIHq_F6ndj);*j88|9KXwg;pz9N3R~n2w+8`Ac_q=+OTyC zPbe6u_bGr_q-bkp&4YKeAL7}s7_F~J!8*+k39FTEj|S0gn#r8P{yfk>Ewnq{FAnQF zg~JioM3U4wpaS!(xIG7qbVH5|XR(~m{g39({jcZyjsHnX5h^K)N`zE8pyUuKr%Iuc zQjtjI9OaPO=CA|iY>smr=4_60J|7>^r|%!|-EK4Te!pJN z=XHKw&+B?zNacDa3+XAv;_dZIG=$x*=dF_CwJ=K?TuEZS(1+9*O{@)IQy8xVK=`5u z2PrFk!EDm%7SQ%ZvAK1buji#O;w{@XQ$OitW4d=Q+NHJvaHPkjrVflrMBZUwZ00+Lf$1&G{gmT`uDMIeHYhPT&dZ_;qqpbJzqHUP3{!HVwttwRNUpHL; z)oz|S5{LFp9Ubu2zx5EiKdVGn=}q_n`iqWcK#V`i#pr&=3=GH`c>FXegvO{_wVU!B5v}LxQ#Zh~Y40DrO;0x6chGeqGFP!+sL)z?`@uNuy4zSXB?CCPLXg`%%Bf4o? zF`L*>w=1lm<-jVw1tR`QS(yFa9#gh7wSFl!Z?gAcpCXu(G5{9WkX%{07C zG{UCU!&9~2*-@W&Y|*}73a3n}+_gRjJ=)P$2a4Nw#;sptEVKRytc%3X$Q_~I%3Gx% zD*LGXIjGw`i20USj5Kxpr<$;lUt?|XUOSQLowRd}!A~7F!27>?55bgt1(v82M&q-J zfprG4atc~Nwxx@1bThv41#+_%cE6<59h4An8jnXn_=UBa>qLx8CV|h*x;*bQZ!L}1 zy2#KjObch64oCNib4U`cu1ahV@Rq-TSLm56*&-mJED0d zoe1E~-%^F<2hpC_9OX#p_A?4Mv%JQq2bC?*4Zy1+!3QSPSy6FRJ>73Kve-`ZDbq0M zWNSAXssRPNZ(Xp5toOVX!%{O^vcL} zwRje`doVxVuOmeUap5ofVGw5;(s!(F`6gaW?4?Rhk8BaAg<8}bEWJ+A&WJsQ?`;aB zGArp!H_aG}W7m(PD5+_$1?5>M(`nqbqc5xY0i}83Q_FxJUm3_ww{|C}EU!nk^s!!? z#+hl=XDQTTAO^rR$@)Hok`zlT?Jd*23GQPpJKW+Q2#?QqPsL{aXNis31aOc^be};G z#%o`b4xk`pf&bp6omS_NMEf3?w;8pwEs)$7tYuoD17!K7Sxn6eV2Xj4(*p4al!`ac z*VT>#knj;(!K^v&Czy1 zAI|G!Zd-ko9uM1oCNle=ziOkj{|)U`OS3cZ2{}WWvV;?Zbt|H3b4@Pg)f)^D0Z%*` z*ICy(BIWd=9O{b=yr!KII_k;qj%$*2r~?U^pNJQpMnJ~Tw1GNeV#iBygfwSbp~FYn zz%mETi>PEgH?6_Z<>UL|J}>C5bx^B{Ov#|yk+3jVbCTQZtE!#Z*mB?U4V+SSGgmhT zd*NfA`||=5>;F@$29oeiiC$BtvY5S4gc4u3qvllkUtJ+!9J=n;u?z}5WyC;(Y`7Dw z@KJ8|iF#bWpmhze67-j>Awm2HU9AYBVI8I>(XPR|wjj)^t}kaE|dT z+8Ds*PW211W%Lo|ervv`KDNMbnL!2iZ0Rq3^lJ~@Xm}X|-7v?&eBmw=&a}l>cXGAZ zLmLM%(TsdRO}z=JlsB;|M4(#b=6&SIuVSoImnKgDVt`64ijc&PQ=oro0zlG`N5dLN|HG}BS z0euLgUA!@M>vM_KZR*rk#!Jd${@+^;E5*;5WqiPZu&EmT>rTG+TR+YeF+_2 znDJX{5ug+PxmuCq9Sg%(YjRrFFq$AoYIO%Qy^Imp#aiWI(ODA)#uw4n&fsfpt=pGI zrUfwMyl;Jwbr{h`(x^JujCVB4R3dVUcPRDOe`EDcTMK#(ETSUG?U!1Q@xksw6X{Q{ z-iYiwO>}HG#^jg`UfLtS5nWr9Xil}91XCn`)vB&rR(~FS9SghDR4hC*;+TLO9@o!7 zbprDm`P2nTCOa+EY-;yd_B%y?w?pTBh;J?i=tv(P^JTcs)Yht&RW;cTIo{sh*0v(! z{`$Lg)Dz#ax;r?kel5*+>l4&R`|&FH1k3(Lj2#8NRISW~9!Q(S??X&bI-2MEOU*^5(AReO6^nwqdq>i?kkA13;-lpeN~k(`2-B zKe5ESnq~b=H{Fy#*WB(};lx4>&dp(8yw8RJ{g6V_gJv6^?iR7*i{DvH^P?-z+n&f3`l0Yl=lVYdJcMS5MHE zRDIL4Z&~8g%QbX0Hlxr6b-V1WKl<~ddDLmsjTlQ87f4~8nztg1Phj}vVh&|}D{1$j z@@|^P)gOv~(5+PSywbHMkS748a(F1ld?Kqj*+u{KvSE^qljh{I7XeA{!_+!m7&sE0 zx`5E1&Er77Ad*toEZvS}6ndeVF!56_Z5#-n$Fc1_1%V_3PUno#&rPuZe{iVVsAEx( zY&GRg>z!ILW+*Ea6Mio@m;HU~P{Kfv3OVJps@K?<5X}qLiCEO?X^_b)KTJaB|BWli z(48>z$42?M!1pG9fgRJ0oI*R;{!EXO()vNer7eg0l@!5%lS!g1 zW)ozXTe-Z+U3^dbdIfE97|~cuK@PA%n5J_THG;#>Um=kuLeklUf&tE4;{;kDwX=X= z$!wsxL6wJKi_f;LsWY)!9}PGDbZ%jhfk9bOsdALNK2qIb9n%$oU2jj_&0|)&F(5QE zs~i$=0_x7L(@@8)t2#B-*VS}+Ad7l{!Ztwu)^Mm`{3bcO=y>F7qF&MDWDMf#G~K25 zKo`gGnFai}v%n{db(%xDnU8mpy?r@Ce=z!~{}@d)btkU|Yjs_w5#_tWzn5C+rWqg{ z=>CWdPLPAJyr%*wFg2K&ziDFS0HfbSdvnzkY}y6B;2Ynvl#MXq4dQ8`l_mNzRDYYP zp)c#{nXi#$PV${#rG=5Y*x6d+DH5)#<9W5l`sM^@cv z{a516G(3+U{iK>+BXB2wrTNsx!mrlMQuOf#zd2A38Qj7GPfArayIGRHb#7mVMtIE% zNA^vT+1rL-*>u2nY`4}M$2cvjrxodr+EK6F$aJW20hxSLQ*%4P~nDW{|Noq)5nYyrRsIq0CZ};DealzPUjY?IV%Cdy~y7k zcJ=i0t=y#+guiczDjjBKNSW}KQ0v<8Xlf;ncF+PqO7wkW$hJ?<>cpeJ7huwI$nZdC zdU3ah@-2!)1(r9cZtVX4&KUUR2>ivGZ1F2Rk9Ax;V*R1#Fdc^>^D3~?pN3)lm`IqO zK72?Gi219Y+ceeHXcEhv%=}Oa`Em1%OC#u7=EC3{&QDc_*y>Uz2zpxgM#ItT-YCIA zw)W|u-rAqQf~E$*WXgo5>Uvb~JmXfs$TM?i&gx^f-d1Dqjs7gP&@ImY&9$|8YoDa1 zx?2)r14XVt{7Y^Fee|VHx1?UjQ|A?oKCXCJOUImrgNLXZ07Rk^VG%+rl zJ@vLeZire!|DvFdHiPusKE`LPzE9M|4jsU}i`C{vJmxrZy3j(8hu150BOv^pGW==q zR3Ir-r_+Fn1f+MXWMkT@_h=rK07cn(PT1SCRvDHB^}EHwl{LXM-RbuL2s@klxp>oV zK8NYOZOT{a1e52aREM+gcnn1ejrFlgH65u_hS+@5m_p5R=+~}6wcc3IsS&JwD|mtx z25Kjg|CC00TRaq3HUGDm!Oe7?|h%`-Z zRu(!Qs!pQmiSDGc$0yw7pz8nWCyIo5)#s)TWj#W*6q&hV0DvFpU_#%ngJXQ`S>{*v zW(Lvk5bvSnU6RR%#tkwF&No_7&k}R92oApDx|A7HRQ4=QP7kGN=#?I{p%s^;&4Lizb%6Cnk{lKT{FORX+Db? zpnv-Cnw@RhXigdZ&{J>el=>5yS?t-`kG_ER4Z7zT-CWm_@?r>UZj%;;IOVEQVx+s*`3z1Z|IM164q0IJt@vPlbbkz*jO&scdjX&2l8@}G0nhq4}) zsM2~{=UxL7_`r31|8dmntCl)niHL^5FfKUA|6Q~bU2fA4?%)za)b$-&G9#xy3!+^j zff+f5`muob1+r98wr~*2eGMm z8<4xC%&Mgjf9}A#rYD7yF(&MYru)_#72HFsJfST279Oa`Us%e6wREseynnJz56IPc zi+l)pSF7{;j^V3o>RM}2HlXG4y>QAmbjs3{J#IG%(%VSu{X!F!{>~Wd?s^ki2xD$# zXp<{)aK0}6k?l3Noc51Jy{@s7B_V@h>o-93953ZZ6O0d&qf@6yEvOxlY6dUAYR<`N z)yU+QNd7Njy?Nuungg)Y7I>J+ruAhVYWBM$Py48MmgSJl8$AH*PVLX1*XmHKyz+YA zw1l@G^6KTGRb7IImk(>mBbnlzpP`S<>y9dAx>0kcG-zD+ditX}BJn%gdcioMnz*M6 zCd{i@5Afz|n(AJ)i&5w6W}e2^TIqumna(WoDpq=JACwSQNe3Fk&<_KP=N@jFimiVu@7q%%Wbky_W~Uiu4oNqw|J zLf1LyP|cLQVxTNx%?^ZVhF*KY+-HSVio?;(4T!z;(1$a=&#yEPn?S$w)Ej| znc=7_O}gkmdt`trVJ|)E1n(EvQ7NvYjDz22!kAC>E+@y-9ZahQsiw#2@8LZha`f0% z`#~E`g8Acgt6L}6wpyR++(9pj3cZ6hyXz=6X~smFs@Ly3l=$6(Vdar@gDv3DmDp9PcBd3 zv8PYfsvWe=+Zch?+TKyyn&^5=zI`dAddk1Q+cs`FFdQdy08ubij6XK2%2{NNqh0sZ zZuY@>mhN3S9nKkOZ9S}2YjRfhV7zM*y@-q@~Fs2rat}(=ObVsq%vFXZ6b7PIN1_=U9m1gjbHX3SkFykDTC=?(i)yMs)8xZgFd7yjS`J9w+F`r$b@q{jBd zPuwCC&Szg+SHh6)&LHPuhkgCNv|Q&p_Au>@cBUX5labCFjrAnWslO)FrYdT;W;fI< zuYT=ZaYsM0kNMF2oE^m$P-qFR>gezoO-su&>Pr2NJ8kF(TNj4#x{oBmNQ1Fa?H!GI zT>#f7S~t|eP|Y=0W#e=C;pf2ojZ8Pd?j*D(kKz9pny2P8qK`5?4D-B+1^oi=$Gby{ zhn=#c^lvEpdaAFwC1qA`hLJg9Ah?OBBhln7l*F+2g{?{LgeD+I52G}pKHI&b)AriN z{D!`8;I8ov!BO!(gOKn=?skpwQ6DiY;0v2=!HdKo(2^Am3a1VgVFK6Ygi18Bbpwp>0?kdnm_u(<%JsMt|R)+xzcDt@S3ftrrB?ybB%SkO4%x!*vPvBV!T?LytSkwZKb;D z&ilU9eJeA~Y9FkllEocT>;k^5aXcHH%XOx~Dw|tTf0i^U9z+3ICtk3cHEnJbz{?J~ z)n>8pFoX9x0pJKuUVlAp1@YfE^uTOB=DEM6PrGd z9F1*w(Kx0qHtPJC;lV`?eEMB#KO&*i`m%BV99`~KoYakJg8^FJk6Xnd9mo}TwPw2=snm81N63AJ{=%I&9YN!fFozy&(wL(8r-9b0lfVht}y_lyguun)lmDxbk&0* zsrI^Mt4|ZgFnKVWk9-UO!e2|X^<^?xw55pree|sj6uhssLm<*&?Dxbd#E||sgm?-WtPzq zTM8vnh|RiQND{CsoW2)=_xQU3)~@0pjK={pY;I@!^eEQx&`xi1>y=ciT8=+)Nb0NqV!29$6c0t#W}w% zR6h>)uY$yiH=SQnRTF%W%L$SE#ojWy?%t4;nQnX4t!ht*IFkpz#gsj8Gxdw+-sXHp zEsA0FePvB^8=xPn*|~KxP()8qT?$7vXFzp*JHP{}yXXUoeY!T0hSl+QjkD<7hr)Bf zK{_4Ekq6%H9}Y6}aNck)C|uXeb+aF#w;o{XQ!eY!1*zeeB#9=|(}ToC@hKi$J;T&)c&muqkT4#lX>AuZ`K+FJVuW%TRN zJ_ub{Z7!LpI$%u)(WBTT$`iA>{#-n^Zz8AA)Hq~mu|;r39X(AS!ilf~6;=!konX#! z$8>g}UeqX#b!Z(>++2@bOg>}^LxEZVt_|GdKo)_AIYOfg=#7%{A%eV)?4vC291az6i;>1Z``lZ>l$8 zcC|hfrVIuT@Qx-nOd5mHe#Pw}g%q*cp2nA+7Ql_43-y4Do@xf~ce`menHrPcITSL z2(hR=;e2x&n%hhR&F4j(^)VeC0Gb?H)UM&Ht-V4jHr28MsB*CYT#5$zg-PW)3x)yA zyMWw1Y>B+S;)0tabYa>g|TE0Y1;Mgu1z6c5xOVTg+A1ikU2=zk8aG0Rn(G&rv;{WCm5E4n9`a?cHj{|p;sY!lM`5qw$R8Rov7o>%DEtS6rY)w`T)SV;(tSk1Jf6Gc-Q{CY3V*viJWG8B&Ft0~yequr^Fp}a~#r)I?R)O^a_P-Zg-?fJ(kM2!N zIi~7RGfpk1G_h%OEZFxNXBwn_2-M*Yn4+ax*P96ZA~at)IGhQke&yQbpaE21@Y>Hx zU0a9qaoBW=^2`1Dn+4ND2^p(!L`0=tF|Lr$0C5N_+CgLgw+q};Sgul~IL zQ;Q*8^o^VL)X!)4hpXv83oG98$*@{=P515c3Osm^PI|a$y9-4W>AY(i;E8(eNt;a? zMhp0(?NXX)8T!4aHVkMIZ`ViLyO@3oGUORrm%{uxSUJ}Qs5@4@rKzUn;v=8))9^zXHx{kN+)wn7vINC1B zlcgPu*V;m|n~a$lR<37;j`r5ZYS65H&l=F?U?=Y~WcsRL-VO$K9A)R~VBCOiho?C7 zV1=t$#^pd(a}kcMY>Tg4Qx|g2AS%CtPubtklbr3G(4gM>G0lrW4?3HWevzB@6^2(R z8=>ceSoKW(n%&uUNSm%&WCF2S@cA0Nnx`XuYbIiqCc%$!_PUmgsGRH#(I4nR?f_FM z@>f)=N>-8O`rz`eYGpys+!pZZ9y4=X`P4K6%=>4tgHL-PViW5#Ac@<1o_-m6vO!Y1uhqvX0AN3@hy*~|r6Bqo_9245rsIT`GAv*3^W*PEa~OCPma97rq00xC- zR+Ax`T#W}#<0aq1uzz(m4<0T3UvlP|)^WORL|eRG*klLr*FO}N3DCd!S_h_zcb(yV zbt6ny?DQf(8Edl}gIBy4M!{(VZcj%h-ctYg1@k058P;1VrvZ{};xB=M}1pWmiIohT9sPvX>!Azb%XCl$RirL?8Q_eSo`~P6du7}|uZ+6Th0}G*7>4)lh zgEgyqzDqyuPyQD)-c3wS4PZSJ`1(oRYmFuO(tzw7Zsd_V0RA3m;g1XXQUAqu*R-Gt zI$vBV!Kby}H4R3tKEa%tyzVAF_Z;4){a{UZGE*$fUB;@gO+;Xoyw! z;=*>_VD-|XB%K;Xom8_*!&*9FImT;SX`Z9@wjW+(jhcVv*IaA5a%ry(7A9egrR+@- zsd+Bj`OqjOtc?Ule2}hI1(im>I%_Zy7|*nt82jCkaq z?!NAnZKm|Y_r0s6gO(nNI@=SGKGn|fR$qd|+!~(ltKp2}zmgHlO;_30RGhEiGYi}4 zKA_1JJL`3&K|K3nS%e-S&(N`@!@Ts&0u%R6|29K4e!;DsLzkDL_V-e>en`dW(6Qxo zAv&v0se7;suUc*jav9a3k0m9{I2xkuRk%w<0?!f&ys<&WHfUc=##n6FRF4u}32HY; zlPc+7yAVWs98+4&&Hc|LTpyZHCRbOIsHQH})j>m>>r+cFKXdiMwL6y>>fiq@G%r`c6?I+R02)I$-JzR_gRXSToUFNK6RP@hEMGXiRN)Rt2(ny4ZluUXx1L zs5f*0>t?&2J^I;o)GAxh`KqIsH}o+yD>aOczI0tE53Dc3`c+<2r0I)j?i4F(ZYnm_ z;0Wz@s+h0IE(PSQt5)x1&V%|ZtR@0_Rl8bC7$`kS4GiU|dAJLET#Q%x(zh~IkP)QS zn82w&M(b-w7do%GMaf#Z0VX~i@~G#|M&#znR26>>QXwRKd`6My6aeNYnXY_ zP7?yLr(nInnl(jzqI0w`UdO7|s)#T7yi;|Z;%WxlKMtL~XDV>6>)HUmFrBX~(d^n< z(H>T<&GhIHRY$Km_C-}Rrfve-`dtwM{1;1~P{*U38FGA>XVA-{nf>*k4x)Q*u>Dx} zdeD4UfAsKk=!s@r*ByWo!eXJ)B?B1x7>hJzxGQMzOkHi;qfb{}%_(?T+oHloaOzOm zCO!DX5-l^(k~G^7aAd3kpV`@KbaFv4q`G0};-ixWS?UzRu^a>NN%LyZ9!vgsWHQ$7 zC#)jVrjVMiOl9n~6fDsm7JD8KxU7d|H9ZaWVu~VGW3ygGdFAOsjLs&t(vc|RVq&x# zVkhgC$-w>;R-s1}(snVlR5wgT%4sdNM%?xkvh7ba$xjz5;I#K*6Q{OCgq}Bm)Ax0y zou-ZG>u(awPk8MH!1%k+?B5FZ?-}fiT&`))tDa;g#ps30FlG$7>UoOJH>;pER(`Qi z*t9cV=Jdt;jNkjj)5FT6Um+wUIfwz|3IN@|lLw(s^fR;p#f{1{*BHoS20SrgesD!| zyZr$b+A>Hl&9og7Y%TKQdNk6NDq2yyl-|Ks+H9Na-G_^lvP>}{FEFY?-}>eIx}&tK zn>B#dd-Z7m6NXB2g8b~uGF2s{nUQiSQ%sn%A2u%WstFnW7Y1$AyjCAqv3D>w-J&WeS zw=_T7OYh1AgfYaJiUTX%QtRNxNctF30mCFrd` zF6~`bkme+y6Vn}8&Gp3C#5yJD$Tj@;bXUN~L(SRcrrz}G>Z(VBdROXI4%CWXCfD+b z9^g&RA=2LlS8LoK!hO=!vSU(Io&VdUAFky^Xyq%I`RIRYc(ZqxuDa3BrhUtrW^m67 z-8QG}|8M_k$z4`Lr2z6UdG&JJ+30;Hs(YZjR>xxczd~#$M>v}N`;tOB8oj z7kYz}*xSKI84S~8$_ms6X`0?oNHvK$Sl40gL~E_Dq3QKq(u%Zyl#|vD59ic%thB`6 zKFzj`rf2H~V78;6{84zbsWp1?8y5uMi}-03_UL95;jOa{^f32An>t|Ju4suZ8f?aczt!f^dfq`_ zyR(@$eEN+|f)gbp?MQ@v6M-(?u*hvdGd@)*PCD(Q7fs4nG$Gx@fjUJSnons5{ZXcw zevYkLv4&fkZkznr+Q@vqxW`(;2BA?~o%LHY)0K4wef~+Gf?8ce3{ej{y8gCM8kw6UIk z9^~Bx8)K&TO=$`EJs>44{>MVF9J9*h`o*F1i$?V0+jGIc;g3s<{*Gjgi%g349!OhF zE7s?)xjAcH#g{X~TXV&6skJ^$DhvHrxWb4N)W`rbVhpdjxiW?N%P2eSR1&P4eJW|E zoi6$nXkRr1;x}h(#%q@QuPGne=2+Z5r7+GD7BUE=r&Qe|F{mz|_^vWCv#C>^&dqCC zYHEUoX7(jWI5UX0ohqGOZD7dOFeU)A*554Y&gS5q6Il;p+=(2H^P1XJ>#Mc~_RO@I za^I`z37S_Jm|YDiJzUvj0{!^biHR=FXGW`EFr&w!?G|`Ffyrx}k z+LtfGTF+54#m-M4UN3#IPge2uv@OQfpc)*8pRYPM4>C>=PxDaY`crp}Ik?kY^Q)QZ zqHhy%&}OiYruJ@Rt8F$s=`Wv=HT58&NT-Tz_!bx~oe;VCeWTgQHf@RI?rWqDjEo6j zscZG2ZGM#;m*zZtN}cjn<}Ty+aOV+PGr-*L&S&`~R=M<{@jVk;d`_>d@_pO$z0+A? z{B(^Dx*!b?wK7DnLYP3p>z->?CcnH-*0xYTK6Bi~)z?7F@4>E*tCAkl2zE@U(IqoY za~kz?(S>R$Rg=@}QVX2WEMHraHO(5Y{+z>_HrMEc(C?zy99aRi>W3;rt+cDB$#5^# z=RN%0^wbJ2shm8a?U`TEWWK%G!?@qAND%lJ_0k4>7SxAEHCoL#No}cE&ft(na?D9rlOy6#q(#vB)bMWo+ak&9Y3_QN=vYq6 z0blzGCw8gx*jidp->z3+13j$TA~hbBpV<)z*wK_VSO=k`ATvxf*veB1hs0}nRb5I# zl7DhZL9G9+gnHa8Xb^=4F4NqS_U-umH@ZGtw;Xx;m-KWLYDM|*6oQSHpoUv^tH=ky zdmK2}&P=u54I&XMCqb^O0i7!&4Bun7cLo8zI|v)`ss62sy#8$B`Ef0B@Y6Gq^5guv zIJ>l~?t*DoyNL}i+|ySlstqXAy(U8_sy>;aCqtmiwbmY*>`=o#sEkh9gPr<&X-Nj= zYp$8$uv{Ak)Uwj%u5BW9ByeCGEo)3XdeyVAIfL6cRGj0f43BFbsViNv->>UO@+)V0 z(>yT~#d?#Vi`hW=_2-7r^ml!spN0ar4E;HO?r8%j5AeKCdUCc49nxqFquq5Y0Mk3A zPE9=(t6lxXkB$LQ0eMoGAli;)>7<177?$g1j4X`>&~i z_t{aqtrrzR*p>4-?f|1cWmwY~IJAnscVLS1wKgdgQ@PgwS$(dhF~|w%oxSNSSv|nG zS_%&=;!OMYyexN}rMM)Eo9`$3Fu=|`d5D%Y4H$vkWw|*9Xul8Djd9J}xv~?JB6K9|~iU^eUnhCxC@W2aAqXSDI-49}qjbiJ-9ON_41Hdqwp&2&W_A1r zaTwoF6?l5Q?Wb({?;O3W(`bl(^`nXDE50klbuuWy1xQYW4wliWzpJh8=IW0b6s^kE{BW#b_P!P2PQ|w zBo50Uy}*C3<^P^h!veCkR$4uxibAbqdIStJ#-u;);ns(@@6sLi|0K{e;wEAutI8^=vRX1Vumqnios`O4u| zSmVbo-4LQhXy`8fQ(rhve`avYI>V|`ajNs~MXdBP_0_r^jeGq}+Z!e6vn+V<#M9GP zOSu4{s@mzln_UQ+Jj%gD(k8m67``ii{`w(m9pe?fP z%xp?01pI!*8okD_rw<(=-V5U0-wHy*`sibI{Xw42%(>a$pcm@|-kAj)x^R3mIk&~r0+aJ8<*xW}^Nn=N&Rnk8UBj)d)SkFZo9tVV9**2ZPub$8@Q1x*30b1 z;KMqCjl#5<6VcjVHzK#J$-qo6u*=p2Cmm_$V$;~y-G-O4S6#QUx9>qWJ_}xtiaj*g z(C?kNbfB(y>?rnnAUF0i$9GeHlz)7q% zkau0urv@UO`EaF97HK?We))LIhBf=^LowuDYfWbi^W{>!q1_ywawW5g)>P7pOj9&9 zIbJ9HFroXMxeKz@$|j~y&Gh>H1;lftwmb3sJ-Nu?bG<5Q>f1bDzjhc6zxF3b7s0FR z4I9IkU9n>jE9?kY-I`aUp{|$`g7ezlq(KL5bZG{H2Q!Y?@w;rzwqU8RP3GtZ$is-l zMpK(RpYa;F$WdMVGi|eLjBwO(Y7+=^ZlqFTo-i7#{crW=(ER{JJ3exlPBp-*9AI~E z2_|;9jH(u{nIo_OX7=Kju?_h8HetK_m@QWeMM?Y=tZCPsId|w0bU8{XEf!URC z8tcLkg}<}3hPuaB_BPn`YYoxxFEq1ehmgo#+~VbSvv$kSJp-M#M6Y^iVJ08i;!U}~ z$H)<#b+)#@R)yy_fTbH_-MVQ12q;36Se_o*BYe98a9gKY#ePzp5xIGh?Xg$O=(^|g zbBA#d2VO2T|mQPyob>wTF4&}JdIo7aJ~rs`sHLB-;1 zZ5!Quh+fdXL;!iXMM$3V#igi707;Ag43nnX(Xlkbj7#bJ!#+xA1XY7WeeE;#&|Co!|-M^=e zMJC7xjhYWa&o;#Cv$A~kpYEE$9ZNLNIc%Y81^NMh_cg@5!0aO1Y`qT9b9c=&P#%CH z)@e{JpVhV3(-EeZ_ZEW+w$w25$7T#2>dSrLBwn|gkK+3qWBfQu+Ikao&toH}27^GQ znYj^~;+bfszOL=4JsB0+_SP~>-6*i+#B!mY&UTFmx9h@@QY(gJ!;&rS>0e0VJg`TP z48qIYC`8~T$lPE>H+3t^w@U*aOSi-PjA4+ z&bS2e*-fnm#I@5W9q}m_`G&gGO>7Kisa05NY-ewNwATt-wQqoB1(a=>Tb?QRrq-J| zme=dJ56i$lueBz@M{8SU{oV)_C*=AU(^SvowSI2zfd79~7f*M7L^ya=(Bx2mJj85+ z8@;foHQ+}S8Aj;S+RQQQLwB)zCh~K?hf8@)^NeMmQz_ky({~lBmc|c5m0qym6I$k{ zxz!=sEeuI*j&8SL-La@{+Q5J`!WIUo1(*R~Z-(O`?zLh2od1AbE*PvIsfSsmB_Zi^O_KYtyu$8|3>bv6q0sC1ZH{ZkG@$Hmbhq-b4iYNWOhc&X!z~n!NfOX z#`dMfnt@d}Z2I`?WvEspyX$^bjK)(Naj$7NQ`h=+CEjr!ERA3TxXyeRDu+WeP~`tm z&Pilw!!(6LnK7}kz+h79T!`jx;VTbyKc0Q9g#rJvv@HeAElnQaP^gx;85P^9S-slP zW#84NU)Sm;rSm~goY1J-zFp`d=NR1=pSq|W!uA$pbBqjqleh{_X=9ngqMbzq2-|fk zl%}2Ly<;^81i>%!|{v5wZI$?uTBgwZt|@u43z*G#cp9Ui6CHFe$}4*cw=Njcn+f5cZ@1Q69g*q-jvx4e$^F~v=n zVtB$I+8?V~Q8+keC=~xMs6T3*YJ;np-&KFqtw^Bon;z#I6AIWToujb)`!Tk7=j$~I zF822s$u>DRXs^3gc&rveI@&WwHE z%vn`$I|KjNNl%9bV#AIUAYQ9`BJ#U*wJkx~Dl+&_G^rIfe@FKWeL=RZUZ<47-AwLi zFB{v?7~%R`e^S1GgUcrL;DKZN@;Sj}`q*$=u|uyj^(EMFahhD+qhm*{4Z&_ZcZOxp zA=!%PPQHlMDvnME2XXc1tv!{~w2YLIzy1nEboMpri3>W|-Al_8VZ&wO;5orfs2|G9 zYB|uC_3vg%U2oPlu2Vu6q8sY-{EP?unPE3b#96wM%j@6LJfkxMs<1qIUv=NVfyPId zz@JH4)I^gQD89{*{;DPU!tS~|qRuFEV7UQ%f2LV!$yUMG{9Bq^PgAQhhmlQBr_U5W z4HY#mh%FxmxEq9CxO}ZF&)U$5P2I(amaz?q2+z{#-rRfD)(u}$#56?>V-<@{MZz;P z$>53pB+<}Y=Q+%p*0r0A{^cdF^j!?5ctI-olcr~3O)K3`h|?mF#E_%okeqAY-))?7 zt74P)L?+hZ+sbXlBBlWC+*y}v!9!Emd9GgtE5!8WXnj{;#+ZFq1;U>yj2xo}BT>K) zk(dv{xtpW;Jb+NdY;W~E9bq)LW;?NbX)S$2A=>8DHe;xLE+pKT*IwhAn0}GJDHn9$u$LBLo8-~mzQ2EX zpx&2oBZdB-B>5Q&N2aCdUOStrMWgkhNOu{xq^*sMu(!YR?~iGAO{5O`FR_X)jR+eu z#wFCuNZZ>&Z`zH4FDB7^11^BA>rxUO16ly>!|Z) zobnW`#Ir`a(RxHT-AQ8e(=!_CTFBUDonvan+xG&kt_GTDz?E--Q!;vV*V3{1wn47g z@4ut%Kyjx4+QC+Ldh=`sD_7MqcTLK!9RTJ{qRvwbA3I!s1>k7NnXQ{t+xSs?A{3tpX#4MgQ-_YZHMSSC2e#hMsxi&9;75B_bX%D^kWhQLV5Zr z!{nal>Bn9z@@bj#)$W0xpk}m^$6bo-|^P4rjb2M`> zvtjgQ{7}}RYP|YDeN)3SflGEBFnvomK&#OsY^A3@Q3k8WZssn|jn1_3HV>)PlEP7RfQXJIXIvVHhg6rn0v^H_f>gHUV zxVMKWCm91?uSpZae!`)*b+8*VfT1)N6PvMxzg}L?+@6ouqT1w42{unLiL=12VF503 zKjN~Sk51C&94)KYiR&~V6|iZGeD$~?-#(7(j8q7R?z+dl02i&|PVD$^}^5GRg(SovexR@oHfKi9L? zp4#734+t>z$skPCd6swytG&&d6`f?JVpZP0u1feot?SvPzCL8@Npj+_>ispfyeEcX zQk_CrJ*5q-+-1!!)>P)LNBo)rjHo3Y0-TVz=NOsC+2uO=uuj6M^19a&%1p1q0fBv< z<00#Glc`?609VgxFUPBtyOG)(&InnI;vLEXv2zz4>%?-y zpBASDwe-lQyV5)8L6E)y^YcRibV>xT$j z{Tp)s+UgTyfSIOhX-R&lGc0(fRZLyzKcW`3<-U0NJ>_$?CN47DA1nU4Yi*u$%murA zSqCh2rBQZgHgo_LHEG?wqnk!z@J`cz2==~}fwG@rI+IP!>)B+hj*LT10#cE)0d)9H z%PR1cizfBCpojjcIx=&xSCn7<*qWR~ih0^Rch|Bs;3grCXWa&I|LcLF2O=)c*}i^v zxHmVsTCT5_WU{yKYYpQi8){aDJv;PvsFo!AX-|-@oHS& zqB!T61UIc{UWV!JBs{oKp-Zbnu(A<3^h zArBn;!?X4?f#^y?7UkG85rVw~w5cRf z?{k8^w6+7>q5c8I+84-P@x@bE@*^)^{7SQuXt=D*_5KL?0XR*>`%=ur^hko%KtRFAy=#fspx?dk;OXn>-XFdYnJb!x8^O{Wimq!_dcx uga7YcKkn@}T^JP6phabfNbt7RBc&UvwT)Bn;4rMr{Dl8+@A?0q3;RFs6JJ#T literal 0 HcmV?d00001 From c60bfb877af9f5aa34eac20725e50f857d640a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 13 Jan 2025 17:16:37 +0100 Subject: [PATCH 952/979] chore: Add some missing links to the changelog --- crates/matrix-sdk-crypto/CHANGELOG.md | 4 ++++ crates/matrix-sdk-ui/CHANGELOG.md | 1 + crates/matrix-sdk/CHANGELOG.md | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index b241e0f816d..567c1df6474 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,12 +6,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Features + - Accept stable identifier `sender_device_keys` for MSC4147 (Including device keys with Olm-encrypted events). ([#4420](https://github.com/matrix-org/matrix-rust-sdk/pull/4420)) ## [0.9.0] - 2024-12-18 +### Features + - Expose new API `DehydratedDevices::get_dehydrated_device_pickle_key`, `DehydratedDevices::save_dehydrated_device_pickle_key` and `DehydratedDevices::delete_dehydrated_device_pickle_key` to store/load the dehydrated device pickle key. This allows client to automatically rotate the dehydrated device to avoid one-time-keys exhaustion and to_device accumulation. diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 9aa4649cbf7..acadb6ea0a1 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file. `AttachmentSource` allows to send an attachment either from a file, or with the bytes and the filename of the attachment. Note that all types that implement `Into` also implement `Into`. + ([#4451](https://github.com/matrix-org/matrix-rust-sdk/pull/4451)) ## [0.9.0] - 2024-12-18 diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index c8aa7f0c34a..6e880865601 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -10,12 +10,18 @@ All notable changes to this project will be documented in this file. - Allow to set and check whether an image is animated via its `ImageInfo`. ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) + - Implement `Default` for `BaseImageInfo`, `BaseVideoInfo`, `BaseAudioInfo` and - `BaseFileInfo`. ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) + `BaseFileInfo`. + ([#4503](https://github.com/matrix-org/matrix-rust-sdk/pull/4503)) + - Expose `Client::server_versions()` publicly to allow users of the library to get the versions of Matrix supported by the homeserver. ([#4519](https://github.com/matrix-org/matrix-rust-sdk/pull/4519)) -- Create `RoomPrivacySettings` helper to group room settings functionality related to room access and visibility ([#4401](https://github.com/matrix-org/matrix-rust-sdk/pull/4401)). + +- Create `RoomPrivacySettings` helper to group room settings functionality + related to room access and visibility. + ([#4401](https://github.com/matrix-org/matrix-rust-sdk/pull/4401)) ### Refactor @@ -23,12 +29,19 @@ All notable changes to this project will be documented in this file. `Client::send()` method to the `with_request_config()` builder method. You should call `Client::send(request).with_request_config(request_config).await` now instead. + ([#4443](https://github.com/matrix-org/matrix-rust-sdk/pull/4443)) + - [**breaking**] Remove the `AttachmentConfig::with_thumbnail()` constructor and replace it with the `AttachmentConfig::thumbnail()` builder method. You should call `AttachmentConfig::new().thumbnail(thumbnail)` now instead. + ([#4452](https://github.com/matrix-org/matrix-rust-sdk/pull/4452)) + - [**breaking**] `Room::send_attachment()` and `RoomSendQueue::send_attachment()` now take any type that implements `Into` for the filename. + ([#4451](https://github.com/matrix-org/matrix-rust-sdk/pull/4451)) + - [**breaking**] `Recovery::are_we_the_last_man_standing()` has been renamed to `is_last_device()`. + ([#4522](https://github.com/matrix-org/matrix-rust-sdk/pull/4522)) ## [0.9.0] - 2024-12-18 From e9487b085125d3cd2057c8258a5a1a2c25edc4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 13 Jan 2025 17:32:07 +0100 Subject: [PATCH 953/979] fix(timeline): Add UTDs to the timeline conditionally --- .../src/timeline/event_handler.rs | 4 +- .../integration/timeline/pinned_event.rs | 139 +++++++++++++++++- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 8cdcf1256ee..b3288ca299f 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -447,7 +447,9 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { TimelineEventKind::UnableToDecrypt { content, utd_cause } => { // TODO: Handle replacements if the replaced event is also UTD - self.add_item(TimelineItemContent::unable_to_decrypt(content, utd_cause), None); + if should_add { + self.add_item(TimelineItemContent::unable_to_decrypt(content, utd_cause), None); + } // Let the hook know that we ran into an unable-to-decrypt that is added to the // timeline. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index 6a1e75a95fd..a6996762372 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -16,8 +16,19 @@ use matrix_sdk_ui::{ Timeline, }; use ruma::{ - event_id, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, - MilliSecondsSinceUnixEpoch, OwnedRoomId, + event_id, + events::{ + room::{ + encrypted::{ + EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent, + }, + message::RoomMessageEventContentWithoutRelation, + }, + AnyTimelineEvent, + }, + owned_device_id, owned_room_id, owned_user_id, + serde::Raw, + EventId, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, UserId, }; use serde_json::json; use stream_assert::assert_pending; @@ -342,6 +353,104 @@ async fn test_pinned_timeline_with_no_pinned_event_ids_is_just_empty() { test_helper.server.reset().await; } +#[async_test] +async fn test_pinned_timeline_with_no_pinned_events_and_an_utd_is_just_empty() { + let mut test_helper = TestHelper::new().await; + let room_id = test_helper.room_id.clone(); + let event_id = event_id!("$1:morpheus.localhost"); + let sender_id = owned_user_id!("@example:localhost"); + + // Join the room + let joined_room_builder = JoinedRoomBuilder::new(&room_id) + // Set up encryption + .add_state_event(StateTestEvent::Encryption); + + // Sync the joined room + let json_response = + SyncResponseBuilder::new().add_joined_room(joined_room_builder).build_json_sync_response(); + mock_sync(&test_helper.server, json_response, None).await; + test_helper + .client + .sync_once(test_helper.sync_settings.clone()) + .await + .expect("Sync should work"); + test_helper.server.reset().await; + + // Load initial timeline items: an empty `m.room.pinned_events` event + let _ = test_helper.setup_sync_response(Vec::new(), Some(Vec::new())).await; + + // Mock encrypted event for which we have now keys (an UTD) + let utd_event = create_utd(&room_id, &sender_id, event_id); + mock_event(&test_helper.server, &room_id, event_id, TimelineEvent::new(utd_event)).await; + + let room = test_helper.client.get_room(&room_id).unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(1)).build().await.unwrap(); + + // The timeline couldn't load any events, but it expected none, so it just + // returns an empty list + let (items, _) = timeline.subscribe().await; + assert!(items.is_empty()); + + test_helper.server.reset().await; +} + +#[async_test] +async fn test_pinned_timeline_with_pinned_utd_contains_it() { + let test_helper = TestHelper::new().await; + let room_id = test_helper.room_id.clone(); + let event_id = event_id!("$1:morpheus.localhost"); + let sender_id = owned_user_id!("@example:localhost"); + + // Join the room + let joined_room_builder = JoinedRoomBuilder::new(&room_id) + // Set up encryption + .add_state_event(StateTestEvent::Encryption) + // And pinned event ids + .add_state_event(StateTestEvent::Custom(json!( + { + "content": { + "pinned": [event_id] + }, + "event_id": "$15139375513VdeRF:localhost", + "origin_server_ts": 151393755, + "sender": sender_id, + "state_key": "", + "type": "m.room.pinned_events", + "unsigned": { + "age": 703422 + } + } + ))); + + // Sync the joined room + let json_response = + SyncResponseBuilder::new().add_joined_room(joined_room_builder).build_json_sync_response(); + mock_sync(&test_helper.server, json_response, None).await; + test_helper + .client + .sync_once(test_helper.sync_settings.clone()) + .await + .expect("Sync should work"); + test_helper.server.reset().await; + + // Mock encrypted pinned event for which we have now keys (an UTD) + let utd_event = create_utd(&room_id, &sender_id, event_id); + mock_event(&test_helper.server, &room_id, event_id, TimelineEvent::new(utd_event)).await; + + let room = test_helper.client.get_room(&room_id).unwrap(); + let timeline = + Timeline::builder(&room).with_focus(pinned_events_focus(1)).build().await.unwrap(); + + // The timeline loaded with just a day divider and the pinned UTD + let (items, _) = timeline.subscribe().await; + assert_eq!(items.len(), 2); + let pinned_utd_event = items.last().unwrap().as_event().unwrap(); + assert_eq!(pinned_utd_event.event_id().unwrap(), event_id); + + test_helper.server.reset().await; +} + #[async_test] async fn test_edited_events_are_reflected_in_sync() { let mut test_helper = TestHelper::new().await; @@ -766,6 +875,32 @@ impl TestHelper { } } +fn create_utd(room_id: &RoomId, sender_id: &UserId, event_id: &EventId) -> Raw { + EventFactory::new() + .room(room_id) + .sender(sender_id) + .event(RoomEncryptedEventContent::new( + EncryptedEventScheme::MegolmV1AesSha2( + MegolmV1AesSha2ContentInit { + ciphertext: String::from( + "AwgAEpABhetEzzZzyYrxtEVUtlJnZtJcURBlQUQJ9irVeklCTs06LwgTMQj61PMUS4Vy\ + YOX+PD67+hhU40/8olOww+Ud0m2afjMjC3wFX+4fFfSkoWPVHEmRVucfcdSF1RSB4EmK\ + PIP4eo1X6x8kCIMewBvxl2sI9j4VNvDvAN7M3zkLJfFLOFHbBviI4FN7hSFHFeM739Zg\ + iwxEs3hIkUXEiAfrobzaMEM/zY7SDrTdyffZndgJo7CZOVhoV6vuaOhmAy4X2t4UnbuV\ + JGJjKfV57NAhp8W+9oT7ugwO", + ), + device_id: owned_device_id!("KIUVQQSDTM"), + sender_key: String::from("LvryVyoCjdONdBCi2vvoSbI34yTOx7YrCFACUEKoXnc"), + session_id: String::from("64H7XKokIx0ASkYDHZKlT5zd/Zccz/cQspPNdvnNULA"), + } + .into(), + ), + None, + )) + .event_id(event_id) + .into_raw_timeline() +} + fn pinned_events_focus(max_events_to_load: u16) -> TimelineFocus { TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests: 10 } } From b9014a5e2a20b0c58cd5b8d0cf0f283c44498142 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 14 Jan 2025 11:15:08 +0100 Subject: [PATCH 954/979] test: keep a single sync in `test_delayed_invite_response_and_sent_message_decryption()` This removes one sync that happens in the background, because it's likely spurious and may be confusing the server about what's been seen by the current client. --- .../src/tests/sliding_sync/room.rs | 100 ++++++++++-------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index f845287f6b9..7a441e9ec17 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -38,7 +38,7 @@ use matrix_sdk::{ }, sliding_sync::VersionBuilder, test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, - Client, RoomInfo, RoomMemberships, RoomState, SlidingSyncList, SlidingSyncMode, + Client, Room, RoomInfo, RoomMemberships, RoomState, SlidingSyncList, SlidingSyncMode, }; use matrix_sdk_base::{ ruma::{owned_room_id, room_alias_id}, @@ -835,10 +835,28 @@ async fn test_delayed_decryption_latest_event() -> Result<()> { Ok(()) } +async fn get_or_wait_for_room(client: &Client, room_id: &RoomId) -> Room { + let (mut rooms, mut room_stream) = client.rooms_stream(); + + loop { + if let Some(room) = rooms.iter().find(|room| room.room_id() == room_id) { + return room.clone(); + } + + if let Ok(Some(diffs)) = timeout(Duration::from_secs(3), room_stream.next()).await { + for diff in diffs { + diff.apply(&mut rooms); + } + } else { + panic!("bob never founds out about the room"); + } + } +} + #[tokio::test] -async fn test_delayed_invite_response_and_sent_message_decryption() -> Result<()> { - let alice = TestClientBuilder::new("alice").use_sqlite().build().await?; - let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; +async fn test_delayed_invite_response_and_sent_message_decryption() { + let alice = TestClientBuilder::new("alice").use_sqlite().build().await.unwrap(); + let bob = TestClientBuilder::new("bob").use_sqlite().build().await.unwrap(); let alice_sync_service = SyncService::builder(alice.clone()).build().await.unwrap(); alice_sync_service.start().await; @@ -853,74 +871,72 @@ async fn test_delayed_invite_response_and_sent_message_decryption() -> Result<() is_direct: true, preset: Some(RoomPreset::PrivateChat), })) - .await?; - alice_room.enable_encryption().await?; + .await + .unwrap(); + alice_room.enable_encryption().await.unwrap(); // Initial message to make sure any lazy /members call is performed before the // test actually starts alice_room .send(RoomMessageEventContent::text_plain("dummy message to make members call")) - .await?; + .await + .unwrap(); // Send the invite to Bob and a message to reproduce the edge case alice_room.invite_user_by_id(bob.user_id().unwrap()).await.unwrap(); - alice_room.send(RoomMessageEventContent::text_plain("hello world")).await?; + alice_room.send(RoomMessageEventContent::text_plain("hello world")).await.unwrap(); - // Wait until Bob receives the invite - let bob_sync_stream = bob.sync_stream(SyncSettings::new()).await; - pin_mut!(bob_sync_stream); + let room_id = alice_room.room_id(); - while let Some(Ok(response)) = - timeout(Duration::from_secs(3), bob_sync_stream.next()).await.expect("Room sync timed out") - { - if response.rooms.invite.contains_key(alice_room.room_id()) { - break; - } - } + // Wait until Bob receives the invite. + let bob_room = get_or_wait_for_room(&bob, room_id).await; + + // Join the room from Bob's client. + let bob_timeline = bob_room.timeline().await.unwrap(); + let (_, timeline_stream) = bob_timeline.subscribe().await; + pin_mut!(timeline_stream); - // Join the room from Bob's client - let bob_room = bob.get_room(alice_room.room_id()).unwrap(); - bob_room.join().await?; + info!("Bob joins the room."); + bob_room.join().await.unwrap(); assert_eq!(alice_room.state(), RoomState::Joined); assert!(alice_room.is_encrypted().await.unwrap()); assert_eq!(bob_room.state(), RoomState::Joined); assert!(bob_room.is_encrypted().await.unwrap()); - let bob_timeline = bob_room.timeline().await?; - let (_, timeline_stream) = bob_timeline.subscribe().await; - pin_mut!(timeline_stream); - - // Get previous events, including the sent messages - bob_timeline.paginate_backwards(3).await?; - - // Look for the sent message, which should not be an UTD event - loop { - let diff = timeout(Duration::from_millis(300), timeline_stream.next()) - .await - .expect("Failed to receive the decrypted sent message") - .unwrap(); + // Get previous events, including the sent messages. + bob_timeline.paginate_backwards(3).await.unwrap(); + // Look for the sent message, which should not be an UTD event. + while let Ok(Some(diff)) = timeout(Duration::from_secs(3), timeline_stream.next()).await { trace!(?diff, "Received diff from Bob's room"); match diff { - VectorDiff::PushBack { value: event } + VectorDiff::PushFront { value: event } + | VectorDiff::PushBack { value: event } | VectorDiff::Insert { value: event, .. } | VectorDiff::Set { value: event, .. } => { - if let Some(content) = event.as_event().map(|e| e.content()) { - if let Some(message) = content.as_message() { - if message.body() == "hello world" { - return Ok(()); - } + let Some(event) = event.as_event() else { + continue; + }; - panic!("Unexpected message event found"); - } + let content = event.content(); + + if content.as_unable_to_decrypt().is_some() { + info!("Observed UTD for {}", event.event_id().unwrap()); + } + + if let Some(message) = content.as_message() { + assert_eq!(message.body(), "hello world"); + return; } } _ => {} } } + + panic!("We never received the decrypted event!"); } #[tokio::test] From e015a531da018e6dece8be1e5369392c743475d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 10 Jan 2025 13:19:58 +0100 Subject: [PATCH 955/979] feat(room): Add `fn Room::own_membership_details` This will retrieve the room member info of both the current user and the info for the sender of the current user's room member event. --- crates/matrix-sdk/src/room/mod.rs | 152 ++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 28ca6523da4..19210294482 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2852,6 +2852,35 @@ impl Room { Ok(Invite { invitee, inviter }) } + /// Get the membership details for the current user. + /// + /// Returns: + /// - If the current user was present in the room, a tuple of the + /// current user's [`RoomMember`] info and the member info of the + /// sender of that member event. + /// - If the current user is not present, an error. + pub async fn own_membership_details(&self) -> Result<(RoomMember, Option)> { + let Some(own_member) = self.get_member_no_sync(self.own_user_id()).await? else { + return Err(Error::InsufficientData); + }; + + let sender_member = + if let Some(member) = self.get_member_no_sync(own_member.event().sender()).await? { + // If the sender room member info is already available, return it + Some(member) + } else if self.are_members_synced() { + // The room members are synced and we couldn't find the sender info + None + } else if self.sync_members().await.is_ok() { + // Try getting the sender room member info again after syncing + self.get_member_no_sync(own_member.event().sender()).await? + } else { + None + }; + + Ok((own_member, sender_member)) + } + /// Forget this room. /// /// This communicates to the homeserver that it should forget the room. @@ -3656,6 +3685,7 @@ pub struct TryFromReportedContentScoreError(()); #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { + use assert_matches2::assert_matches; use matrix_sdk_base::{store::ComposerDraftType, ComposerDraft, SessionMeta}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, test_json, JoinedRoomBuilder, StateTestEvent, @@ -3888,4 +3918,126 @@ mod tests { (event_id.to_owned(), user_id.to_owned()) ) } + + #[async_test] + async fn test_own_room_membership_with_no_own_member_event() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + + let room = server.sync_joined_room(&client, room_id).await; + + // Since there is no member event for the own user, the method fails. + // This should never happen in an actual room. + let error = room.own_membership_details().await.err(); + assert!(error.is_some()); + } + + #[async_test] + async fn test_own_room_membership_with_own_member_event_but_unknown_sender() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + let user_id = user_id!("@example:localhost"); + + let f = EventFactory::new().room(room_id).sender(user_id!("@alice:b.c")); + let joined_room_builder = JoinedRoomBuilder::new(room_id) + .add_state_bulk(vec![f.member(user_id).into_raw_sync().cast()]); + let room = server.sync_room(&client, joined_room_builder).await; + + // When we load the membership details + let ret = room.own_membership_details().await; + assert_matches!(ret, Ok((member, sender))); + + // We get the member info for the current user + assert_eq!(member.event().user_id(), user_id); + + // But there is no info for the sender + assert!(sender.is_none()); + } + + #[async_test] + async fn test_own_room_membership_with_own_member_event_and_own_sender() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + let user_id = user_id!("@example:localhost"); + + let f = EventFactory::new().room(room_id).sender(user_id); + let joined_room_builder = JoinedRoomBuilder::new(room_id) + .add_state_bulk(vec![f.member(user_id).into_raw_sync().cast()]); + let room = server.sync_room(&client, joined_room_builder).await; + + // When we load the membership details + let ret = room.own_membership_details().await; + assert_matches!(ret, Ok((member, sender))); + + // We get the current user's member info + assert_eq!(member.event().user_id(), user_id); + + // And the sender has the same info, since it's also the current user + assert!(sender.is_some()); + assert_eq!(sender.unwrap().event().user_id(), user_id); + } + + #[async_test] + async fn test_own_room_membership_with_own_member_event_and_known_sender() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + let user_id = user_id!("@example:localhost"); + let sender_id = user_id!("@alice:b.c"); + + let f = EventFactory::new().room(room_id).sender(sender_id); + let joined_room_builder = JoinedRoomBuilder::new(room_id).add_state_bulk(vec![ + f.member(user_id).into_raw_sync().cast(), + // The sender info comes from the sync + f.member(sender_id).into_raw_sync().cast(), + ]); + let room = server.sync_room(&client, joined_room_builder).await; + + // When we load the membership details + let ret = room.own_membership_details().await; + assert_matches!(ret, Ok((member, sender))); + + // We get the current user's member info + assert_eq!(member.event().user_id(), user_id); + + // And also the sender info from the events received in the sync + assert!(sender.is_some()); + assert_eq!(sender.unwrap().event().user_id(), sender_id); + } + + #[async_test] + async fn test_own_room_membership_with_own_member_event_and_unknown_but_available_sender() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!a:b.c"); + let user_id = user_id!("@example:localhost"); + let sender_id = user_id!("@alice:b.c"); + + let f = EventFactory::new().room(room_id).sender(sender_id); + let joined_room_builder = JoinedRoomBuilder::new(room_id) + .add_state_bulk(vec![f.member(user_id).into_raw_sync().cast()]); + let room = server.sync_room(&client, joined_room_builder).await; + + // We'll receive the member info through the /members endpoint + server + .mock_get_members() + .ok(vec![f.member(sender_id).into_raw_timeline().cast()]) + .mock_once() + .mount() + .await; + + // We get the current user's member info + let ret = room.own_membership_details().await; + assert_matches!(ret, Ok((member, sender))); + + // We get the current user's member info + assert_eq!(member.event().user_id(), user_id); + + // And also the sender info from the /members endpoint + assert!(sender.is_some()); + assert_eq!(sender.unwrap().event().user_id(), sender_id); + } } From bd5d7aafee3cac797e709eadc2f914438211a526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 10 Jan 2025 13:20:10 +0100 Subject: [PATCH 956/979] feat(ffi): Add FFI bindings for `fn Room::own_membership_details`. Also add `membership_change_reason` field to `ffi::RoomMember`. --- bindings/matrix-sdk-ffi/src/room_member.rs | 4 +++- bindings/matrix-sdk-ffi/src/room_preview.rs | 26 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/room_member.rs b/bindings/matrix-sdk-ffi/src/room_member.rs index 36e40743f62..b980d14a81b 100644 --- a/bindings/matrix-sdk-ffi/src/room_member.rs +++ b/bindings/matrix-sdk-ffi/src/room_member.rs @@ -76,7 +76,7 @@ pub fn matrix_to_user_permalink(user_id: String) -> Result Ok(user_id.matrix_to_uri().to_string()) } -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct RoomMember { pub user_id: String, pub display_name: Option, @@ -87,6 +87,7 @@ pub struct RoomMember { pub normalized_power_level: i64, pub is_ignored: bool, pub suggested_role_for_power_level: RoomMemberRole, + pub membership_change_reason: Option, } impl TryFrom for RoomMember { @@ -103,6 +104,7 @@ impl TryFrom for RoomMember { normalized_power_level: m.normalized_power_level(), is_ignored: m.is_ignored(), suggested_role_for_power_level: m.suggested_role_for_power_level(), + membership_change_reason: m.event().reason().map(|s| s.to_owned()), }) } } diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 49d040c52e1..b31da89c4e9 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -64,6 +64,32 @@ impl RoomPreview { let invite_details = room.invite_details().await.ok()?; invite_details.inviter.and_then(|m| m.try_into().ok()) } + + /// Get the membership details for the current user. + pub async fn own_membership_details(&self) -> Option { + let room = self.client.get_room(&self.inner.room_id)?; + + let (own_member, sender_member) = match room.own_membership_details().await { + Ok(memberships) => memberships, + Err(error) => { + warn!("Couldn't get membership info: {error}"); + return None; + } + }; + + Some(RoomMembershipDetails { + own_room_member: own_member.try_into().ok()?, + sender_room_member: sender_member.and_then(|member| member.try_into().ok()), + }) + } +} + +/// Contains the current user's room member info and the optional room member +/// info of the sender of the `m.room.member` event that this info represents. +#[derive(uniffi::Record)] +pub struct RoomMembershipDetails { + pub own_room_member: RoomMember, + pub sender_room_member: Option, } impl RoomPreview { From fedf7d214f0252918e043a18ede2007b11d36285 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 7 Jan 2025 13:44:59 +0000 Subject: [PATCH 957/979] test: add some snapshot tests before we change anything --- Cargo.lock | 1 + testing/matrix-sdk-test/Cargo.toml | 1 + .../src/test_json/keys_query_sets.rs | 18 ++++ ...tionTestData::dan_keys_query_response.snap | 98 +++++++++++++++++++ ..._keys_query_response_device_loggedout.snap | 81 +++++++++++++++ ...utionTestData::me_keys_query_response.snap | 55 +++++++++++ ...onTestData::own_keys_query_response_1.snap | 55 +++++++++++ 7 files changed, 309 insertions(+) create mode 100644 testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap create mode 100644 testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap create mode 100644 testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap create mode 100644 testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap diff --git a/Cargo.lock b/Cargo.lock index ac2f7d71988..08b1ac2c43f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3469,6 +3469,7 @@ dependencies = [ "ctor", "getrandom", "http", + "insta", "matrix-sdk-common", "matrix-sdk-test-macros", "once_cell", diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index 75b8e2953f8..d87a394cfd4 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -18,6 +18,7 @@ doctest = false [dependencies] as_variant = { workspace = true } http = { workspace = true } +insta = { workspace = true } matrix-sdk-common = { path = "../../crates/matrix-sdk-common" } matrix-sdk-test-macros = { version = "0.7.0", path = "../matrix-sdk-test-macros" } once_cell = { workspace = true } diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 9e0e43a7a8b..34282b745b5 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -1,3 +1,4 @@ +use insta::{assert_json_snapshot, with_settings}; use ruma::{ api::client::keys::get_keys::v3::Response as KeyQueryResponse, device_id, encryption::DeviceKeys, serde::Raw, user_id, DeviceId, OwnedDeviceId, UserId, @@ -98,6 +99,9 @@ impl KeyDistributionTestData { } }); + with_settings!({sort_maps => true}, { + assert_json_snapshot!("KeyDistributionTestData::me_keys_query_response", data); + }); ruma_response_from_json(&data) } @@ -200,6 +204,10 @@ impl KeyDistributionTestData { } }); + with_settings!({sort_maps => true}, { + assert_json_snapshot!("KeyDistributionTestData::dan_keys_query_response", data); + }); + ruma_response_from_json(&data) } @@ -283,6 +291,13 @@ impl KeyDistributionTestData { } }); + with_settings!({sort_maps => true}, { + assert_json_snapshot!( + "KeyDistributionTestData::dan_keys_query_response_device_loggedout", + data + ); + }); + ruma_response_from_json(&data) } @@ -631,6 +646,9 @@ impl VerificationViolationTestData { } }); + with_settings!({sort_maps => true}, { + assert_json_snapshot!("VerificationViolationTestData::own_keys_query_response_1", data); + }); ruma_response_from_json(&data) } diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap new file mode 100644 index 00000000000..83fe4ed7868 --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap @@ -0,0 +1,98 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "device_keys": { + "@dan:localhost": { + "FRGNMZVOKA": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "FRGNMZVOKA", + "keys": { + "curve25519:FRGNMZVOKA": "Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4", + "ed25519:FRGNMZVOKA": "jVroR0JoRemjF0vJslY3HirJgwfX5gm5DCM64hZgkI0" + }, + "signatures": { + "@dan:localhost": { + "ed25519:FRGNMZVOKA": "+row23EcWR2D8EKgwzZmy3dWz/l5DHvEHR6jHKnBohphEIsBl0o3Cp9rIztFpStFGRPSAa3xEqfMVW2dIaKkCg" + } + }, + "user_id": "@dan:localhost" + }, + "JHPUERYQUW": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "JHPUERYQUW", + "keys": { + "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", + "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" + }, + "signatures": { + "@dan:localhost": { + "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", + "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" + } + }, + "user_id": "@dan:localhost" + } + } + }, + "failures": {}, + "master_keys": { + "@dan:localhost": { + "keys": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" + }, + "signatures": { + "@dan:localhost": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", + "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" + }, + "@me:localhost": { + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" + } + }, + "usage": [ + "master" + ], + "user_id": "@dan:localhost" + } + }, + "self_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" + }, + "signatures": { + "@dan:localhost": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@dan:localhost" + } + }, + "user_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" + }, + "signatures": { + "@dan:localhost": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@dan:localhost" + } + } +} diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap new file mode 100644 index 00000000000..67e66315a9a --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap @@ -0,0 +1,81 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "device_keys": { + "@dan:localhost": { + "JHPUERYQUW": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "JHPUERYQUW", + "keys": { + "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", + "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" + }, + "signatures": { + "@dan:localhost": { + "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", + "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" + } + }, + "user_id": "@dan:localhost" + } + } + }, + "failures": {}, + "master_keys": { + "@dan:localhost": { + "keys": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" + }, + "signatures": { + "@dan:localhost": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", + "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" + }, + "@me:localhost": { + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" + } + }, + "usage": [ + "master" + ], + "user_id": "@dan:localhost" + } + }, + "self_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" + }, + "signatures": { + "@dan:localhost": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@dan:localhost" + } + }, + "user_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" + }, + "signatures": { + "@dan:localhost": { + "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@dan:localhost" + } + } +} diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap new file mode 100644 index 00000000000..fc90577dd78 --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap @@ -0,0 +1,55 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "master_keys": { + "@me:localhost": { + "keys": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4" + }, + "signatures": { + "@me:localhost": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "5G9+Ns28rzNd+2DvP73Y0orr8sxduRQcrJj0YB7ZygH7oeXshvGLeQn6mcNs7q7ZrMR5bYlXxopufKSWWoKpCg", + "ed25519:YVKUSVBKWX": "ih1Kmj4dTB1AjjkwrLA2qIL3e/oPUFisP5Ic8kGp29wrpoHokasKKnkRl1zS7zq6iBcOL6aOZLPPX/ZHYCX5BQ" + } + }, + "usage": [ + "master" + ], + "user_id": "@me:localhost" + } + }, + "self_signing_keys": { + "@me:localhost": { + "keys": { + "ed25519:9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw": "9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw" + }, + "signatures": { + "@me:localhost": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "amiKDLpWIwUQPzq+eov6KJsoskkWA1YzrGNb7HF3OcGV0nm4t7df0tUdZB/OpREtT5D78BKtzOPUipde2DxUAw" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@me:localhost" + } + }, + "user_signing_keys": { + "@me:localhost": { + "keys": { + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY" + }, + "signatures": { + "@me:localhost": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "Cv56vTHAzRkvdcELleOlhECZQP0pXcikCdEZrnXbkjXQ/k0ZvVOJ1beG/SiH8xc6zh1bCIMYv96C9p8o+7VZCQ" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@me:localhost" + } + } +} diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap new file mode 100644 index 00000000000..30eb655fdc0 --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap @@ -0,0 +1,55 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "master_keys": { + "@alice:localhost": { + "keys": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk" + }, + "signatures": { + "@alice:localhost": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "FX+srrw9SRmi12fexYHH1jrlEIWgOfre1aPNzDZWcAlaP9WKRdhcQGh70/3F9hk/PGr51I+ux62YgU4xnRTqAA", + "ed25519:PWVCNMMGCT": "teLq0rCYKX9h8WXu6kH8UE6HPKAtkF/DwCncxJGvVBCyZRtLHD8W1yYEzJXjTNynn+4fibQZBhR3th1RGLn4Ag" + } + }, + "usage": [ + "master" + ], + "user_id": "@alice:localhost" + } + }, + "self_signing_keys": { + "@alice:localhost": { + "keys": { + "ed25519:WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw": "WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw" + }, + "signatures": { + "@alice:localhost": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "lCV9R1xjD34arzq/CAuej1XBv+Ip4dFfAGHfe7znbW7rnwKDaX5PaX3MHk+EIC7nXvUYEAn502WcUFme5c0cCQ" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@alice:localhost" + } + }, + "user_signing_keys": { + "@alice:localhost": { + "keys": { + "ed25519:MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0": "MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0" + }, + "signatures": { + "@alice:localhost": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "A73QfZ5Dzhh7abdal/sEaq1bfgxzPFU8Bvwa9Y5TIe/a5jTmLVubNmsMSsO5tOT+b6aVJg1G4FtId0Q/cb1aAA" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@alice:localhost" + } + } +} From c9bac4ff2bca7a39617caa5ab3ac69c75f22f4b7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 14 Jan 2025 12:40:38 +0000 Subject: [PATCH 958/979] test: snapshot the generated object rather than the JSON We're going to be switching away from JSON-twiddling, so let's snapshot the real object rather than the JSON. --- testing/matrix-sdk-test/Cargo.toml | 5 +-- testing/matrix-sdk-test/src/lib.rs | 14 +++++++- .../src/test_json/keys_query_sets.rs | 33 ++++++++++++------- ...tionTestData::dan_keys_query_response.snap | 3 +- ..._keys_query_response_device_loggedout.snap | 3 +- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index d87a394cfd4..080cea820c4 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -22,8 +22,9 @@ insta = { workspace = true } matrix-sdk-common = { path = "../../crates/matrix-sdk-common" } matrix-sdk-test-macros = { version = "0.7.0", path = "../matrix-sdk-test-macros" } once_cell = { workspace = true } -# Enabling the unstable feature for polls support. -ruma = { workspace = true, features = ["rand", "unstable-msc3381"] } +# Enable the unstable feature for polls support. +# "client-api-s" enables need the "server" feature of ruma-client-api, which is needed to serialize Response objects to JSON. +ruma = { workspace = true, features = ["client-api-s", "rand", "unstable-msc3381"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/testing/matrix-sdk-test/src/lib.rs b/testing/matrix-sdk-test/src/lib.rs index 5a3c5d721e8..8b80e4ad798 100644 --- a/testing/matrix-sdk-test/src/lib.rs +++ b/testing/matrix-sdk-test/src/lib.rs @@ -2,7 +2,9 @@ use http::Response; pub use matrix_sdk_test_macros::async_test; use once_cell::sync::Lazy; use ruma::{ - api::{client::sync::sync_events::v3::Response as SyncResponse, IncomingResponse}, + api::{ + client::sync::sync_events::v3::Response as SyncResponse, IncomingResponse, OutgoingResponse, + }, room_id, user_id, RoomId, UserId, }; use serde_json::Value as JsonValue; @@ -169,3 +171,13 @@ pub fn ruma_response_from_json( Response::builder().status(200).body(json_bytes).expect("Failed to build HTTP response"); ResponseType::try_from_http_response(http_response).expect("Can't parse the response json") } + +/// Serialise a typed Ruma [`OutgoingResponse`] object to JSON. +pub fn ruma_response_to_json( + response: ResponseType, +) -> serde_json::Value { + let http_response: Response> = + response.try_into_http_response().expect("Failed to build HTTP response"); + let json_bytes = http_response.into_body(); + serde_json::from_slice(&json_bytes).expect("Can't parse the response JSON") +} diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 34282b745b5..c3d75bd47cb 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -7,7 +7,7 @@ use serde_json::{json, Value}; use super::keys_query::{keys_query, master_keys, KeysQueryUser}; use crate::{ - ruma_response_from_json, + ruma_response_from_json, ruma_response_to_json, test_json::keys_query::{device_keys_payload, self_signing_keys}, }; @@ -99,10 +99,14 @@ impl KeyDistributionTestData { } }); + let response: KeyQueryResponse = ruma_response_from_json(&data); with_settings!({sort_maps => true}, { - assert_json_snapshot!("KeyDistributionTestData::me_keys_query_response", data); + assert_json_snapshot!( + "KeyDistributionTestData::me_keys_query_response", + ruma_response_to_json(response.clone()), + ); }); - ruma_response_from_json(&data) + response } /// Dan has cross-signing setup, one device is cross signed `JHPUERYQUW`, @@ -204,11 +208,14 @@ impl KeyDistributionTestData { } }); + let response: KeyQueryResponse = ruma_response_from_json(&data); with_settings!({sort_maps => true}, { - assert_json_snapshot!("KeyDistributionTestData::dan_keys_query_response", data); + assert_json_snapshot!( + "KeyDistributionTestData::dan_keys_query_response", + ruma_response_to_json(response.clone()), + ); }); - - ruma_response_from_json(&data) + response } /// Same as `dan_keys_query_response` but `FRGNMZVOKA` was removed. @@ -291,14 +298,14 @@ impl KeyDistributionTestData { } }); + let response: KeyQueryResponse = ruma_response_from_json(&data); with_settings!({sort_maps => true}, { assert_json_snapshot!( "KeyDistributionTestData::dan_keys_query_response_device_loggedout", - data + ruma_response_to_json(response.clone()), ); }); - - ruma_response_from_json(&data) + response } /// Dave is a user that has not enabled cross-signing @@ -646,10 +653,14 @@ impl VerificationViolationTestData { } }); + let response: KeyQueryResponse = ruma_response_from_json(&data); with_settings!({sort_maps => true}, { - assert_json_snapshot!("VerificationViolationTestData::own_keys_query_response_1", data); + assert_json_snapshot!( + "VerificationViolationTestData::own_keys_query_response_1", + ruma_response_to_json(response.clone()), + ); }); - ruma_response_from_json(&data) + response } /// A second `/keys/query` response for Alice, containing a *different* set diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap index 83fe4ed7868..cbba19a00e2 100644 --- a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap @@ -1,6 +1,6 @@ --- source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs -expression: data +expression: ruma_response_to_json(response.clone()) --- { "device_keys": { @@ -42,7 +42,6 @@ expression: data } } }, - "failures": {}, "master_keys": { "@dan:localhost": { "keys": { diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap index 67e66315a9a..e2a38389278 100644 --- a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap @@ -1,6 +1,6 @@ --- source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs -expression: data +expression: ruma_response_to_json(response.clone()) --- { "device_keys": { @@ -25,7 +25,6 @@ expression: data } } }, - "failures": {}, "master_keys": { "@dan:localhost": { "keys": { From b6be4d5170a9dd148dc3778c00da944874f7d389 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 8 Jan 2025 01:05:41 +0000 Subject: [PATCH 959/979] test: remove redundant sig on master key Our test helper won't do this, and it's redundant --- testing/matrix-sdk-test/src/test_json/keys_query_sets.rs | 2 -- ..._sets__KeyDistributionTestData::me_keys_query_response.snap | 3 +-- ...rificationViolationTestData::own_keys_query_response_1.snap | 3 +-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index c3d75bd47cb..ad29596da1a 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -56,7 +56,6 @@ impl KeyDistributionTestData { "signatures": { "@me:localhost": { "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "5G9+Ns28rzNd+2DvP73Y0orr8sxduRQcrJj0YB7ZygH7oeXshvGLeQn6mcNs7q7ZrMR5bYlXxopufKSWWoKpCg", - "ed25519:YVKUSVBKWX": "ih1Kmj4dTB1AjjkwrLA2qIL3e/oPUFisP5Ic8kGp29wrpoHokasKKnkRl1zS7zq6iBcOL6aOZLPPX/ZHYCX5BQ" } }, "usage": [ @@ -610,7 +609,6 @@ impl VerificationViolationTestData { "signatures": { "@alice:localhost": { "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "FX+srrw9SRmi12fexYHH1jrlEIWgOfre1aPNzDZWcAlaP9WKRdhcQGh70/3F9hk/PGr51I+ux62YgU4xnRTqAA", - "ed25519:PWVCNMMGCT": "teLq0rCYKX9h8WXu6kH8UE6HPKAtkF/DwCncxJGvVBCyZRtLHD8W1yYEzJXjTNynn+4fibQZBhR3th1RGLn4Ag" } }, "usage": [ diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap index fc90577dd78..4a0330b121c 100644 --- a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap @@ -10,8 +10,7 @@ expression: data }, "signatures": { "@me:localhost": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "5G9+Ns28rzNd+2DvP73Y0orr8sxduRQcrJj0YB7ZygH7oeXshvGLeQn6mcNs7q7ZrMR5bYlXxopufKSWWoKpCg", - "ed25519:YVKUSVBKWX": "ih1Kmj4dTB1AjjkwrLA2qIL3e/oPUFisP5Ic8kGp29wrpoHokasKKnkRl1zS7zq6iBcOL6aOZLPPX/ZHYCX5BQ" + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "5G9+Ns28rzNd+2DvP73Y0orr8sxduRQcrJj0YB7ZygH7oeXshvGLeQn6mcNs7q7ZrMR5bYlXxopufKSWWoKpCg" } }, "usage": [ diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap index 30eb655fdc0..44e7d4852a7 100644 --- a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap @@ -10,8 +10,7 @@ expression: data }, "signatures": { "@alice:localhost": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "FX+srrw9SRmi12fexYHH1jrlEIWgOfre1aPNzDZWcAlaP9WKRdhcQGh70/3F9hk/PGr51I+ux62YgU4xnRTqAA", - "ed25519:PWVCNMMGCT": "teLq0rCYKX9h8WXu6kH8UE6HPKAtkF/DwCncxJGvVBCyZRtLHD8W1yYEzJXjTNynn+4fibQZBhR3th1RGLn4Ag" + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "FX+srrw9SRmi12fexYHH1jrlEIWgOfre1aPNzDZWcAlaP9WKRdhcQGh70/3F9hk/PGr51I+ux62YgU4xnRTqAA" } }, "usage": [ From 5fadde5a6deeb01e523ba82647bed62e1b04a047 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 8 Jan 2025 14:04:01 +0000 Subject: [PATCH 960/979] test: implement test user data builder type ... and use it for some simple data --- Cargo.lock | 1 + testing/matrix-sdk-test/Cargo.toml | 3 +- .../src/test_json/keys_query_sets.rs | 364 +++++++++++++++--- 3 files changed, 314 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08b1ac2c43f..03552059e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3478,6 +3478,7 @@ dependencies = [ "serde_json", "tokio", "tracing-subscriber", + "vodozemac", "wasm-bindgen-test", "wiremock", ] diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index 080cea820c4..9e80e7c22af 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -24,9 +24,10 @@ matrix-sdk-test-macros = { version = "0.7.0", path = "../matrix-sdk-test-macros" once_cell = { workspace = true } # Enable the unstable feature for polls support. # "client-api-s" enables need the "server" feature of ruma-client-api, which is needed to serialize Response objects to JSON. -ruma = { workspace = true, features = ["client-api-s", "rand", "unstable-msc3381"] } +ruma = { workspace = true, features = ["canonical-json", "client-api-s", "rand", "unstable-msc3381"] } serde = { workspace = true } serde_json = { workspace = true } +vodozemac = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ctor = "0.2.9" diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index ad29596da1a..1080a11345b 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -1,9 +1,17 @@ +use std::{collections::BTreeMap, default::Default}; + use insta::{assert_json_snapshot, with_settings}; use ruma::{ - api::client::keys::get_keys::v3::Response as KeyQueryResponse, device_id, - encryption::DeviceKeys, serde::Raw, user_id, DeviceId, OwnedDeviceId, UserId, + api::client::keys::get_keys::v3::Response as KeyQueryResponse, + device_id, + encryption::{CrossSigningKey, DeviceKeys, KeyUsage}, + serde::Raw, + user_id, CanonicalJsonValue, CrossSigningKeyId, CrossSigningOrDeviceSignatures, + CrossSigningOrDeviceSigningKeyId, DeviceId, OwnedBase64PublicKey, + OwnedBase64PublicKeyOrDeviceId, OwnedDeviceId, OwnedUserId, SigningKeyAlgorithm, UserId, }; use serde_json::{json, Value}; +use vodozemac::{Curve25519PublicKey, Ed25519PublicKey, Ed25519SecretKey, Ed25519Signature}; use super::keys_query::{keys_query, master_keys, KeysQueryUser}; use crate::{ @@ -11,6 +19,205 @@ use crate::{ test_json::keys_query::{device_keys_payload, self_signing_keys}, }; +/// A test helper for building test data sets for `/keys/query` response objects +/// ([`KeyQueryResponse`]). +pub struct KeyQueryResponseTemplate { + /// The User ID of the user that this test data is about. + user_id: OwnedUserId, + + /// The user's private master cross-signing key, once it has been set via + /// [`KeyQueryResponseTemplate::with_cross_signing_keys`]. + master_cross_signing_key: Option, + + /// The user's private self-signing key, once it has been set via + /// [`KeyQueryResponseTemplate::with_cross_signing_keys`]. + self_signing_key: Option, + + /// The user's private user-signing key, once it has been set via + /// [`KeyQueryResponseTemplate::with_cross_signing_keys`]. + user_signing_key: Option, + + /// The structured representation of the user's public master cross-signing + /// key, ready for return in the `/keys/query` response. + /// + /// This starts off as `None`, but is populated with the correct + /// object when the master key is set. It accumulates additional + /// signatures when the key is cross-signed + /// via [`KeyQueryResponseTemplate::with_user_verification_signature`]. + master_cross_signing_key_json: Option, + + /// The JSON object containing the public, signed, device keys, added via + /// [`KeyQueryResponseTemplate::with_device`]. + device_keys: BTreeMap>, +} + +impl KeyQueryResponseTemplate { + /// Create a new [`KeyQueryResponseTemplate`] for the given user. + pub fn new(user_id: OwnedUserId) -> Self { + KeyQueryResponseTemplate { + user_id, + master_cross_signing_key: None, + self_signing_key: None, + user_signing_key: None, + master_cross_signing_key_json: None, + device_keys: Default::default(), + } + } + + /// Add a set of cross-signing keys to the data to be returned. + /// + /// The private keys must be provided here so that signatures can be + /// correctly calculated. + pub fn with_cross_signing_keys( + mut self, + master_cross_signing_key: Ed25519SecretKey, + self_signing_key: Ed25519SecretKey, + user_signing_key: Ed25519SecretKey, + ) -> Self { + let master_public_key = master_cross_signing_key.public_key(); + self.master_cross_signing_key = Some(master_cross_signing_key); + self.self_signing_key = Some(self_signing_key); + self.user_signing_key = Some(user_signing_key); + + // For the master key, we build the CrossSigningKey object upfront, so that we + // can start to accumulate signatures. For the other keys, we generate + // the JSON representation on-demand. + self.master_cross_signing_key_json = + Some(self.signed_cross_signing_key(&master_public_key, KeyUsage::Master)); + + self + } + + /// Add a device to the data to be returned. + /// + /// As well as a device ID and public Curve25519 device key, the *private* + /// Ed25519 device key must be provided so that the signature can be + /// calculated. + /// + /// The device can optionally be signed by the self-signing key by setting + /// `cross_signed` to `true`. + pub fn with_device( + mut self, + device_id: &DeviceId, + curve25519_public_key: &Curve25519PublicKey, + ed25519_secret_key: &Ed25519SecretKey, + cross_signed: bool, + ) -> Self { + let mut device_keys = json!({ + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": device_id.to_owned(), + "keys": { + format!("curve25519:{device_id}"): curve25519_public_key.to_base64(), + format!("ed25519:{device_id}"): ed25519_secret_key.public_key().to_base64(), + }, + "signatures": {}, + "user_id": self.user_id.clone(), + }); + + sign_json(&mut device_keys, ed25519_secret_key, &self.user_id, device_id.as_str()); + if cross_signed { + let ssk = self + .self_signing_key + .as_ref() + .expect("must call with_cross_signing_keys() before creating cross-signed device"); + sign_json(&mut device_keys, ssk, &self.user_id, &ssk.public_key().to_base64()); + } + + let raw_device_keys = serde_json::from_value(device_keys).unwrap(); + self.device_keys.insert(device_id.to_owned(), raw_device_keys); + self + } + + /// Add the signature from another user to our master key, as would happen + /// if that user had verified us. + pub fn with_user_verification_signature( + mut self, + signing_user_id: &UserId, + signing_user_user_signing_key: &Ed25519SecretKey, + ) -> Self { + let master_key = self.master_cross_signing_key_json.as_mut().expect( + "must call with_cross_signing_key() before calling 'with_user_verification_signature'", + ); + sign_cross_signing_key(master_key, signing_user_user_signing_key, signing_user_id); + self + } + + /// Build a `/keys/query` response containing this user's data. + pub fn build_response(&self) -> KeyQueryResponse { + let mut response = KeyQueryResponse::default(); + + if !self.device_keys.is_empty() { + response.device_keys = + BTreeMap::from([(self.user_id.clone(), self.device_keys.clone())]); + } + + if let Some(master_key) = &self.master_cross_signing_key_json { + response.master_keys.insert( + self.user_id.clone(), + Raw::new(master_key).expect("unable to serialize msk"), + ); + } + + if let Some(self_signing_key) = &self.self_signing_key { + let ssk = self + .signed_cross_signing_key(&self_signing_key.public_key(), KeyUsage::SelfSigning); + response + .self_signing_keys + .insert(self.user_id.clone(), Raw::new(&ssk).expect("unable to serialize ssk")); + } + + if let Some(user_signing_key) = &self.user_signing_key { + let usk = self + .signed_cross_signing_key(&user_signing_key.public_key(), KeyUsage::UserSigning); + response + .user_signing_keys + .insert(self.user_id.clone(), Raw::new(&usk).expect("unable to serialize usk")); + } + + response + } + + /// Build a [`CrossSigningKey`] structure for part of a `/keys/query` + /// response. + /// + /// Such a structure represents one of the three public cross-signing keys, + /// and is always signed by (at least) our master key. + /// + /// # Arguments + /// + /// - `public_key`: the public Ed25519 key to be returned by `/keys/query`. + /// - `key_usage`: an indicator of whether this will be the master, + /// user-signing, or self-signing key. + fn signed_cross_signing_key( + &self, + public_key: &Ed25519PublicKey, + key_usage: KeyUsage, + ) -> CrossSigningKey { + let public_key_base64 = OwnedBase64PublicKey::with_bytes(public_key.as_bytes()); + let mut key = CrossSigningKey::new( + self.user_id.clone(), + vec![key_usage], + BTreeMap::from([( + CrossSigningKeyId::from_parts(SigningKeyAlgorithm::Ed25519, &public_key_base64), + public_key_base64.to_string(), + )]), + CrossSigningOrDeviceSignatures::new(), + ); + + // Sign with our master key. + let master_key = self + .master_cross_signing_key + .as_ref() + .expect("must set master key before calling `signed_cross_signing_key`"); + sign_cross_signing_key(&mut key, master_key, &self.user_id); + + key + } +} + /// This set of keys/query response was generated using a local synapse. /// Each users was created, device added according to needs and the payload /// of the keys query have been copy/pasted here. @@ -45,60 +252,21 @@ impl KeyDistributionTestData { pub const USER_SIGNING_KEY_PRIVATE_EXPORT: &'static str = "zQSosK46giUFs2ACsaf32bA7drcIXbmViyEt+TLfloI"; + /// Current user's private user-signing key, as an [`Ed25519SecretKey`]. + pub fn me_private_user_signing_key() -> Ed25519SecretKey { + Ed25519SecretKey::from_base64(Self::USER_SIGNING_KEY_PRIVATE_EXPORT).unwrap() + } + /// Current user keys query response containing the cross-signing keys pub fn me_keys_query_response() -> KeyQueryResponse { - let data = json!({ - "master_keys": { - "@me:localhost": { - "keys": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4" - }, - "signatures": { - "@me:localhost": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "5G9+Ns28rzNd+2DvP73Y0orr8sxduRQcrJj0YB7ZygH7oeXshvGLeQn6mcNs7q7ZrMR5bYlXxopufKSWWoKpCg", - } - }, - "usage": [ - "master" - ], - "user_id": "@me:localhost" - } - }, - "self_signing_keys": { - "@me:localhost": { - "keys": { - "ed25519:9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw": "9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw" - }, - "signatures": { - "@me:localhost": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "amiKDLpWIwUQPzq+eov6KJsoskkWA1YzrGNb7HF3OcGV0nm4t7df0tUdZB/OpREtT5D78BKtzOPUipde2DxUAw" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@me:localhost" - } - }, - "user_signing_keys": { - "@me:localhost": { - "keys": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY" - }, - "signatures": { - "@me:localhost": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "Cv56vTHAzRkvdcELleOlhECZQP0pXcikCdEZrnXbkjXQ/k0ZvVOJ1beG/SiH8xc6zh1bCIMYv96C9p8o+7VZCQ" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@me:localhost" - } - } - }); + let builder = KeyQueryResponseTemplate::new(Self::me_id().to_owned()) + .with_cross_signing_keys( + Ed25519SecretKey::from_base64(Self::MASTER_KEY_PRIVATE_EXPORT).unwrap(), + Ed25519SecretKey::from_base64(Self::SELF_SIGNING_KEY_PRIVATE_EXPORT).unwrap(), + Self::me_private_user_signing_key(), + ); - let response: KeyQueryResponse = ruma_response_from_json(&data); + let response = builder.build_response(); with_settings!({sort_maps => true}, { assert_json_snapshot!( "KeyDistributionTestData::me_keys_query_response", @@ -1297,3 +1465,93 @@ impl MaloIdentityChangeDataSet { ruma_response_from_json(&data) } } + +/// Calculate the signature for a JSON object, without adding that signature to +/// the object. +/// +/// # Arguments +/// +/// * `value` - the JSON object to be signed. +/// * `signing_key` - the Ed25519 key to sign with. +fn calculate_json_signature(mut value: Value, signing_key: &Ed25519SecretKey) -> Ed25519Signature { + // strip `unsigned` and any existing signatures + let json_object = value.as_object_mut().expect("value must be object"); + json_object.remove("signatures"); + json_object.remove("unsigned"); + + let canonical_json: CanonicalJsonValue = + value.try_into().expect("could not convert to canonicaljson"); + + // do the signing + signing_key.sign(canonical_json.to_string().as_ref()) +} + +/// Add a signature to a JSON object, following the Matrix JSON-signing spec (https://spec.matrix.org/v1.12/appendices/#signing-details). +/// +/// # Arguments +/// +/// * `value` - the JSON object to be signed. +/// * `signing_key` - the Ed25519 key to sign with. +/// * `user_id` - the user doing the signing. This will be used to add the +/// signature to the object. +/// * `key_identifier` - the name of the key being used to sign with, +/// *excluding* the `ed25519` prefix. +/// +/// # Panics +/// +/// If the JSON value passed in is not an object, or contains a non-object +/// `signatures` property. +fn sign_json( + value: &mut Value, + signing_key: &Ed25519SecretKey, + user_id: &UserId, + key_identifier: &str, +) { + let signature = calculate_json_signature(value.clone(), signing_key); + + let value_obj = value.as_object_mut().expect("value must be object"); + + let signatures_obj = value_obj + .entry("signatures") + .or_insert_with(|| serde_json::Map::new().into()) + .as_object_mut() + .expect("signatures key must be object"); + + let user_signatures_obj = signatures_obj + .entry(user_id.to_string()) + .or_insert_with(|| serde_json::Map::new().into()) + .as_object_mut() + .expect("signatures keys must be object"); + + user_signatures_obj.insert(format!("ed25519:{key_identifier}"), signature.to_base64().into()); +} + +/// Add a signature to a [`CrossSigningKey`] object. +/// +/// This is similar to [`sign_json`], but operates on the deserialized +/// [`CrossSigningKey`] object rather than the serialized JSON. +/// +/// # Arguments +/// +/// * `value` - the [`CrossSigningKey`] object to be signed. +/// * `signing_key` - the Ed25519 key to sign with. +/// * `user_id` - the user doing the signing. This will be used to add the +/// signature to the object. +fn sign_cross_signing_key( + value: &mut CrossSigningKey, + signing_key: &Ed25519SecretKey, + user_id: &UserId, +) { + let key_json = serde_json::to_value(value.clone()).unwrap(); + let signature = calculate_json_signature(key_json, signing_key); + + // Poke the signature into the struct + let signing_key_id: OwnedBase64PublicKeyOrDeviceId = + OwnedBase64PublicKey::with_bytes(signing_key.public_key().as_bytes()).into(); + + value.signatures.insert_signature( + user_id.to_owned(), + CrossSigningOrDeviceSigningKeyId::from_parts(SigningKeyAlgorithm::Ed25519, &signing_key_id), + signature.to_base64(), + ); +} From 25ea5fdd73288af2d93d06d84dc1c28dd10528ce Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 8 Jan 2025 14:05:02 +0000 Subject: [PATCH 961/979] test: use builder for some more test data --- .../src/test_json/keys_query_sets.rs | 58 +++---------------- 1 file changed, 7 insertions(+), 51 deletions(-) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 1080a11345b..96b3cdb4efd 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -768,58 +768,14 @@ impl VerificationViolationTestData { /// `/keys/query` response for Alice, containing the public cross-signing /// keys. pub fn own_keys_query_response_1() -> KeyQueryResponse { - let data = json!({ - "master_keys": { - "@alice:localhost": { - "keys": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk" - }, - "signatures": { - "@alice:localhost": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "FX+srrw9SRmi12fexYHH1jrlEIWgOfre1aPNzDZWcAlaP9WKRdhcQGh70/3F9hk/PGr51I+ux62YgU4xnRTqAA", - } - }, - "usage": [ - "master" - ], - "user_id": "@alice:localhost" - } - }, - "self_signing_keys": { - "@alice:localhost": { - "keys": { - "ed25519:WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw": "WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw" - }, - "signatures": { - "@alice:localhost": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "lCV9R1xjD34arzq/CAuej1XBv+Ip4dFfAGHfe7znbW7rnwKDaX5PaX3MHk+EIC7nXvUYEAn502WcUFme5c0cCQ" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@alice:localhost" - } - }, - "user_signing_keys": { - "@alice:localhost": { - "keys": { - "ed25519:MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0": "MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0" - }, - "signatures": { - "@alice:localhost": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "A73QfZ5Dzhh7abdal/sEaq1bfgxzPFU8Bvwa9Y5TIe/a5jTmLVubNmsMSsO5tOT+b6aVJg1G4FtId0Q/cb1aAA" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@alice:localhost" - } - } - }); + let builder = KeyQueryResponseTemplate::new(Self::own_id().to_owned()) + .with_cross_signing_keys( + Ed25519SecretKey::from_base64(Self::MASTER_KEY_PRIVATE_EXPORT).unwrap(), + Ed25519SecretKey::from_base64(Self::SELF_SIGNING_KEY_PRIVATE_EXPORT).unwrap(), + Ed25519SecretKey::from_base64(Self::USER_SIGNING_KEY_PRIVATE_EXPORT).unwrap(), + ); - let response: KeyQueryResponse = ruma_response_from_json(&data); + let response = builder.build_response(); with_settings!({sort_maps => true}, { assert_json_snapshot!( "VerificationViolationTestData::own_keys_query_response_1", From 49748dbd4b0fe2572a9c5fd64dfcf6577b0f19d4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 7 Jan 2025 16:42:56 +0000 Subject: [PATCH 962/979] test: factor out common parts of `dan_keys_query_response{_loggedout}` --- .../src/test_json/keys_query_sets.rs | 137 +++++------------- 1 file changed, 38 insertions(+), 99 deletions(-) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 96b3cdb4efd..f23b1acb925 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -276,10 +276,45 @@ impl KeyDistributionTestData { response } - /// Dan has cross-signing setup, one device is cross signed `JHPUERYQUW`, - /// but not the other one `FRGNMZVOKA`. - /// `@dan` identity is signed by `@me` identity (alice trust dan) + /// Dan has cross-signing set up; one device is cross-signed (`JHPUERYQUW`), + /// but not the other one (`FRGNMZVOKA`). + /// + /// `@dan`'s identity is signed by `@me`'s identity (Alice trusts Dan). pub fn dan_keys_query_response() -> KeyQueryResponse { + let response = Self::dan_keys_query_response_common(); + with_settings!({sort_maps => true}, { + assert_json_snapshot!( + "KeyDistributionTestData::dan_keys_query_response", + ruma_response_to_json(response.clone()), + ); + }); + response + } + + /// Same as `dan_keys_query_response` but `FRGNMZVOKA` was removed. + pub fn dan_keys_query_response_device_loggedout() -> KeyQueryResponse { + let mut response = Self::dan_keys_query_response_common(); + response + .device_keys + .get_mut(Self::dan_id()) + .unwrap() + .remove(Self::dan_unsigned_device_id()); + + with_settings!({sort_maps => true}, { + assert_json_snapshot!( + "KeyDistributionTestData::dan_keys_query_response_device_loggedout", + ruma_response_to_json(response.clone()), + ); + }); + response + } + + /// Common helper for [`Self::dan_keys_query_response`] and + /// [`Self::dan_keys_query_response_device_loggedout`]. + /// + /// Returns the full response, including both devices, without writing the + /// snapshot. + fn dan_keys_query_response_common() -> KeyQueryResponse { let data: Value = json!({ "device_keys": { "@dan:localhost": { @@ -376,102 +411,6 @@ impl KeyDistributionTestData { }); let response: KeyQueryResponse = ruma_response_from_json(&data); - with_settings!({sort_maps => true}, { - assert_json_snapshot!( - "KeyDistributionTestData::dan_keys_query_response", - ruma_response_to_json(response.clone()), - ); - }); - response - } - - /// Same as `dan_keys_query_response` but `FRGNMZVOKA` was removed. - pub fn dan_keys_query_response_device_loggedout() -> KeyQueryResponse { - let data = json!({ - "device_keys": { - "@dan:localhost": { - "JHPUERYQUW": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "JHPUERYQUW", - "keys": { - "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", - "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" - }, - "signatures": { - "@dan:localhost": { - "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" - } - }, - "user_id": "@dan:localhost", - }, - } - }, - "failures": {}, - "master_keys": { - "@dan:localhost": { - "keys": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", - "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" - }, - "@me:localhost": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" - } - }, - "usage": [ - "master" - ], - "user_id": "@dan:localhost" - } - }, - "self_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@dan:localhost" - } - }, - "user_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@dan:localhost" - } - } - }); - - let response: KeyQueryResponse = ruma_response_from_json(&data); - with_settings!({sort_maps => true}, { - assert_json_snapshot!( - "KeyDistributionTestData::dan_keys_query_response_device_loggedout", - ruma_response_to_json(response.clone()), - ); - }); response } From 47f8b32ea1a138443f477f331280a0e7fa7e84e5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 8 Jan 2025 11:33:32 +0000 Subject: [PATCH 963/979] test: give Dan new keys Regenerate Dan's data with new cross-signing and device keys, for which I know the private keys. The signatures are manually calculated for now; this will be improved in a later commit. --- .../src/test_json/keys_query_sets.rs | 40 +++++++++++++------ ...tionTestData::dan_keys_query_response.snap | 25 ++++++------ ..._keys_query_response_device_loggedout.snap | 21 +++++----- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index f23b1acb925..36eabe792ec 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -276,6 +276,18 @@ impl KeyDistributionTestData { response } + /// Dan's (base64-encoded) private master cross-signing key. + const DAN_PRIVATE_MASTER_CROSS_SIGNING_KEY: &'static str = + "QGZo39k199RM0NYvPvFNXBspc5llftHWKKHqEi25q0U"; + + /// Dan's (base64-encoded) private self-signing key. + const DAN_PRIVATE_SELF_SIGNING_KEY: &'static str = + "0ES1HO5VXpy/BsXxadwsk6QcwH/ci99KkV9ZlPakHlU"; + + /// Dan's (base64-encoded) private user-signing key. + const DAN_PRIVATE_USER_SIGNING_KEY: &'static str = + "vSdfrHJO8sZH/54r1uCg8BE0CdcDVGkPQNOu7Ej8BBs"; + /// Dan has cross-signing set up; one device is cross-signed (`JHPUERYQUW`), /// but not the other one (`FRGNMZVOKA`). /// @@ -326,16 +338,18 @@ impl KeyDistributionTestData { "device_id": "JHPUERYQUW", "keys": { "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", - "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" + // Private key: "yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM" + "ed25519:JHPUERYQUW": "+qMutlCB/eWCbI3bIskWhjYVGrRX8hF+F48sNsmg1YE" }, "signatures": { "@dan:localhost": { - "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" + "ed25519:JHPUERYQUW": "I9mcfT2BIwbWfga1P85rdbhEDh5qc/3pLgY8jsqlzXbjl4AfHKBZUvcgQ54kKFOf/jK9pTK2Ed35cDQ1QZ44Cw", + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "msmrAwToOpBmSLeWyThuV7vS7fXGB291C3KUrM+2a0L6CS6pqpK12nBZG/dLeDzVSAamyWjB3OuwhTl0kE7UCA" } }, "user_id": "@dan:localhost", }, + "FRGNMZVOKA": { "algorithms": [ "m.olm.v1.curve25519-aes-sha2", @@ -344,11 +358,12 @@ impl KeyDistributionTestData { "device_id": "FRGNMZVOKA", "keys": { "curve25519:FRGNMZVOKA": "Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4", - "ed25519:FRGNMZVOKA": "jVroR0JoRemjF0vJslY3HirJgwfX5gm5DCM64hZgkI0" + // Private key: "/SlFtNKxTPN+i4pHzSPWZ1Oc6ymMB33sS32GXZkaLos" + "ed25519:FRGNMZVOKA": "xp/IW9Sh8Jw/buUYlARWD20EV2TdUG/SZ+Pa4iEtcew" }, "signatures": { "@dan:localhost": { - "ed25519:FRGNMZVOKA": "+row23EcWR2D8EKgwzZmy3dWz/l5DHvEHR6jHKnBohphEIsBl0o3Cp9rIztFpStFGRPSAa3xEqfMVW2dIaKkCg" + "ed25519:FRGNMZVOKA": "G6f8s4a3rXOnODPdSQTUOjJ0YtxlxwDSTPNkzbAMHEwdmnmUuFhTdEYNP/dzDDGd8kViWpMteaPqvlAKIlvlBg" } }, "user_id": "@dan:localhost", @@ -359,15 +374,14 @@ impl KeyDistributionTestData { "master_keys": { "@dan:localhost": { "keys": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", - "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "WiSxmg/RpT8BTHcLvNS7vgKyonsRpX/K6E9EzEfLpNOxXfisPClbpuVtxEb3te/Cx/1UKaio1MJcDxqKFMVmDw" }, "@me:localhost": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "lSa34sDcviLPgk8yECGxwdSt2074sf2R8uSmxpTuK5NBqv9dpu0NwTJkO4PLohQTvleIYZSH+uXc3mPZpFy8DQ" } }, "usage": [ @@ -379,11 +393,11 @@ impl KeyDistributionTestData { "self_signing_keys": { "@dan:localhost": { "keys": { - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZKjShHHjXbnkhAUk6P2c03FsUr4wB+3xH7llKV/7QD5wuOapgM7OucABCixivA6xiUVWPGjzZ76y6DFG6YloAA" } }, "usage": [ @@ -395,11 +409,11 @@ impl KeyDistributionTestData { "user_signing_keys": { "@dan:localhost": { "keys": { - "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" + "ed25519:2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk": "2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "bsjJnBMMYcRwcNysPhW9r8jOXuui7otHJH1/clnf7jhEHzj2v8ei4IjZaKLvodFdNLyl/DQE5eOKlhCgt5ekDQ" } }, "usage": [ diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap index cbba19a00e2..abc677cf92f 100644 --- a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap @@ -13,11 +13,11 @@ expression: ruma_response_to_json(response.clone()) "device_id": "FRGNMZVOKA", "keys": { "curve25519:FRGNMZVOKA": "Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4", - "ed25519:FRGNMZVOKA": "jVroR0JoRemjF0vJslY3HirJgwfX5gm5DCM64hZgkI0" + "ed25519:FRGNMZVOKA": "xp/IW9Sh8Jw/buUYlARWD20EV2TdUG/SZ+Pa4iEtcew" }, "signatures": { "@dan:localhost": { - "ed25519:FRGNMZVOKA": "+row23EcWR2D8EKgwzZmy3dWz/l5DHvEHR6jHKnBohphEIsBl0o3Cp9rIztFpStFGRPSAa3xEqfMVW2dIaKkCg" + "ed25519:FRGNMZVOKA": "G6f8s4a3rXOnODPdSQTUOjJ0YtxlxwDSTPNkzbAMHEwdmnmUuFhTdEYNP/dzDDGd8kViWpMteaPqvlAKIlvlBg" } }, "user_id": "@dan:localhost" @@ -30,12 +30,12 @@ expression: ruma_response_to_json(response.clone()) "device_id": "JHPUERYQUW", "keys": { "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", - "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" + "ed25519:JHPUERYQUW": "+qMutlCB/eWCbI3bIskWhjYVGrRX8hF+F48sNsmg1YE" }, "signatures": { "@dan:localhost": { - "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "msmrAwToOpBmSLeWyThuV7vS7fXGB291C3KUrM+2a0L6CS6pqpK12nBZG/dLeDzVSAamyWjB3OuwhTl0kE7UCA", + "ed25519:JHPUERYQUW": "I9mcfT2BIwbWfga1P85rdbhEDh5qc/3pLgY8jsqlzXbjl4AfHKBZUvcgQ54kKFOf/jK9pTK2Ed35cDQ1QZ44Cw" } }, "user_id": "@dan:localhost" @@ -45,15 +45,14 @@ expression: ruma_response_to_json(response.clone()) "master_keys": { "@dan:localhost": { "keys": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", - "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "WiSxmg/RpT8BTHcLvNS7vgKyonsRpX/K6E9EzEfLpNOxXfisPClbpuVtxEb3te/Cx/1UKaio1MJcDxqKFMVmDw" }, "@me:localhost": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "lSa34sDcviLPgk8yECGxwdSt2074sf2R8uSmxpTuK5NBqv9dpu0NwTJkO4PLohQTvleIYZSH+uXc3mPZpFy8DQ" } }, "usage": [ @@ -65,11 +64,11 @@ expression: ruma_response_to_json(response.clone()) "self_signing_keys": { "@dan:localhost": { "keys": { - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZKjShHHjXbnkhAUk6P2c03FsUr4wB+3xH7llKV/7QD5wuOapgM7OucABCixivA6xiUVWPGjzZ76y6DFG6YloAA" } }, "usage": [ @@ -81,11 +80,11 @@ expression: ruma_response_to_json(response.clone()) "user_signing_keys": { "@dan:localhost": { "keys": { - "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" + "ed25519:2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk": "2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "bsjJnBMMYcRwcNysPhW9r8jOXuui7otHJH1/clnf7jhEHzj2v8ei4IjZaKLvodFdNLyl/DQE5eOKlhCgt5ekDQ" } }, "usage": [ diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap index e2a38389278..9bcf722fc51 100644 --- a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap @@ -13,12 +13,12 @@ expression: ruma_response_to_json(response.clone()) "device_id": "JHPUERYQUW", "keys": { "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", - "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" + "ed25519:JHPUERYQUW": "+qMutlCB/eWCbI3bIskWhjYVGrRX8hF+F48sNsmg1YE" }, "signatures": { "@dan:localhost": { - "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "msmrAwToOpBmSLeWyThuV7vS7fXGB291C3KUrM+2a0L6CS6pqpK12nBZG/dLeDzVSAamyWjB3OuwhTl0kE7UCA", + "ed25519:JHPUERYQUW": "I9mcfT2BIwbWfga1P85rdbhEDh5qc/3pLgY8jsqlzXbjl4AfHKBZUvcgQ54kKFOf/jK9pTK2Ed35cDQ1QZ44Cw" } }, "user_id": "@dan:localhost" @@ -28,15 +28,14 @@ expression: ruma_response_to_json(response.clone()) "master_keys": { "@dan:localhost": { "keys": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", - "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "WiSxmg/RpT8BTHcLvNS7vgKyonsRpX/K6E9EzEfLpNOxXfisPClbpuVtxEb3te/Cx/1UKaio1MJcDxqKFMVmDw" }, "@me:localhost": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "lSa34sDcviLPgk8yECGxwdSt2074sf2R8uSmxpTuK5NBqv9dpu0NwTJkO4PLohQTvleIYZSH+uXc3mPZpFy8DQ" } }, "usage": [ @@ -48,11 +47,11 @@ expression: ruma_response_to_json(response.clone()) "self_signing_keys": { "@dan:localhost": { "keys": { - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZKjShHHjXbnkhAUk6P2c03FsUr4wB+3xH7llKV/7QD5wuOapgM7OucABCixivA6xiUVWPGjzZ76y6DFG6YloAA" } }, "usage": [ @@ -64,11 +63,11 @@ expression: ruma_response_to_json(response.clone()) "user_signing_keys": { "@dan:localhost": { "keys": { - "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" + "ed25519:2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk": "2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk" }, "signatures": { "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "bsjJnBMMYcRwcNysPhW9r8jOXuui7otHJH1/clnf7jhEHzj2v8ei4IjZaKLvodFdNLyl/DQE5eOKlhCgt5ekDQ" } }, "usage": [ From 3a3cc54067f8586dd34f3080408f3b5d6ac5ebf7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 8 Jan 2025 00:46:24 +0000 Subject: [PATCH 964/979] test: generate dan's data dynamically --- .../src/test_json/keys_query_sets.rs | 124 ++++-------------- 1 file changed, 25 insertions(+), 99 deletions(-) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 36eabe792ec..acf1c873bb9 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -219,8 +219,6 @@ impl KeyQueryResponseTemplate { } /// This set of keys/query response was generated using a local synapse. -/// Each users was created, device added according to needs and the payload -/// of the keys query have been copy/pasted here. /// /// The current user is `@me:localhost`, the private part of the /// cross-signing keys have been exported using the console with the @@ -327,105 +325,33 @@ impl KeyDistributionTestData { /// Returns the full response, including both devices, without writing the /// snapshot. fn dan_keys_query_response_common() -> KeyQueryResponse { - let data: Value = json!({ - "device_keys": { - "@dan:localhost": { - "JHPUERYQUW": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "JHPUERYQUW", - "keys": { - "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", - // Private key: "yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM" - "ed25519:JHPUERYQUW": "+qMutlCB/eWCbI3bIskWhjYVGrRX8hF+F48sNsmg1YE" - }, - "signatures": { - "@dan:localhost": { - "ed25519:JHPUERYQUW": "I9mcfT2BIwbWfga1P85rdbhEDh5qc/3pLgY8jsqlzXbjl4AfHKBZUvcgQ54kKFOf/jK9pTK2Ed35cDQ1QZ44Cw", - "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "msmrAwToOpBmSLeWyThuV7vS7fXGB291C3KUrM+2a0L6CS6pqpK12nBZG/dLeDzVSAamyWjB3OuwhTl0kE7UCA" - } - }, - "user_id": "@dan:localhost", - }, + let builder = KeyQueryResponseTemplate::new(Self::dan_id().to_owned()) + .with_cross_signing_keys( + Ed25519SecretKey::from_base64(Self::DAN_PRIVATE_MASTER_CROSS_SIGNING_KEY).unwrap(), + Ed25519SecretKey::from_base64(Self::DAN_PRIVATE_SELF_SIGNING_KEY).unwrap(), + Ed25519SecretKey::from_base64(Self::DAN_PRIVATE_USER_SIGNING_KEY).unwrap(), + ) + .with_user_verification_signature(Self::me_id(), &Self::me_private_user_signing_key()); + + // Add signed device JHPUERYQUW + let builder = builder.with_device( + Self::dan_signed_device_id(), + &Curve25519PublicKey::from_base64("PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4") + .unwrap(), + &Ed25519SecretKey::from_base64("yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM").unwrap(), + true, + ); - "FRGNMZVOKA": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "FRGNMZVOKA", - "keys": { - "curve25519:FRGNMZVOKA": "Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4", - // Private key: "/SlFtNKxTPN+i4pHzSPWZ1Oc6ymMB33sS32GXZkaLos" - "ed25519:FRGNMZVOKA": "xp/IW9Sh8Jw/buUYlARWD20EV2TdUG/SZ+Pa4iEtcew" - }, - "signatures": { - "@dan:localhost": { - "ed25519:FRGNMZVOKA": "G6f8s4a3rXOnODPdSQTUOjJ0YtxlxwDSTPNkzbAMHEwdmnmUuFhTdEYNP/dzDDGd8kViWpMteaPqvlAKIlvlBg" - } - }, - "user_id": "@dan:localhost", - }, - } - }, - "failures": {}, - "master_keys": { - "@dan:localhost": { - "keys": { - "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo" - }, - "signatures": { - "@dan:localhost": { - "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "WiSxmg/RpT8BTHcLvNS7vgKyonsRpX/K6E9EzEfLpNOxXfisPClbpuVtxEb3te/Cx/1UKaio1MJcDxqKFMVmDw" - }, - "@me:localhost": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "lSa34sDcviLPgk8yECGxwdSt2074sf2R8uSmxpTuK5NBqv9dpu0NwTJkO4PLohQTvleIYZSH+uXc3mPZpFy8DQ" - } - }, - "usage": [ - "master" - ], - "user_id": "@dan:localhost" - } - }, - "self_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA" - }, - "signatures": { - "@dan:localhost": { - "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZKjShHHjXbnkhAUk6P2c03FsUr4wB+3xH7llKV/7QD5wuOapgM7OucABCixivA6xiUVWPGjzZ76y6DFG6YloAA" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@dan:localhost" - } - }, - "user_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk": "2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk" - }, - "signatures": { - "@dan:localhost": { - "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "bsjJnBMMYcRwcNysPhW9r8jOXuui7otHJH1/clnf7jhEHzj2v8ei4IjZaKLvodFdNLyl/DQE5eOKlhCgt5ekDQ" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@dan:localhost" - } - } - }); + // Add unsigned device FRGNMZVOKA + let builder = builder.with_device( + Self::dan_unsigned_device_id(), + &Curve25519PublicKey::from_base64("Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4") + .unwrap(), + &Ed25519SecretKey::from_base64("/SlFtNKxTPN+i4pHzSPWZ1Oc6ymMB33sS32GXZkaLos").unwrap(), + false, + ); - let response: KeyQueryResponse = ruma_response_from_json(&data); - response + builder.build_response() } /// Dave is a user that has not enabled cross-signing From fe3cc09ae0a9226d26c8a0d41fe545a74f189328 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 14 Jan 2025 16:23:28 +0000 Subject: [PATCH 965/979] test: add examples for the new builder --- .../src/test_json/keys_query_sets.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index acf1c873bb9..6410a8d74c8 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -21,6 +21,71 @@ use crate::{ /// A test helper for building test data sets for `/keys/query` response objects /// ([`KeyQueryResponse`]). +/// +/// # Examples +/// +/// A simple case with no cross-signing identity and a single device: +/// +/// ``` +/// # use ruma::{device_id, owned_user_id}; +/// # use vodozemac::{Curve25519PublicKey, Ed25519SecretKey}; +/// # use matrix_sdk_test::test_json::keys_query_sets::KeyQueryResponseTemplate; +/// +/// let pub_curve_key = "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4"; +/// let ed25519_key = "yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM"; +/// +/// let builder = KeyQueryResponseTemplate::new(owned_user_id!("@alice:localhost")) +/// .with_device( +/// device_id!("TESTDEVICE"), +/// &Curve25519PublicKey::from_base64(pub_curve_key).unwrap(), +/// &Ed25519SecretKey::from_base64(ed25519_key).unwrap(), +/// false, +/// ); +/// +/// let response = builder.build_response(); +/// ``` +/// +/// +/// A more complex case, with cross-signing keys and a signed device: +/// +/// ``` +/// # use ruma::{device_id, owned_user_id, user_id}; +/// # use vodozemac::{Curve25519PublicKey, Ed25519SecretKey}; +/// # use matrix_sdk_test::test_json::keys_query_sets::KeyQueryResponseTemplate; +/// +/// // Private cross-signing keys +/// let master_key = "QGZo39k199RM0NYvPvFNXBspc5llftHWKKHqEi25q0U"; +/// let ssk = "0ES1HO5VXpy/BsXxadwsk6QcwH/ci99KkV9ZlPakHlU"; +/// let usk = "vSdfrHJO8sZH/54r1uCg8BE0CdcDVGkPQNOu7Ej8BBs"; +/// +/// // Device keys +/// let pub_curve_key = "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4"; +/// let ed25519_key = "yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM"; +/// +/// let other_user_usk = "zQSosK46giUFs2ACsaf32bA7drcIXbmViyEt+TLfloI"; +/// +/// let builder = KeyQueryResponseTemplate::new(owned_user_id!("@me:localhost")) +/// // add cross-signing keys +/// .with_cross_signing_keys( +/// Ed25519SecretKey::from_base64(master_key).unwrap(), +/// Ed25519SecretKey::from_base64(ssk).unwrap(), +/// Ed25519SecretKey::from_base64(usk).unwrap(), +/// ) +/// // add verification from another user +/// .with_user_verification_signature( +/// user_id!("@them:localhost"), +/// &Ed25519SecretKey::from_base64(other_user_usk).unwrap(), +/// ) +/// // add signed device +/// .with_device( +/// device_id!("SECUREDEVICE"), +/// &Curve25519PublicKey::from_base64(pub_curve_key).unwrap(), +/// &Ed25519SecretKey::from_base64(ed25519_key).unwrap(), +/// true, +/// ); +/// +/// let response = builder.build_response(); +/// ``` pub struct KeyQueryResponseTemplate { /// The User ID of the user that this test data is about. user_id: OwnedUserId, From 8bd94318c05532ac3c0c52dd3c84c8578928ecab Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 15 Jan 2025 12:27:40 +0000 Subject: [PATCH 966/979] fix(tests): Fix a flaky test by marking a room's members as synced. This is intended to prevent the test `test_when_user_in_verification_violation_becomes_verified_we_report_it` flaking. I found that sometimes when it called `Room::members` the result was empty due to it trying to fetch the answer from the server. This change prevents that behaviour. I don't know why the behaviour was inconsistent before. --- crates/matrix-sdk-base/src/rooms/normal.rs | 11 +++++++++++ crates/matrix-sdk/src/room/identity_status_changes.rs | 2 ++ 2 files changed, 13 insertions(+) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 3a1b8a61d75..b393ddac9f1 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -391,6 +391,17 @@ impl Room { self.inner.read().members_synced } + /// Mark this Room as holding all member information. + /// + /// Useful in tests if we want to persuade the Room not to sync when asked + /// about its members. + #[cfg(feature = "testing")] + pub fn mark_members_synced(&self) { + self.inner.update(|info| { + info.members_synced = true; + }); + } + /// Mark this Room as still missing member information. pub fn mark_members_missing(&self) { self.inner.update_if(|info| { diff --git a/crates/matrix-sdk/src/room/identity_status_changes.rs b/crates/matrix-sdk/src/room/identity_status_changes.rs index 6e7a8602d39..7c86905a306 100644 --- a/crates/matrix-sdk/src/room/identity_status_changes.rs +++ b/crates/matrix-sdk/src/room/identity_status_changes.rs @@ -917,6 +917,8 @@ mod tests { .build_sync_response(); client.process_sync(create_room_sync_response).await.unwrap(); let room = client.get_room(&DEFAULT_TEST_ROOM_ID).expect("Room should exist"); + room.inner.mark_members_synced(); + assert_eq!(room.state(), RoomState::Joined); assert_eq!( *room From de7397a20eb05e059d3978fb25c44d306263f5e9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 15 Jan 2025 11:48:19 +0100 Subject: [PATCH 967/979] feat(event cache): handle redacted redactions in the `AllEventsCache` This is unlikely that it will affect us, so not worth adding a test IMO, but for the sake of completeness: this handles redacted redactions in the `AllEventsCache` too. --- crates/matrix-sdk/src/event_cache/mod.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index d41c54d88bd..e1c58dc0082 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -381,13 +381,23 @@ impl AllEventsCache { }; // Handle redactions separately, as their logic is slightly different. - if let AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(ev)) = &ev { - if let Some(redacted_event_id) = ev.content.redacts.as_ref().or(ev.redacts.as_ref()) { + if let AnySyncMessageLikeEvent::RoomRedaction(room_redaction) = &ev { + let redacted_event_id = match room_redaction { + SyncRoomRedactionEvent::Original(ev) => { + ev.content.redacts.as_ref().or(ev.redacts.as_ref()) + } + SyncRoomRedactionEvent::Redacted(redacted_redaction) => { + redacted_redaction.content.redacts.as_ref() + } + }; + + if let Some(redacted_event_id) = redacted_event_id { self.relations .entry(redacted_event_id.to_owned()) .or_default() - .insert(ev.event_id.to_owned(), RelationType::Replacement); + .insert(ev.event_id().to_owned(), RelationType::Replacement); } + return; } From 560e582e41de592bbb3200e0f3b27d4ac5f478d6 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 16 Jan 2025 10:37:22 +0100 Subject: [PATCH 968/979] test: get rid of `mock_redaction` and replace it with the holy MatrixMockServer --- .../tests/integration/timeline/mod.rs | 75 +++++++------------ .../tests/integration/timeline/reactions.rs | 56 +++++++------- .../tests/integration/room/joined.rs | 12 +-- testing/matrix-sdk-test/src/mocks.rs | 12 --- 4 files changed, 58 insertions(+), 97 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 570323f4e8c..303bd0ae0fd 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -19,14 +19,13 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::{ - assert_let_timeout, config::SyncSettings, test_utils::logged_in_client_with_server, + assert_let_timeout, + config::SyncSettings, + test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, }; use matrix_sdk_test::{ - async_test, - event_factory::EventFactory, - mocks::{mock_encryption_state, mock_redaction}, - sync_timeline_event, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, - SyncResponseBuilder, BOB, + async_test, event_factory::EventFactory, mocks::mock_encryption_state, sync_timeline_event, + JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, SyncResponseBuilder, BOB, }; use matrix_sdk_ui::{ timeline::{ @@ -234,35 +233,28 @@ async fn test_redacted_message() { #[async_test] async fn test_redact_message() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = server.sync_joined_room(&client, room_id).await; - mock_encryption_state(&server, false).await; + server.mock_room_state_encryption().plain().mount().await; - let room = client.get_room(room_id).unwrap(); let timeline = room.timeline().await.unwrap(); let (_, mut timeline_stream) = timeline.subscribe().await; let factory = EventFactory::new(); factory.set_next_ts(MilliSecondsSinceUnixEpoch::now().get().into()); - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event( - factory.sender(user_id!("@a:b.com")).text_msg("buy my bitcoins bro"), - ), - ); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event( + factory.sender(user_id!("@a:b.com")).text_msg("buy my bitcoins bro"), + ), + ) + .await; assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); assert_eq!( @@ -274,7 +266,7 @@ async fn test_redact_message() { assert!(date_divider.is_date_divider()); // Redacting a remote event works. - mock_redaction(event_id!("$42")).mount(&server).await; + server.mock_room_redact().ok(event_id!("$42")).mock_once().mount().await; timeline.redact(&first.as_event().unwrap().identifier(), Some("inapprops")).await.unwrap(); @@ -306,34 +298,19 @@ async fn test_redact_message() { #[async_test] async fn test_redact_local_sent_message() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = server.sync_joined_room(&client, room_id).await; - mock_encryption_state(&server, false).await; + server.mock_room_state_encryption().plain().mount().await; - let room = client.get_room(room_id).unwrap(); let timeline = room.timeline().await.unwrap(); let (_, mut timeline_stream) = timeline.subscribe().await; // Mock event sending. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({ "event_id": "$wWgymRfo7ri1uQx0NXO40vLJ" })), - ) - .expect(1) - .mount(&server) - .await; + server.mock_room_send().ok(event_id!("$wWgymRfo7ri1uQx0NXO40vLJ")).mock_once().mount().await; // Send the event so it's added to the send queue as a local event. timeline @@ -364,7 +341,7 @@ async fn test_redact_local_sent_message() { // Mock the redaction response for the event we just sent. Ensure it's called // once. - mock_redaction(event.event_id().unwrap()).expect(1).mount(&server).await; + server.mock_room_redact().ok(event_id!("$redaction_event_id")).mock_once().mount().await; // Let's redact the local echo with the remote handle. timeline.redact(&event.identifier(), None).await.unwrap(); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs index fdc958f9af8..30cfef51000 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs @@ -17,12 +17,13 @@ use std::{sync::Mutex, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use eyeball_im::VectorDiff; use futures_util::{FutureExt as _, StreamExt as _}; -use matrix_sdk::{assert_next_matches_with_timeout, test_utils::logged_in_client_with_server}; +use matrix_sdk::{ + assert_next_matches_with_timeout, + test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, +}; use matrix_sdk_test::{ - async_test, - event_factory::EventFactory, - mocks::{mock_encryption_state, mock_redaction}, - JoinedRoomBuilder, SyncResponseBuilder, ALICE, + async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, + SyncResponseBuilder, ALICE, }; use matrix_sdk_ui::timeline::{ReactionStatus, RoomExt as _}; use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id}; @@ -39,21 +40,14 @@ async fn test_abort_before_being_sent() { // This test checks that a reaction could be aborted *before* or *while* it's // being sent by the send queue. - let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let user_id = client.user_id().unwrap(); - - // Make the test aware of the room. - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(Default::default()).await.unwrap(); - server.reset().await; + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = server.sync_joined_room(&client, room_id).await; - mock_encryption_state(&server, false).await; + server.mock_room_state_encryption().plain().mount().await; - let room = client.get_room(room_id).unwrap(); let timeline = room.timeline().await.unwrap(); let (initial_items, mut stream) = timeline.subscribe().await; @@ -62,14 +56,13 @@ async fn test_abort_before_being_sent() { let f = EventFactory::new(); let event_id = event_id!("$1"); - sync_builder.add_joined_room( - JoinedRoomBuilder::new(room_id) - .add_timeline_event(f.text_msg("hello").sender(&ALICE).event_id(event_id)), - ); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(Default::default()).await.unwrap(); - server.reset().await; + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("hello").sender(&ALICE).event_id(event_id)), + ) + .await; assert_let!(Some(VectorDiff::PushBack { value: first }) = stream.next().await); let item = first.as_event().unwrap(); @@ -82,9 +75,8 @@ async fn test_abort_before_being_sent() { // Now we try to add two reactions to this message… // Mock the send endpoint with a delay, to give us time to abort the sending. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + server + .mock_room_send() .respond_with( ResponseTemplate::new(200) .set_body_json(json!({ @@ -92,16 +84,18 @@ async fn test_abort_before_being_sent() { })) .set_delay(Duration::from_millis(150)), ) - .expect(1) + .mock_once() .named("send") - .mount(&server) + .mount() .await; - mock_redaction(event_id!("$3")).mount(&server).await; + server.mock_room_redact().ok(event_id!("$3")).mock_once().mount().await; // We add the reaction… timeline.toggle_reaction(&item_id, "👍").await.unwrap(); + let user_id = client.user_id().unwrap(); + // First toggle (local echo). { assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index e2ab2a568da..54b3935f342 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -16,7 +16,7 @@ use matrix_sdk_base::{RoomMembersUpdate, RoomState}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, - mocks::{mock_encryption_state, mock_redaction}, + mocks::mock_encryption_state, test_json::{self, sync::CUSTOM_ROOM_POWER_LEVELS}, EphemeralTestEvent, GlobalAccountDataTestEvent, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, @@ -348,12 +348,14 @@ async fn test_room_message_send() { #[async_test] async fn test_room_redact() { - let (client, server) = synced_client().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - let event_id = event_id!("$h29iv0s8:example.com"); - mock_redaction(event_id).mount(&server).await; + let room_id = *DEFAULT_TEST_ROOM_ID; + let room = server.sync_joined_room(&client, room_id).await; - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + let event_id = event_id!("$h29iv0s8:example.com"); + server.mock_room_redact().ok(event_id).mock_once().mount().await; let txn_id = TransactionId::new(); let reason = Some("Indecent material"); diff --git a/testing/matrix-sdk-test/src/mocks.rs b/testing/matrix-sdk-test/src/mocks.rs index d68829ed8b1..1fabb363683 100644 --- a/testing/matrix-sdk-test/src/mocks.rs +++ b/testing/matrix-sdk-test/src/mocks.rs @@ -14,8 +14,6 @@ //! Mocks useful to reuse across different testing contexts. -use ruma::EventId; -use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, Mock, MockServer, ResponseTemplate, @@ -23,16 +21,6 @@ use wiremock::{ use crate::test_json; -/// Mount a mock for a redaction endpoint, that will always work and return a -/// 200 response. -pub fn mock_redaction(event_id: &EventId) -> Mock { - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id }))) - .named("redact") -} - /// Mount a Mock on the given server to handle the `GET /// /rooms/.../state/m.room.encryption` endpoint with an option whether it /// should return an encryption event or not. From b3a789af90feef7c03ec422950f5554ff0bd652c Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 16 Jan 2025 10:48:48 +0100 Subject: [PATCH 969/979] test: get rid of the `synced_client` helper Not running a large sync with many events make for simpler test cases, with a more focused scope. --- crates/matrix-sdk/tests/integration/main.rs | 14 +------- .../tests/integration/room/joined.rs | 36 ++++++------------- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 5af194c5193..74febe8e222 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -1,7 +1,6 @@ // The http mocking library is not supported for wasm32 #![cfg(not(target_arch = "wasm32"))] -use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Client}; -use matrix_sdk_test::test_json; +use matrix_sdk::test_utils::logged_in_client_with_server; use serde::Serialize; use wiremock::{ matchers::{header, method, path, query_param, query_param_is_missing}, @@ -25,17 +24,6 @@ mod widget; matrix_sdk_test::init_tracing_for_tests!(); -async fn synced_client() -> (Client, MockServer) { - let (client, server) = logged_in_client_with_server().await; - mock_sync(&server, &*test_json::SYNC, None).await; - - let sync_settings = SyncSettings::new(); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - (client, server) -} - /// Mount a Mock on the given server to handle the `GET /sync` endpoint with /// an optional `since` param that returns a 200 status code with the given /// response body. diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 54b3935f342..68df572bd98 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -43,7 +43,7 @@ use wiremock::{ Mock, ResponseTemplate, }; -use crate::{logged_in_client_with_server, mock_sync, synced_client}; +use crate::{logged_in_client_with_server, mock_sync}; #[async_test] async fn test_invite_user_by_id() { let (client, server) = logged_in_client_with_server().await; @@ -368,23 +368,14 @@ async fn test_room_redact() { #[cfg(not(target_arch = "wasm32"))] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn test_fetch_members_deduplication() { - let (client, server) = synced_client().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room = server.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; // We don't need any members, we're just checking if we're correctly // deduplicating calls to the method. - let response_body = json!({ - "chunk": [], - }); - - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/members")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(response_body)) - .expect(1) - .mount(&server) - .await; - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + server.mock_get_members().ok(Vec::new()).mock_once().mount().await; let mut tasks = Vec::new(); @@ -401,31 +392,26 @@ async fn test_fetch_members_deduplication() { // Wait on all of them at once. join_all(tasks).await; - - // Ensure we called the endpoint exactly once. - server.verify().await; } #[async_test] async fn test_set_name() { - let (client, server) = synced_client().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - mock_sync(&server, &*test_json::SYNC, None).await; - let sync_settings = SyncSettings::new(); - client.sync_once(sync_settings).await.unwrap(); + let room = server.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); let name = "The room name"; Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.name/$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/$")) .and(header("authorization", "Bearer 1234")) .and(body_json(json!({ "name": name, }))) .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) .expect(1) - .mount(&server) + .mount(server.server()) .await; room.set_name(name.to_owned()).await.unwrap(); From 6a0333e812574be4462ca03df4c80cb1c3e7ee62 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Thu, 16 Jan 2025 10:54:19 +0100 Subject: [PATCH 970/979] test: replace Option::default() by None --- crates/matrix-sdk/tests/integration/room/tags.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/room/tags.rs b/crates/matrix-sdk/tests/integration/room/tags.rs index dc48244d3da..9d17a0cc555 100644 --- a/crates/matrix-sdk/tests/integration/room/tags.rs +++ b/crates/matrix-sdk/tests/integration/room/tags.rs @@ -92,7 +92,7 @@ async fn test_set_favourite() { // Server will be called to set the room as favourite. mock_tag_api(&server, TagName::Favorite, TagOperation::Set, 1).await; - room.set_is_favourite(true, Option::default()).await.unwrap(); + room.set_is_favourite(true, None).await.unwrap(); // Mock the response from the server. let tags = BTreeMap::from([(TagName::Favorite, TagInfo::default())]); @@ -125,7 +125,7 @@ async fn test_set_favourite_on_low_priority_room() { mock_tag_api(&server, TagName::Favorite, TagOperation::Set, 1).await; mock_tag_api(&server, TagName::LowPriority, TagOperation::Remove, 1).await; - room.set_is_favourite(true, Option::default()).await.unwrap(); + room.set_is_favourite(true, None).await.unwrap(); // Mock the response from the server. let tags = BTreeMap::from([(TagName::Favorite, TagInfo::default())]); @@ -148,7 +148,7 @@ async fn test_unset_favourite() { // Server will be called to unset the room as favourite. mock_tag_api(&server, TagName::Favorite, TagOperation::Remove, 1).await; - room.set_is_favourite(false, Option::default()).await.unwrap(); + room.set_is_favourite(false, None).await.unwrap(); server.verify().await; } @@ -165,7 +165,7 @@ async fn test_set_low_priority() { // Server will be called to set the room as favourite. mock_tag_api(&server, TagName::LowPriority, TagOperation::Set, 1).await; - room.set_is_low_priority(true, Option::default()).await.unwrap(); + room.set_is_low_priority(true, None).await.unwrap(); // Mock the response from the server. let tags = BTreeMap::from([(TagName::LowPriority, TagInfo::default())]); @@ -198,7 +198,7 @@ async fn test_set_low_priority_on_favourite_room() { mock_tag_api(&server, TagName::LowPriority, TagOperation::Set, 1).await; mock_tag_api(&server, TagName::Favorite, TagOperation::Remove, 1).await; - room.set_is_low_priority(true, Option::default()).await.unwrap(); + room.set_is_low_priority(true, None).await.unwrap(); // Mock the response from the server. let tags = BTreeMap::from([(TagName::LowPriority, TagInfo::default())]); @@ -221,7 +221,7 @@ async fn test_unset_low_priority() { // Server will be called to unset the room as favourite. mock_tag_api(&server, TagName::LowPriority, TagOperation::Remove, 1).await; - room.set_is_low_priority(false, Option::default()).await.unwrap(); + room.set_is_low_priority(false, None).await.unwrap(); server.verify().await; } From 3dd81fbe2ccf8e164b596cdd7a1a7e5203ed4d83 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 16 Jan 2025 10:49:53 +0000 Subject: [PATCH 971/979] test: rename snapshots not to contain `:` Windows doens't allow you to have `:` in its filenames --- testing/matrix-sdk-test/src/test_json/keys_query_sets.rs | 8 ++++---- ...KeyDistributionTestData__dan_keys_query_response.snap} | 0 ...stData__dan_keys_query_response_device_loggedout.snap} | 0 ..._KeyDistributionTestData__me_keys_query_response.snap} | 0 ...tionViolationTestData__own_keys_query_response_1.snap} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename testing/matrix-sdk-test/src/test_json/snapshots/{matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap => matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__dan_keys_query_response.snap} (100%) rename testing/matrix-sdk-test/src/test_json/snapshots/{matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap => matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__dan_keys_query_response_device_loggedout.snap} (100%) rename testing/matrix-sdk-test/src/test_json/snapshots/{matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap => matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__me_keys_query_response.snap} (100%) rename testing/matrix-sdk-test/src/test_json/snapshots/{matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap => matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData__own_keys_query_response_1.snap} (100%) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 6410a8d74c8..c2ed04f531f 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -332,7 +332,7 @@ impl KeyDistributionTestData { let response = builder.build_response(); with_settings!({sort_maps => true}, { assert_json_snapshot!( - "KeyDistributionTestData::me_keys_query_response", + "KeyDistributionTestData__me_keys_query_response", ruma_response_to_json(response.clone()), ); }); @@ -359,7 +359,7 @@ impl KeyDistributionTestData { let response = Self::dan_keys_query_response_common(); with_settings!({sort_maps => true}, { assert_json_snapshot!( - "KeyDistributionTestData::dan_keys_query_response", + "KeyDistributionTestData__dan_keys_query_response", ruma_response_to_json(response.clone()), ); }); @@ -377,7 +377,7 @@ impl KeyDistributionTestData { with_settings!({sort_maps => true}, { assert_json_snapshot!( - "KeyDistributionTestData::dan_keys_query_response_device_loggedout", + "KeyDistributionTestData__dan_keys_query_response_device_loggedout", ruma_response_to_json(response.clone()), ); }); @@ -722,7 +722,7 @@ impl VerificationViolationTestData { let response = builder.build_response(); with_settings!({sort_maps => true}, { assert_json_snapshot!( - "VerificationViolationTestData::own_keys_query_response_1", + "VerificationViolationTestData__own_keys_query_response_1", ruma_response_to_json(response.clone()), ); }); diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__dan_keys_query_response.snap similarity index 100% rename from testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap rename to testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__dan_keys_query_response.snap diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__dan_keys_query_response_device_loggedout.snap similarity index 100% rename from testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap rename to testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__dan_keys_query_response_device_loggedout.snap diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__me_keys_query_response.snap similarity index 100% rename from testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap rename to testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData__me_keys_query_response.snap diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData__own_keys_query_response_1.snap similarity index 100% rename from testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap rename to testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData__own_keys_query_response_1.snap From 425e48a46d087814c0cd72be3513111701f8ae3d Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 15 Jan 2025 14:47:33 +0100 Subject: [PATCH 972/979] feat(linked chunk): add `LinkedChunk::replace_item_at` to replace an item from a given position --- .../src/linked_chunk/as_vector.rs | 43 +++++++++--- .../matrix-sdk-common/src/linked_chunk/mod.rs | 67 ++++++++++++++++++ .../src/linked_chunk/relational.rs | 64 +++++++++++++++++ .../src/linked_chunk/updates.rs | 12 ++++ .../src/event_cache_store.rs | 68 +++++++++++++++++++ .../src/timeline/controller/state.rs | 20 ++++++ crates/matrix-sdk/src/event_cache/room/mod.rs | 37 +++++----- 7 files changed, 286 insertions(+), 25 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs index c0d65af1c15..ec23243084f 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/as_vector.rs @@ -364,6 +364,14 @@ impl UpdateToVectorDiff { } } + Update::ReplaceItem { at: position, item } => { + let (offset, (_chunk_index, _chunk_length)) = self.map_to_offset(position); + + // The chunk length doesn't change. + + diffs.push(VectorDiff::Set { index: offset, value: item.clone() }); + } + Update::RemoveItem { at: position } => { let (offset, (_chunk_index, chunk_length)) = self.map_to_offset(position); @@ -484,15 +492,7 @@ mod tests { assert_eq!(diffs, expected_diffs); for diff in diffs { - match diff { - VectorDiff::Insert { index, value } => accumulator.insert(index, value), - VectorDiff::Append { values } => accumulator.append(values), - VectorDiff::Remove { index } => { - accumulator.remove(index); - } - VectorDiff::Clear => accumulator.clear(), - diff => unimplemented!("{diff:?}"), - } + diff.apply(accumulator); } } @@ -710,6 +710,31 @@ mod tests { vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h'] ); + // Replace element 8 by an uppercase J. + linked_chunk + .replace_item_at(linked_chunk.item_position(|item| *item == 'j').unwrap(), 'J') + .unwrap(); + + assert_items_eq!( + linked_chunk, + ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'J', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h'] + ); + + // From an `ObservableVector` point of view, it would look like: + // + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // | m | a | w | x | y | b | d | i | J | k | l | e | f | g | z | h | + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // ^^^^ + // | + // new! + apply_and_assert_eq( + &mut accumulator, + as_vector.take(), + &[VectorDiff::Set { index: 8, value: 'J' }], + ); + // Let's try to clear the linked chunk now. linked_chunk.clear(); diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 113edbdc8c4..06bb4c63cee 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -528,6 +528,47 @@ impl LinkedChunk { Ok(removed_item) } + /// Replace item at a specified position in the [`LinkedChunk`]. + /// + /// `position` must point to a valid item, otherwise the method returns + /// `Err`. + pub fn replace_item_at(&mut self, position: Position, item: Item) -> Result<(), Error> + where + Item: Clone, + { + let chunk_identifier = position.chunk_identifier(); + let item_index = position.index(); + + let chunk = self + .links + .chunk_mut(chunk_identifier) + .ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?; + + match &mut chunk.content { + ChunkContent::Gap(..) => { + return Err(Error::ChunkIsAGap { identifier: chunk_identifier }) + } + + ChunkContent::Items(current_items) => { + if item_index >= current_items.len() { + return Err(Error::InvalidItemIndex { index: item_index }); + } + + // Avoid one spurious clone by notifying about the update *before* applying it. + if let Some(updates) = self.updates.as_mut() { + updates.push(Update::ReplaceItem { + at: Position(chunk_identifier, item_index), + item: item.clone(), + }); + } + + current_items[item_index] = item; + } + } + + Ok(()) + } + /// Insert a gap at a specified position in the [`LinkedChunk`]. /// /// Because the `position` can be invalid, this method returns a @@ -2919,4 +2960,30 @@ mod tests { ] ); } + + #[test] + fn test_replace_item() { + let mut linked_chunk = LinkedChunk::<3, char, ()>::new(); + + linked_chunk.push_items_back(['a', 'b', 'c']); + linked_chunk.push_gap_back(()); + // Sanity check. + assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [-]); + + // Replace item in bounds. + linked_chunk.replace_item_at(Position(ChunkIdentifier::new(0), 1), 'B').unwrap(); + assert_items_eq!(linked_chunk, ['a', 'B', 'c'] [-]); + + // Attempt to replace out-of-bounds. + assert_matches!( + linked_chunk.replace_item_at(Position(ChunkIdentifier::new(0), 3), 'Z'), + Err(Error::InvalidItemIndex { index: 3 }) + ); + + // Attempt to replace gap. + assert_matches!( + linked_chunk.replace_item_at(Position(ChunkIdentifier::new(1), 0), 'Z'), + Err(Error::ChunkIsAGap { .. }) + ); + } } diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs index fabc55a2e14..833c0f72415 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/relational.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -136,6 +136,19 @@ impl RelationalLinkedChunk { } } + Update::ReplaceItem { at, item } => { + let existing = self + .items + .iter_mut() + .find(|item| item.position == at) + .expect("trying to replace at an unknown position"); + assert!( + matches!(existing.item, Either::Item(..)), + "trying to replace a gap with an item" + ); + existing.item = Either::Item(item); + } + Update::RemoveItem { at } => { let mut entry_to_remove = None; @@ -938,4 +951,55 @@ mod tests { // The linked chunk is correctly reloaded. assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']); } + + #[test] + fn test_replace_item() { + let room_id = room_id!("!r0:matrix.org"); + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] }, + // update item at (0; 1). + Update::ReplaceItem { at: Position::new(CId::new(0), 1), item: 'B' }, + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: None, + },], + ); + + // Items contains the pushed *and* replaced items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 0), + item: Either::Item('a') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 1), + item: Either::Item('B') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 2), + item: Either::Item('c') + }, + ], + ); + } } diff --git a/crates/matrix-sdk-common/src/linked_chunk/updates.rs b/crates/matrix-sdk-common/src/linked_chunk/updates.rs index c14ee4ab09e..ac91749ca4f 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/updates.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/updates.rs @@ -79,6 +79,18 @@ pub enum Update { items: Vec, }, + /// An item has been replaced in the linked chunk. + /// + /// The `at` position MUST resolve to the actual position an existing *item* + /// (not a gap). + ReplaceItem { + /// The position of the item that's being replaced. + at: Position, + + /// The new value for the item. + item: Item, + }, + /// An item has been removed inside a chunk of kind Items. RemoveItem { /// The [`Position`] of the item. diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 88c9d1801bb..2afef54ef91 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -459,6 +459,28 @@ impl EventCacheStore for SqliteEventCacheStore { } } + Update::ReplaceItem { at, item: event } => { + let chunk_id = at.chunk_identifier().index(); + let index = at.index(); + + trace!(%room_id, "replacing item @ {chunk_id}:{index}"); + + let serialized = serde_json::to_vec(&event)?; + let content = this.encode_value(serialized)?; + + // The event id should be the same, but just in case it changed… + let event_id = event.event_id().map(|event_id| event_id.to_string()); + + txn.execute( + r#" + UPDATE events + SET content = ?, event_id = ? + WHERE room_id = ? AND chunk_id = ? AND position = ? + "#, + (content, event_id, &hashed_room_id, chunk_id, index,) + )?; + } + Update::RemoveItem { at } => { let chunk_id = at.chunk_identifier().index(); let index = at.index(); @@ -978,6 +1000,52 @@ mod tests { }); } + #[async_test] + async fn test_linked_chunk_replace_item() { + let store = get_event_cache_store().await.expect("creating cache store failed"); + + let room_id = &DEFAULT_TEST_ROOM_ID; + + store + .handle_linked_chunk_updates( + room_id, + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(42), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(42), 0), + items: vec![ + make_test_event(room_id, "hello"), + make_test_event(room_id, "world"), + ], + }, + Update::ReplaceItem { + at: Position::new(ChunkIdentifier::new(42), 1), + item: make_test_event(room_id, "yolo"), + }, + ], + ) + .await + .unwrap(); + + let mut chunks = store.reload_linked_chunk(room_id).await.unwrap(); + + assert_eq!(chunks.len(), 1); + + let c = chunks.remove(0); + assert_eq!(c.identifier, ChunkIdentifier::new(42)); + assert_eq!(c.previous, None); + assert_eq!(c.next, None); + assert_matches!(c.content, ChunkContent::Items(events) => { + assert_eq!(events.len(), 2); + check_test_event(&events[0], "hello"); + check_test_event(&events[1], "yolo"); + }); + } + #[async_test] async fn test_linked_chunk_remove_chunk() { let store = get_event_cache_store().await.expect("creating cache store failed"); diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 2f91777777c..f8e5cb82282 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -497,6 +497,26 @@ impl TimelineStateTransaction<'_> { .await; } + VectorDiff::Set { index: event_index, value: event } => { + if let Some(timeline_item_index) = self + .items + .all_remote_events() + .get(event_index) + .and_then(|meta| meta.timeline_item_index) + { + self.handle_remote_event( + event, + TimelineItemPosition::UpdateDecrypted { timeline_item_index }, + room_data_provider, + settings, + &mut date_divider_adjuster, + ) + .await; + } else { + warn!(event_index, "Set update dropped because there wasn't any attached timeline item index."); + } + } + VectorDiff::Remove { index: event_index } => { self.remove_timeline_item(event_index, &mut date_divider_adjuster); } diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 08b18b69aa4..de83c5cac8f 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -636,24 +636,28 @@ mod private { let _ = closure(); } + fn strip_relations_from_event(ev: &mut SyncTimelineEvent) { + match &mut ev.kind { + TimelineEventKind::Decrypted(decrypted) => { + // Remove all information about encryption info for + // the bundled events. + decrypted.unsigned_encryption_info = None; + + // Remove the `unsigned`/`m.relations` field, if needs be. + Self::strip_relations_if_present(&mut decrypted.event); + } + + TimelineEventKind::UnableToDecrypt { event, .. } + | TimelineEventKind::PlainText { event } => { + Self::strip_relations_if_present(event); + } + } + } + /// Strips the bundled relations from a collection of events. fn strip_relations_from_events(items: &mut [SyncTimelineEvent]) { for ev in items.iter_mut() { - match &mut ev.kind { - TimelineEventKind::Decrypted(decrypted) => { - // Remove all information about encryption info for - // the bundled events. - decrypted.unsigned_encryption_info = None; - - // Remove the `unsigned`/`m.relations` field, if needs be. - Self::strip_relations_if_present(&mut decrypted.event); - } - - TimelineEventKind::UnableToDecrypt { event, .. } - | TimelineEventKind::PlainText { event } => { - Self::strip_relations_if_present(event); - } - } + Self::strip_relations_from_event(ev); } } @@ -672,10 +676,11 @@ mod private { trace!("propagating {} updates", updates.len()); - // Strip relations from the `PushItems` updates. + // Strip relations from updates which insert or replace items. for up in updates.iter_mut() { match up { Update::PushItems { items, .. } => Self::strip_relations_from_events(items), + Update::ReplaceItem { item, .. } => Self::strip_relations_from_event(item), // Other update kinds don't involve adding new events. Update::NewItemsChunk { .. } | Update::NewGapChunk { .. } From 6f780a499c5211d53a8d93aad6b8a14399b15431 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 15 Jan 2025 16:45:16 +0100 Subject: [PATCH 973/979] test(timeline): use assert_let_timeout more in the timeline's code --- .../tests/integration/timeline/mod.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 303bd0ae0fd..a54081b9532 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -114,15 +114,15 @@ async fn test_reaction() { server.reset().await; // The new message starts with their author's read receipt. - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let_timeout!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next()); let event_item = message.as_event().unwrap(); assert_matches!(event_item.content(), TimelineItemContent::Message(_)); assert_eq!(event_item.read_receipts().len(), 1); // The new message is getting the reaction, which implies an implicit read // receipt that's obtained first. - assert_let!( - Some(VectorDiff::Set { index: 0, value: updated_message }) = timeline_stream.next().await + assert_let_timeout!( + Some(VectorDiff::Set { index: 0, value: updated_message }) = timeline_stream.next() ); let event_item = updated_message.as_event().unwrap(); assert_let!(TimelineItemContent::Message(msg) = event_item.content()); @@ -131,8 +131,8 @@ async fn test_reaction() { assert_eq!(event_item.reactions().len(), 0); // Then the reaction is taken into account. - assert_let!( - Some(VectorDiff::Set { index: 0, value: updated_message }) = timeline_stream.next().await + assert_let_timeout!( + Some(VectorDiff::Set { index: 0, value: updated_message }) = timeline_stream.next() ); let event_item = updated_message.as_event().unwrap(); assert_let!(TimelineItemContent::Message(msg) = event_item.content()); @@ -145,7 +145,9 @@ async fn test_reaction() { assert_eq!(senders.as_slice(), [user_id!("@bob:example.org")]); // The date divider. - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let_timeout!( + Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next() + ); assert!(date_divider.is_date_divider()); sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( @@ -163,8 +165,8 @@ async fn test_reaction() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!( - Some(VectorDiff::Set { index: 1, value: updated_message }) = timeline_stream.next().await + assert_let_timeout!( + Some(VectorDiff::Set { index: 1, value: updated_message }) = timeline_stream.next() ); let event_item = updated_message.as_event().unwrap(); assert_let!(TimelineItemContent::Message(msg) = event_item.content()); From 50383098ff8dd6ea00dc988e01b3542d8e63d079 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 15 Jan 2025 16:55:57 +0100 Subject: [PATCH 974/979] feat(event cache): redact events in the database whenever they're redacted --- crates/matrix-sdk-base/src/lib.rs | 7 +- crates/matrix-sdk-base/src/rooms/mod.rs | 2 +- crates/matrix-sdk-base/src/rooms/normal.rs | 6 +- .../src/deserialized_responses.rs | 13 ++ crates/matrix-sdk/src/event_cache/mod.rs | 13 +- .../matrix-sdk/src/event_cache/pagination.rs | 11 +- .../matrix-sdk/src/event_cache/room/events.rs | 87 +++++++++- crates/matrix-sdk/src/event_cache/room/mod.rs | 20 ++- .../tests/integration/event_cache.rs | 151 +++++++++++++++++- 9 files changed, 292 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index ba165ba8026..cc4e5128403 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -37,7 +37,6 @@ mod rooms; pub mod read_receipts; pub use read_receipts::PreviousEventsProvider; -pub use rooms::RoomMembersUpdate; pub mod sliding_sync; pub mod store; @@ -56,9 +55,9 @@ pub use http; pub use matrix_sdk_crypto as crypto; pub use once_cell; pub use rooms::{ - Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, - RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState, - RoomStateFilter, + apply_redaction, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, + RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, + RoomMemberships, RoomState, RoomStateFilter, }; pub use store::{ ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index d738199ed95..a128a10f17e 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -12,7 +12,7 @@ use std::{ use bitflags::bitflags; pub use members::RoomMember; pub use normal::{ - Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, + apply_redaction, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState, RoomStateFilter, }; use regex::Regex; diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index b393ddac9f1..f534ad399e8 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -2018,7 +2018,9 @@ impl RoomInfo { } } -fn apply_redaction( +/// Apply a redaction to the given target `event`, given the raw redaction event +/// and the room version. +pub fn apply_redaction( event: &Raw, raw_redaction: &Raw, room_version: &RoomVersionId, @@ -2044,7 +2046,7 @@ fn apply_redaction( let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because)); if let Err(e) = redact_result { - warn!("Failed to redact latest event: {e}"); + warn!("Failed to redact event: {e}"); return None; } diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 69458887539..cf27dae0d9c 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -390,6 +390,19 @@ impl SyncTimelineEvent { self.kind.raw() } + /// Replace the raw event included in this item by another one. + pub fn replace_raw(&mut self, replacement: Raw) { + match &mut self.kind { + TimelineEventKind::Decrypted(decrypted) => decrypted.event = replacement, + TimelineEventKind::UnableToDecrypt { event, .. } + | TimelineEventKind::PlainText { event } => { + // It's safe to cast `AnyMessageLikeEvent` into `AnySyncMessageLikeEvent`, + // because the former contains a superset of the fields included in the latter. + *event = replacement.cast(); + } + } + } + /// If the event was a decrypted event that was successfully decrypted, get /// its encryption info. Otherwise, `None`. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index e1c58dc0082..0ea2fa05b55 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -52,7 +52,7 @@ use ruma::{ AnySyncTimelineEvent, }, serde::Raw, - EventId, OwnedEventId, OwnedRoomId, RoomId, + EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, }; use tokio::sync::{ broadcast::{error::RecvError, Receiver}, @@ -622,10 +622,21 @@ impl EventCacheInner { let room_state = RoomEventCacheState::new(room_id.to_owned(), self.store.clone()).await?; + let room_version = self + .client + .get() + .and_then(|client| client.get_room(room_id)) + .map(|room| room.clone_info().room_version_or_default()) + .unwrap_or_else(|| { + warn!("unknown room version for {room_id}, using default V1"); + RoomVersionId::V1 + }); + let room_event_cache = RoomEventCache::new( self.client.clone(), room_state, room_id.to_owned(), + room_version, self.all_events.clone(), ); diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 4d135666649..31cdf963301 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -181,7 +181,8 @@ impl RoomPagination { // (backward). The `RoomEvents` API expects the first event to be the oldest. .rev() .cloned() - .map(SyncTimelineEvent::from); + .map(SyncTimelineEvent::from) + .collect::>(); let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); @@ -190,20 +191,20 @@ impl RoomPagination { // There is a prior gap, let's replace it by new events! trace!("replaced gap with new events from backpagination"); room_events - .replace_gap_at(sync_events, gap_id) + .replace_gap_at(sync_events.clone(), gap_id) .expect("gap_identifier is a valid chunk id we read previously") } else if let Some(pos) = first_event_pos { // No prior gap, but we had some events: assume we need to prepend events // before those. trace!("inserted events before the first known event"); let report = room_events - .insert_events_at(sync_events, pos) + .insert_events_at(sync_events.clone(), pos) .expect("pos is a valid position we just read above"); (report, Some(pos)) } else { // No prior gap, and no prior events: push the events. trace!("pushing events received from back-pagination"); - let report = room_events.push_events(sync_events); + let report = room_events.push_events(sync_events.clone()); // A new gap may be inserted before the new events, if there are any. let next_pos = room_events.events().next().map(|(item_pos, _)| item_pos); (report, next_pos) @@ -228,6 +229,8 @@ impl RoomPagination { debug!("not storing previous batch token, because we deduplicated all new back-paginated events"); } + room_events.on_new_events(&self.inner.room_version, sync_events.iter()); + BackPaginationOutcome { events, reached_start } }) .await?; diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 516432304a6..142594ecb7a 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -15,14 +15,17 @@ use std::cmp::Ordering; use eyeball_im::VectorDiff; -use matrix_sdk_base::event_cache::store::DEFAULT_CHUNK_CAPACITY; pub use matrix_sdk_base::event_cache::{Event, Gap}; +use matrix_sdk_base::{apply_redaction, event_cache::store::DEFAULT_CHUNK_CAPACITY}; use matrix_sdk_common::linked_chunk::{ AsVector, Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, IterBackward, LinkedChunk, ObservableUpdates, Position, }; -use ruma::OwnedEventId; -use tracing::{debug, error, warn}; +use ruma::{ + events::{room::redaction::SyncRoomRedactionEvent, AnySyncTimelineEvent}, + OwnedEventId, RoomVersionId, +}; +use tracing::{debug, error, instrument, trace, warn}; use super::{ super::deduplicator::{Decoration, Deduplicator}, @@ -90,6 +93,84 @@ impl RoomEvents { self.chunks.clear(); } + /// If the given event is a redaction, try to retrieve the to-be-redacted + /// event in the chunk, and replace it by the redacted form. + #[instrument(skip_all)] + fn maybe_apply_new_redaction(&mut self, room_version: &RoomVersionId, event: &Event) { + let Ok(AnySyncTimelineEvent::MessageLike( + ruma::events::AnySyncMessageLikeEvent::RoomRedaction(redaction), + )) = event.raw().deserialize() + else { + return; + }; + + let Some(event_id) = redaction.redacts(room_version) else { + warn!("missing target event id from the redaction event"); + return; + }; + + // Replace the redacted event by a redacted form, if we knew about it. + let mut items = self.chunks.items(); + + if let Some((pos, target_event)) = + items.find(|(_, item)| item.event_id().as_deref() == Some(event_id)) + { + // Don't redact already redacted events. + if let Ok(deserialized) = target_event.raw().deserialize() { + match deserialized { + AnySyncTimelineEvent::MessageLike(ev) => { + if ev.original_content().is_none() { + // Already redacted. + return; + } + } + AnySyncTimelineEvent::State(ev) => { + if ev.original_content().is_none() { + // Already redacted. + return; + } + } + } + } + + if let Some(redacted_event) = apply_redaction( + target_event.raw(), + event.raw().cast_ref::(), + room_version, + ) { + let mut copy = target_event.clone(); + + // It's safe to cast `redacted_event` here: + // - either the event was an `AnyTimelineEvent` cast to `AnySyncTimelineEvent` + // when calling .raw(), so it's still one under the hood. + // - or it wasn't, and it's a plain `AnySyncTimelineEvent` in this case. + copy.replace_raw(redacted_event.cast()); + + // Get rid of the immutable borrow on self.chunks. + drop(items); + + self.chunks + .replace_item_at(pos, copy) + .expect("should have been a valid position of an item"); + } + } else { + trace!("redacted event is missing from the linked chunk"); + } + + // TODO: remove all related events too! + } + + /// Callback to call whenever we touch events in the database. + pub fn on_new_events<'a>( + &mut self, + room_version: &RoomVersionId, + events: impl Iterator, + ) { + for ev in events { + self.maybe_apply_new_redaction(room_version, ev); + } + } + /// Push events after all events or gaps. /// /// The last event in `events` is the most recent one. diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index de83c5cac8f..5d011d4a906 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -25,7 +25,7 @@ use matrix_sdk_base::{ use ruma::{ events::{relation::RelationType, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent}, serde::Raw, - EventId, OwnedEventId, OwnedRoomId, + EventId, OwnedEventId, OwnedRoomId, RoomVersionId, }; use tokio::sync::{ broadcast::{Receiver, Sender}, @@ -61,9 +61,18 @@ impl RoomEventCache { client: WeakClient, state: RoomEventCacheState, room_id: OwnedRoomId, + room_version: RoomVersionId, all_events_cache: Arc>, ) -> Self { - Self { inner: Arc::new(RoomEventCacheInner::new(client, state, room_id, all_events_cache)) } + Self { + inner: Arc::new(RoomEventCacheInner::new( + client, + state, + room_id, + room_version, + all_events_cache, + )), + } } /// Subscribe to room updates for this room, after getting the initial list @@ -189,6 +198,9 @@ pub(super) struct RoomEventCacheInner { /// The room id for this room. room_id: OwnedRoomId, + /// The room version for this room. + pub(crate) room_version: RoomVersionId, + /// Sender part for subscribers to this room. pub sender: Sender, @@ -222,12 +234,14 @@ impl RoomEventCacheInner { client: WeakClient, state: RoomEventCacheState, room_id: OwnedRoomId, + room_version: RoomVersionId, all_events_cache: Arc>, ) -> Self { let sender = Sender::new(32); let weak_room = WeakRoom::new(client, room_id); Self { room_id: weak_room.room_id().to_owned(), + room_version, state: RwLock::new(state), all_events: all_events_cache, sender, @@ -444,6 +458,8 @@ impl RoomEventCacheInner { .expect("we obtained the valid position beforehand"); } } + + room_events.on_new_events(&self.room_version, sync_timeline_events.iter()); }) .await?; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index e43fcbb1f2a..a2e2aaf7d44 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -22,7 +22,11 @@ use matrix_sdk::{ use matrix_sdk_test::{ async_test, event_factory::EventFactory, GlobalAccountDataTestEvent, JoinedRoomBuilder, }; -use ruma::{event_id, room_id, user_id}; +use ruma::{ + event_id, + events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent}, + room_id, user_id, RoomVersionId, +}; use serde_json::json; use tokio::{spawn, sync::broadcast, time::sleep}; @@ -1453,3 +1457,148 @@ async fn test_dont_delete_gap_that_wasnt_inserted() { // This doesn't cause an update, because nothing changed. assert!(stream.is_empty()); } + +#[async_test] +async fn test_apply_redaction_when_redaction_comes_later() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + let room_id = room_id!("!omelette:fromage.fr"); + + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + + // Start with a room with two events. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.text_msg("inapprops").event_id(event_id!("$1")).into_raw_sync(), + ), + ) + .await; + + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + + // Wait for the first event. + let (events, mut subscriber) = room_event_cache.subscribe().await.unwrap(); + if events.is_empty() { + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv() + ); + } + + // Sync a redaction for the event $1. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.redaction(event_id!("$1")).into_raw_sync()), + ) + .await; + + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() + ); + + assert_eq!(diffs.len(), 2); + + // First, the redaction event itself. + { + assert_let!(VectorDiff::Append { values: new_events } = &diffs[0]); + assert_eq!(new_events.len(), 1); + let ev = new_events[0].raw().deserialize().unwrap(); + assert_let!( + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(ev)) = ev + ); + assert_eq!(ev.redacts(&RoomVersionId::V1).unwrap(), event_id!("$1")); + } + + // Then, we have an update for the redacted event. + { + assert_let!(VectorDiff::Set { index: 0, value: redacted_event } = &diffs[1]); + let ev = redacted_event.raw().deserialize().unwrap(); + assert_let!( + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(ev)) = ev + ); + // The event has been redacted! + assert_matches!(ev.as_original(), None); + } + + // And done for now. + assert!(subscriber.is_empty()); +} + +#[async_test] +async fn test_apply_redaction_when_redacted_and_redaction_are_in_same_sync() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_cache = client.event_cache(); + + // Immediately subscribe the event cache to sync updates. + event_cache.subscribe().unwrap(); + event_cache.enable_storage().unwrap(); + + let room_id = room_id!("!omelette:fromage.fr"); + let room = server.sync_joined_room(&client, room_id).await; + let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); + let (_events, mut subscriber) = room_event_cache.subscribe().await.unwrap(); + + let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c")); + + // Now include a sync with both the original event *and* the redacted one. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("bleh").event_id(event_id!("$2")).into_raw_sync()) + .add_timeline_event(f.redaction(event_id!("$2")).into_raw_sync()), + ) + .await; + + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() + ); + + assert_eq!(diffs.len(), 2); + + // First, we get an update with all the new events. + { + assert_let!(VectorDiff::Append { values: new_events } = &diffs[0]); + assert_eq!(new_events.len(), 2); + + // The original event. + let ev = new_events[0].raw().deserialize().unwrap(); + assert_let!( + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(ev)) = ev + ); + assert_eq!(ev.as_original().unwrap().content.body(), "bleh"); + + // The redaction. + let ev = new_events[1].raw().deserialize().unwrap(); + assert_let!( + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(ev)) = ev + ); + assert_eq!(ev.redacts(&RoomVersionId::V1).unwrap(), event_id!("$2")); + } + + // Then the redaction of the event happens separately. + { + assert_let!(VectorDiff::Set { index: 0, value: redacted_event } = &diffs[1]); + let ev = redacted_event.raw().deserialize().unwrap(); + assert_let!( + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(ev)) = ev + ); + // The event has been redacted! + assert_matches!(ev.as_original(), None); + } + + // That's all, folks! + assert!(subscriber.is_empty()); +} From 7fa06cb0289b657acfa54e8dcc2e21f724b3aa7b Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 15 Jan 2025 18:17:36 +0100 Subject: [PATCH 975/979] refactor(timeline): rename `TimelineItemPosition::UpdateDecrypted` to `UpdateAt` --- .../src/timeline/controller/state.rs | 8 ++++---- .../matrix-sdk-ui/src/timeline/event_handler.rs | 17 ++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index f8e5cb82282..5763f990d22 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -281,7 +281,7 @@ impl TimelineState { let handle_one_res = txn .handle_remote_event( event.into(), - TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx }, + TimelineItemPosition::UpdateAt { timeline_item_index: idx }, room_data_provider, settings, &mut date_divider_adjuster, @@ -506,7 +506,7 @@ impl TimelineStateTransaction<'_> { { self.handle_remote_event( event, - TimelineItemPosition::UpdateDecrypted { timeline_item_index }, + TimelineItemPosition::UpdateAt { timeline_item_index }, room_data_provider, settings, &mut date_divider_adjuster, @@ -592,7 +592,7 @@ impl TimelineStateTransaction<'_> { | TimelineItemPosition::Start { origin } | TimelineItemPosition::At { origin, .. } => origin, - TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self + TimelineItemPosition::UpdateAt { timeline_item_index: idx } => self .items .get(idx) .and_then(|item| item.as_event()) @@ -876,7 +876,7 @@ impl TimelineStateTransaction<'_> { self.items.insert_remote_event(event_index, event_meta.base_meta()); } - TimelineItemPosition::UpdateDecrypted { .. } => { + TimelineItemPosition::UpdateAt { .. } => { if let Some(event) = self.items.get_remote_event_by_event_id_mut(event_meta.event_id) { diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index b3288ca299f..f9d286b45dd 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -300,11 +300,11 @@ pub(super) enum TimelineItemPosition { origin: RemoteEventOrigin, }, - /// A single item is updated, after it's been successfully decrypted. + /// A single item is updated. /// - /// This happens when an item that was a UTD must be replaced with the - /// decrypted event. - UpdateDecrypted { + /// This can happen for instance after a UTD has been successfully + /// decrypted, or when it's been redacted at the source. + UpdateAt { /// The index of the **timeline item**. timeline_item_index: usize, }, @@ -511,7 +511,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { trace!("No new item added"); if let Flow::Remote { - position: TimelineItemPosition::UpdateDecrypted { timeline_item_index }, + position: TimelineItemPosition::UpdateAt { timeline_item_index }, .. } = self.ctx.flow { @@ -609,7 +609,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { match position { TimelineItemPosition::Start { .. } | TimelineItemPosition::At { .. } - | TimelineItemPosition::UpdateDecrypted { .. } => { + | TimelineItemPosition::UpdateAt { .. } => { // Only insert the edit if there wasn't any other edit // before. // @@ -1057,8 +1057,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { | TimelineItemPosition::At { origin, .. } => origin, // For updates, reuse the origin of the encrypted event. - TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self - .items[idx] + TimelineItemPosition::UpdateAt { timeline_item_index: idx } => self.items[idx] .as_event() .and_then(|ev| Some(ev.as_remote()?.origin)) .unwrap_or_else(|| { @@ -1241,7 +1240,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Flow::Remote { event_id: decrypted_event_id, - position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx }, + position: TimelineItemPosition::UpdateAt { timeline_item_index: idx }, .. } => { trace!("Updating timeline item at position {idx}"); From c24770a774d1b6e51de0efb96ceb01bd0f345169 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:26:52 +0000 Subject: [PATCH 976/979] test: add support for dehydrated devices to `KeyQueryResponseTemplate` (#4540) #4476 added some test helpers to generate `/keys/query` responses. We're going to need to test dehydrated devices, so this PR adds support for that. --- .../src/test_json/keys_query_sets.rs | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index c2ed04f531f..95c355c6443 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -29,7 +29,7 @@ use crate::{ /// ``` /// # use ruma::{device_id, owned_user_id}; /// # use vodozemac::{Curve25519PublicKey, Ed25519SecretKey}; -/// # use matrix_sdk_test::test_json::keys_query_sets::KeyQueryResponseTemplate; +/// # use matrix_sdk_test::test_json::keys_query_sets::{KeyQueryResponseTemplate, KeyQueryResponseTemplateDeviceOptions}; /// /// let pub_curve_key = "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4"; /// let ed25519_key = "yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM"; @@ -39,7 +39,7 @@ use crate::{ /// device_id!("TESTDEVICE"), /// &Curve25519PublicKey::from_base64(pub_curve_key).unwrap(), /// &Ed25519SecretKey::from_base64(ed25519_key).unwrap(), -/// false, +/// KeyQueryResponseTemplateDeviceOptions::new(), /// ); /// /// let response = builder.build_response(); @@ -51,7 +51,7 @@ use crate::{ /// ``` /// # use ruma::{device_id, owned_user_id, user_id}; /// # use vodozemac::{Curve25519PublicKey, Ed25519SecretKey}; -/// # use matrix_sdk_test::test_json::keys_query_sets::KeyQueryResponseTemplate; +/// # use matrix_sdk_test::test_json::keys_query_sets::{KeyQueryResponseTemplate, KeyQueryResponseTemplateDeviceOptions}; /// /// // Private cross-signing keys /// let master_key = "QGZo39k199RM0NYvPvFNXBspc5llftHWKKHqEi25q0U"; @@ -81,7 +81,7 @@ use crate::{ /// device_id!("SECUREDEVICE"), /// &Curve25519PublicKey::from_base64(pub_curve_key).unwrap(), /// &Ed25519SecretKey::from_base64(ed25519_key).unwrap(), -/// true, +/// KeyQueryResponseTemplateDeviceOptions::new().verified(true), /// ); /// /// let response = builder.build_response(); @@ -159,14 +159,15 @@ impl KeyQueryResponseTemplate { /// Ed25519 device key must be provided so that the signature can be /// calculated. /// - /// The device can optionally be signed by the self-signing key by setting - /// `cross_signed` to `true`. + /// The device can optionally be signed by the self-signing key by calling + /// [`KeyResponseTemplateDeviceOptions::verified(true)`] on the `options` + /// object. pub fn with_device( mut self, device_id: &DeviceId, curve25519_public_key: &Curve25519PublicKey, ed25519_secret_key: &Ed25519SecretKey, - cross_signed: bool, + options: KeyQueryResponseTemplateDeviceOptions, ) -> Self { let mut device_keys = json!({ "algorithms": [ @@ -182,8 +183,12 @@ impl KeyQueryResponseTemplate { "user_id": self.user_id.clone(), }); + if options.dehydrated { + device_keys["dehydrated"] = Value::Bool(true); + } + sign_json(&mut device_keys, ed25519_secret_key, &self.user_id, device_id.as_str()); - if cross_signed { + if options.verified { let ssk = self .self_signing_key .as_ref() @@ -283,6 +288,39 @@ impl KeyQueryResponseTemplate { } } +/// Options which control the addition of a device to a +/// [`KeyQueryResponseTemplate`], via [`KeyQueryResponseTemplate::with_device`]. +#[derive(Default)] +pub struct KeyQueryResponseTemplateDeviceOptions { + verified: bool, + dehydrated: bool, +} + +impl KeyQueryResponseTemplateDeviceOptions { + /// Creates a blank new set of options ready for configuration. + /// + /// All options are initially set to `false`. + pub fn new() -> Self { + Self::default() + } + + /// Sets the option for whether the device will be verified (i.e., signed by + /// the self-signing key). + pub fn verified(mut self, verified: bool) -> Self { + self.verified = verified; + self + } + + /// Sets the option for whether the device will be marked as "dehydrated", + /// as per [MSC3814]. + /// + /// [MSC3814]: https://github.com/matrix-org/matrix-spec-proposals/pull/3814 + pub fn dehydrated(mut self, dehydrated: bool) -> Self { + self.dehydrated = dehydrated; + self + } +} + /// This set of keys/query response was generated using a local synapse. /// /// The current user is `@me:localhost`, the private part of the @@ -404,7 +442,7 @@ impl KeyDistributionTestData { &Curve25519PublicKey::from_base64("PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4") .unwrap(), &Ed25519SecretKey::from_base64("yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM").unwrap(), - true, + KeyQueryResponseTemplateDeviceOptions::new().verified(true), ); // Add unsigned device FRGNMZVOKA @@ -413,7 +451,7 @@ impl KeyDistributionTestData { &Curve25519PublicKey::from_base64("Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4") .unwrap(), &Ed25519SecretKey::from_base64("/SlFtNKxTPN+i4pHzSPWZ1Oc6ymMB33sS32GXZkaLos").unwrap(), - false, + KeyQueryResponseTemplateDeviceOptions::new(), ); builder.build_response() From 2cb6ee8e6dc0defd68ba0bf0c816f2b4a235dec7 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 16 Jan 2025 14:34:11 +0200 Subject: [PATCH 977/979] chore(ffi): silence useless logs coming out of the ffi crate Setting the default log level to `debug` results in logs like: ``` log: log_event log: latest_event log: log_event log: log_event log: room_info log: latest_event log: log_event log: room_info ``` Presumably they're coming out of the custom tracing configuration and we definitely don't need them. --- bindings/matrix-sdk-ffi/src/platform.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/platform.rs b/bindings/matrix-sdk-ffi/src/platform.rs index 95cccccdfa5..9d0211d700a 100644 --- a/bindings/matrix-sdk-ffi/src/platform.rs +++ b/bindings/matrix-sdk-ffi/src/platform.rs @@ -288,8 +288,9 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[ ]; const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[ - LogTarget::Hyper, // Too verbose - LogTarget::MatrixSdk, // Too generic + LogTarget::Hyper, // Too verbose + LogTarget::MatrixSdk, // Too generic + LogTarget::MatrixSdkFfi, // Too verbose ]; #[derive(uniffi::Record)] @@ -398,7 +399,7 @@ mod tests { filter, "panic=error,\ hyper=warn,\ - matrix_sdk_ffi=trace,\ + matrix_sdk_ffi=info,\ matrix_sdk=info,\ matrix_sdk::client=trace,\ matrix_sdk_crypto=trace,\ From f231c74314c17bf05919118a558e1923b6dc72ef Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 16 Jan 2025 11:31:23 +0000 Subject: [PATCH 978/979] test: simplify examples for `KeyQueryResponseTemplate` Generating keys from slices rather than base64 is easier. Also, s/builder/template/. --- .../src/test_json/keys_query_sets.rs | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 95c355c6443..daeb08f2bce 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -31,21 +31,21 @@ use crate::{ /// # use vodozemac::{Curve25519PublicKey, Ed25519SecretKey}; /// # use matrix_sdk_test::test_json::keys_query_sets::{KeyQueryResponseTemplate, KeyQueryResponseTemplateDeviceOptions}; /// -/// let pub_curve_key = "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4"; -/// let ed25519_key = "yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM"; +/// // Note that (almost) any 32-byte sequence can be used as a private Ed25519 or Curve25519 key. +/// // You can also use an arbitrary 32-byte sequence as a *public* key though of course you will +/// // not know the private key it corresponds to (if indeed there is one). /// -/// let builder = KeyQueryResponseTemplate::new(owned_user_id!("@alice:localhost")) +/// let template = KeyQueryResponseTemplate::new(owned_user_id!("@alice:localhost")) /// .with_device( /// device_id!("TESTDEVICE"), -/// &Curve25519PublicKey::from_base64(pub_curve_key).unwrap(), -/// &Ed25519SecretKey::from_base64(ed25519_key).unwrap(), +/// &Curve25519PublicKey::from(b"curvepubcurvepubcurvepubcurvepub".to_owned()), +/// &Ed25519SecretKey::from_slice(b"device12device12device12device12"), /// KeyQueryResponseTemplateDeviceOptions::new(), /// ); /// -/// let response = builder.build_response(); +/// let response = template.build_response(); /// ``` /// -/// /// A more complex case, with cross-signing keys and a signed device: /// /// ``` @@ -53,38 +53,27 @@ use crate::{ /// # use vodozemac::{Curve25519PublicKey, Ed25519SecretKey}; /// # use matrix_sdk_test::test_json::keys_query_sets::{KeyQueryResponseTemplate, KeyQueryResponseTemplateDeviceOptions}; /// -/// // Private cross-signing keys -/// let master_key = "QGZo39k199RM0NYvPvFNXBspc5llftHWKKHqEi25q0U"; -/// let ssk = "0ES1HO5VXpy/BsXxadwsk6QcwH/ci99KkV9ZlPakHlU"; -/// let usk = "vSdfrHJO8sZH/54r1uCg8BE0CdcDVGkPQNOu7Ej8BBs"; -/// -/// // Device keys -/// let pub_curve_key = "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4"; -/// let ed25519_key = "yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM"; -/// -/// let other_user_usk = "zQSosK46giUFs2ACsaf32bA7drcIXbmViyEt+TLfloI"; -/// -/// let builder = KeyQueryResponseTemplate::new(owned_user_id!("@me:localhost")) +/// let template = KeyQueryResponseTemplate::new(owned_user_id!("@me:localhost")) /// // add cross-signing keys /// .with_cross_signing_keys( -/// Ed25519SecretKey::from_base64(master_key).unwrap(), -/// Ed25519SecretKey::from_base64(ssk).unwrap(), -/// Ed25519SecretKey::from_base64(usk).unwrap(), +/// Ed25519SecretKey::from_slice(b"master12master12master12master12"), +/// Ed25519SecretKey::from_slice(b"self1234self1234self1234self1234"), +/// Ed25519SecretKey::from_slice(b"user1234user1234user1234user1234"), /// ) /// // add verification from another user /// .with_user_verification_signature( /// user_id!("@them:localhost"), -/// &Ed25519SecretKey::from_base64(other_user_usk).unwrap(), +/// &Ed25519SecretKey::from_slice(b"otheruser12otheruser12otheruser1"), /// ) /// // add signed device /// .with_device( /// device_id!("SECUREDEVICE"), -/// &Curve25519PublicKey::from_base64(pub_curve_key).unwrap(), -/// &Ed25519SecretKey::from_base64(ed25519_key).unwrap(), +/// &Curve25519PublicKey::from(b"curvepubcurvepubcurvepubcurvepub".to_owned()), +/// &Ed25519SecretKey::from_slice(b"device12device12device12device12"), /// KeyQueryResponseTemplateDeviceOptions::new().verified(true), /// ); /// -/// let response = builder.build_response(); +/// let response = template.build_response(); /// ``` pub struct KeyQueryResponseTemplate { /// The User ID of the user that this test data is about. From 2bd8c56e64858cf129347830ff5d44b994f6fa17 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 15 Jan 2025 17:53:28 +0000 Subject: [PATCH 979/979] crypto: add some more documentation to `DeviceKeys` This confused me for a while, so I thought more documentation might help. --- crates/matrix-sdk-crypto/src/types/device_keys.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/matrix-sdk-crypto/src/types/device_keys.rs b/crates/matrix-sdk-crypto/src/types/device_keys.rs index 6dc5cdfcfbd..a9173ff6b77 100644 --- a/crates/matrix-sdk-crypto/src/types/device_keys.rs +++ b/crates/matrix-sdk-crypto/src/types/device_keys.rs @@ -38,6 +38,12 @@ use super::{EventEncryptionAlgorithm, Signatures}; /// Specification, encapsulating essential elements such as the public device /// identity keys. /// +/// See also [`ruma::encryption::DeviceKeys`] which is similar, but slightly +/// less comprehensive (it lacks some fields, and the `keys` are represented as +/// base64 strings rather than type-safe [`DeviceKey`]s). We always use this +/// struct to build `/keys/upload` requests and to deserialize `/keys/query` +/// responses. +/// /// [device_keys_spec]: https://spec.matrix.org/v1.10/client-server-api/#_matrixclientv3keysupload_devicekeys #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(try_from = "DeviceKeyHelper", into = "DeviceKeyHelper")] @@ -190,6 +196,8 @@ impl From for DeviceKey { } } +/// A de/serialization helper for [`DeviceKeys`] which maps the `keys` to/from +/// [`DeviceKey`]s. #[derive(Clone, Debug, Deserialize, Serialize)] struct DeviceKeyHelper { pub user_id: OwnedUserId,

for AttachmentSource +where + P: Into, +{ + fn from(value: P) -> Self { + Self::File(value.into()) + } +} diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs index e11ea3a8283..d552b4d38ff 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -22,7 +22,7 @@ use matrix_sdk::{ assert_let_timeout, attachment::AttachmentConfig, test_utils::mocks::MatrixMockServer, }; use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE}; -use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent}; +use matrix_sdk_ui::timeline::{AttachmentSource, EventSendState, RoomExt, TimelineItemContent}; use ruma::{ event_id, events::room::{message::MessageType, MediaSource}, @@ -52,7 +52,7 @@ fn get_filename_and_caption(msg: &MessageType) -> (&str, Option<&str>) { } #[async_test] -async fn test_send_attachment() { +async fn test_send_attachment_from_file() { let mock = MatrixMockServer::new().await; let client = mock.client_builder().build().await; @@ -148,6 +148,105 @@ async fn test_send_attachment() { assert!(timeline_stream.next().now_or_never().is_none()); } +#[async_test] +async fn test_send_attachment_from_bytes() { + let mock = MatrixMockServer::new().await; + let client = mock.client_builder().build().await; + + mock.mock_room_state_encryption().plain().mount().await; + + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = mock.sync_joined_room(&client, room_id).await; + let timeline = room.timeline().await.unwrap(); + + let (items, mut timeline_stream) = + timeline.subscribe_filter_map(|item| item.as_event().cloned()).await; + + assert!(items.is_empty()); + + let f = EventFactory::new(); + mock.sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hello").sender(&ALICE)), + ) + .await; + + // Sanity check. + assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + assert_eq!(msg.body(), "hello"); + + // No other updates. + assert!(timeline_stream.next().now_or_never().is_none()); + + // The data of the file. + let filename = "test.bin"; + let source = + AttachmentSource::Data { bytes: b"hello world".to_vec(), filename: filename.to_owned() }; + + // Set up mocks for the file upload. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(2)).set_body_json( + json!({ + "content_uri": "mxc://sdk.rs/media" + }), + )) + .mock_once() + .mount() + .await; + + mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await; + + // Queue sending of an attachment. + let config = AttachmentConfig::new().caption(Some("caption".to_owned())); + timeline.send_attachment(source, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap(); + + { + assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + + // Body is the caption, because there's both a caption and filename. + assert_eq!(msg.body(), "caption"); + assert_eq!(get_filename_and_caption(msg.msgtype()), (filename, Some("caption"))); + + // The URI refers to the local cache. + assert_let!(MessageType::File(file) = msg.msgtype()); + assert_let!(MediaSource::Plain(uri) = &file.source); + assert!(uri.to_string().contains("localhost")); + } + + // Eventually, the media is updated with the final MXC IDs… + sleep(Duration::from_secs(2)).await; + + { + assert_let_timeout!( + Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next() + ); + assert_let!(TimelineItemContent::Message(msg) = item.content()); + assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); + assert_eq!(get_filename_and_caption(msg.msgtype()), (filename, Some("caption"))); + + // The URI now refers to the final MXC URI. + assert_let!(MessageType::File(file) = msg.msgtype()); + assert_let!(MediaSource::Plain(uri) = &file.source); + assert_eq!(uri.to_string(), "mxc://sdk.rs/media"); + } + + // And eventually the event itself is sent. + { + assert_let_timeout!( + Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next() + ); + assert_matches!(item.send_state(), Some(EventSendState::Sent{ event_id }) => { + assert_eq!(event_id, event_id!("$media")); + }); + } + + // That's all, folks! + assert!(timeline_stream.next().now_or_never().is_none()); +} + #[async_test] async fn test_react_to_local_media() { let mock = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 52a99b3bc74..c9cbe917196 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file. - [**breaking**] Remove the `AttachmentConfig::with_thumbnail()` constructor and replace it with the `AttachmentConfig::thumbnail()` builder method. You should call `AttachmentConfig::new().thumbnail(thumbnail)` now instead. +- [**breaking**] `Room::send_attachment()` and `RoomSendQueue::send_attachment()` + now take any type that implements `Into` for the filename. ## [0.9.0] - 2024-12-18 diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index 4c1878f870e..2aaf6c38b56 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -240,7 +240,7 @@ impl<'a> IntoFuture for SendRawMessageLikeEvent<'a> { #[allow(missing_debug_implementations)] pub struct SendAttachment<'a> { room: &'a Room, - filename: &'a str, + filename: String, content_type: &'a Mime, data: Vec, config: AttachmentConfig, @@ -252,7 +252,7 @@ pub struct SendAttachment<'a> { impl<'a> SendAttachment<'a> { pub(crate) fn new( room: &'a Room, - filename: &'a str, + filename: String, content_type: &'a Mime, data: Vec, config: AttachmentConfig, diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 50ea5341984..6ea0470e7a9 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -1933,12 +1933,12 @@ impl Room { #[instrument(skip_all)] pub fn send_attachment<'a>( &'a self, - filename: &'a str, + filename: impl Into, content_type: &'a Mime, data: Vec, config: AttachmentConfig, ) -> SendAttachment<'a> { - SendAttachment::new(self, filename, content_type, data, config) + SendAttachment::new(self, filename.into(), content_type, data, config) } /// Prepare and send an attachment to this room. @@ -1971,7 +1971,7 @@ impl Room { #[instrument(skip_all)] pub(super) async fn prepare_and_send_attachment<'a>( &'a self, - filename: &'a str, + filename: String, content_type: &'a Mime, data: Vec, mut config: AttachmentConfig, @@ -2076,7 +2076,7 @@ impl Room { pub(crate) fn make_attachment_type( &self, content_type: &Mime, - filename: &str, + filename: String, source: MediaSource, caption: Option, formatted_caption: Option, @@ -2087,8 +2087,8 @@ impl Room { // body is the filename, and the filename is not set. // https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md let (body, filename) = match caption { - Some(caption) => (caption, Some(filename.to_owned())), - None => (filename.to_owned(), None), + Some(caption) => (caption, Some(filename)), + None => (filename, None), }; let (thumbnail_source, thumbnail_info) = thumbnail.unzip(); diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 7c32c8fa000..93a7aec7cb5 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -105,7 +105,7 @@ impl RoomSendQueue { #[instrument(skip_all, fields(event_txn))] pub async fn send_attachment( &self, - filename: &str, + filename: impl Into, content_type: Mime, data: Vec, mut config: AttachmentConfig, @@ -118,6 +118,7 @@ impl RoomSendQueue { return Err(RoomSendQueueError::RoomNotJoined); } + let filename = filename.into(); let upload_file_txn = TransactionId::new(); let send_event_txn = config.txn_id.map_or_else(ChildTransactionId::new, Into::into); From 8e75a940f7ee421ebf240b0f1560244c176dddfb Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 6 Jan 2025 23:25:06 +0100 Subject: [PATCH 863/979] Use Instant from web-time in more places (via ruma re-export) web-time's Instant type is already used elsewhere in the project. It is an alias for std's Instant type on most targets, but tries to call into JavaScript on wasm32-unknown-unknown (assuming that the wasm blob is used in from a browser context). Its Duration type is a plain re-export of std's Duration, even on wasm32-unknown-unknown. --- .../src/event_cache/store/memory_store.rs | 4 ++-- crates/matrix-sdk-common/src/store_locks.rs | 7 +++---- crates/matrix-sdk-common/src/tracing_timer.rs | 3 +-- crates/matrix-sdk-crypto/src/store/memorystore.rs | 5 ++--- crates/matrix-sdk-ui/src/room_list_service/state.rs | 7 ++----- crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs | 6 ++++-- .../tests/integration/room_list_service.rs | 12 ++++++------ crates/matrix-sdk/src/widget/machine/pending.rs | 3 +-- 8 files changed, 21 insertions(+), 26 deletions(-) diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 60ce6806d46..4606e91298e 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant}; +use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock}; use async_trait::async_trait; use matrix_sdk_common::{ @@ -20,7 +20,7 @@ use matrix_sdk_common::{ ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; -use ruma::{MxcUri, OwnedMxcUri, RoomId}; +use ruma::{time::Instant, MxcUri, OwnedMxcUri, RoomId}; use super::{EventCacheStore, EventCacheStoreError, Result}; use crate::{ diff --git a/crates/matrix-sdk-common/src/store_locks.rs b/crates/matrix-sdk-common/src/store_locks.rs index 08af735ed22..4b55d9c595a 100644 --- a/crates/matrix-sdk-common/src/store_locks.rs +++ b/crates/matrix-sdk-common/src/store_locks.rs @@ -500,10 +500,9 @@ mod tests { /// Some code that is shared by almost all `MemoryStore` implementations out /// there. pub mod memory_store_helper { - use std::{ - collections::{hash_map::Entry, HashMap}, - time::{Duration, Instant}, - }; + use std::collections::{hash_map::Entry, HashMap}; + + use ruma::time::{Duration, Instant}; pub fn try_take_leased_lock( leases: &mut HashMap, diff --git a/crates/matrix-sdk-common/src/tracing_timer.rs b/crates/matrix-sdk-common/src/tracing_timer.rs index 91ec4b9dc10..18e6488f526 100644 --- a/crates/matrix-sdk-common/src/tracing_timer.rs +++ b/crates/matrix-sdk-common/src/tracing_timer.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::time::Instant; - +use ruma::time::Instant; use tracing::{callsite::DefaultCallsite, Callsite as _}; /// A named RAII that will show on Drop how long its covered section took to diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 90557acf660..4a4e16dc53c 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -16,14 +16,13 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, convert::Infallible, sync::RwLock as StdRwLock, - time::Instant, }; use async_trait::async_trait; use matrix_sdk_common::store_locks::memory_store_helper::try_take_leased_lock; use ruma::{ - events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, - OwnedUserId, RoomId, TransactionId, UserId, + events::secret::request::SecretName, time::Instant, DeviceId, OwnedDeviceId, OwnedRoomId, + OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, }; use tokio::sync::RwLock; use tracing::warn; diff --git a/crates/matrix-sdk-ui/src/room_list_service/state.rs b/crates/matrix-sdk-ui/src/room_list_service/state.rs index 31d6bc4f9fd..076c4933fcf 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/state.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/state.rs @@ -14,14 +14,11 @@ //! States and actions for the `RoomList` state machine. -use std::{ - future::ready, - sync::Mutex, - time::{Duration, Instant}, -}; +use std::{future::ready, sync::Mutex}; use eyeball::{SharedObservable, Subscriber}; use matrix_sdk::{sliding_sync::Range, SlidingSync, SlidingSyncMode}; +use ruma::time::{Duration, Instant}; use super::Error; diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index 7207b8c2655..ce4c66c51db 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -21,13 +21,15 @@ use std::{ collections::HashMap, sync::{Arc, Mutex}, - time::{Duration, Instant}, }; use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; use matrix_sdk::{crypto::types::events::UtdCause, Client}; use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError}; -use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedServerName, UserId}; +use ruma::{ + time::{Duration, Instant}, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedServerName, UserId, +}; use tokio::{ spawn, sync::{Mutex as AsyncMutex, MutexGuard}, 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 9eb84c7bf7c..a1ca670a288 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -1,7 +1,4 @@ -use std::{ - ops::Not, - time::{Duration, Instant}, -}; +use std::ops::Not; use assert_matches::assert_matches; use eyeball_im::VectorDiff; @@ -27,8 +24,11 @@ use matrix_sdk_ui::{ RoomListService, }; use ruma::{ - api::client::room::create_room::v3::Request as CreateRoomRequest, event_id, - events::room::message::RoomMessageEventContent, mxc_uri, room_id, + api::client::room::create_room::v3::Request as CreateRoomRequest, + event_id, + events::room::message::RoomMessageEventContent, + mxc_uri, room_id, + time::{Duration, Instant}, }; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; diff --git a/crates/matrix-sdk/src/widget/machine/pending.rs b/crates/matrix-sdk/src/widget/machine/pending.rs index 068d14da8ba..162a798cdc3 100644 --- a/crates/matrix-sdk/src/widget/machine/pending.rs +++ b/crates/matrix-sdk/src/widget/machine/pending.rs @@ -15,9 +15,8 @@ //! A wrapper around a hash map that tracks pending requests and makes sure //! that expired requests are removed. -use std::time::{Duration, Instant}; - use indexmap::{map::Entry, IndexMap}; +use ruma::time::{Duration, Instant}; use tracing::warn; use uuid::Uuid; From 412fcab4dca6d9834c7285ddbe0d93d906059fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 7 Jan 2025 09:08:58 +0100 Subject: [PATCH 864/979] test: Await the device creation in the notification client redecryption test --- .../src/tests/timeline.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index ebc049be649..70d11ddc8af 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -526,6 +526,13 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { .await .expect("We should be able to check that the room is encrypted")); + // Create stream listening for devices. + let devices_stream = alice + .encryption() + .devices_stream() + .await + .expect("We should be able to listen to the devices stream"); + // Now here comes bob. let bob = TestClientBuilder::new("bob").use_sqlite().build().await.unwrap(); bob.encryption().wait_for_e2ee_initialization_tasks().await; @@ -569,6 +576,21 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { assert_eq!(bob_room.state(), RoomState::Joined); assert!(bob_room.is_encrypted().await.unwrap()); + // Now we need to wait for Bob's device to turn up. + let wait_for_bob_device = async { + pin_mut!(devices_stream); + + while let Some(devices) = devices_stream.next().await { + if devices.new.contains_key(bob.user_id().unwrap()) { + break; + } + } + }; + + timeout(Duration::from_secs(5), wait_for_bob_device) + .await + .expect("We should be able to load the room list"); + // Let's stop the sync so we don't receive the room key using the usual channel. sync_service.stop().await.expect("We should be able to stop the sync service"); From b7b88f58d299aa2eda8083fabd5805c234677de4 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 6 Jan 2025 17:34:30 +0100 Subject: [PATCH 865/979] feat!(send queue): make unrecoverable errors stop the sending queue Instead of keeping on handling unwedged events from the sending queue, it's now required to re-enable the send queue manually for the room that encountered the sending error, all the time. This is more consistent, and avoids weird behavior when a user would 1. send an event for which sending fails, in an unrecoverable manner, 2. send an event that's actually sendable. --- .../tests/integration/timeline/edit.rs | 3 +++ crates/matrix-sdk/src/send_queue.rs | 17 ++++++------ .../tests/integration/send_queue.rs | 27 ++++++++++++++++--- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index b1f0291854a..9c8e8b2da6c 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -260,6 +260,9 @@ async fn test_edit_local_echo() { let edit_message = item.content().as_message().unwrap(); assert_eq!(edit_message.body(), "hello, world"); + // Re-enable the room's queue. + timeline.room().send_queue().set_enabled(true); + // Observe the event being sent, and replacing the local echo. assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 1b9aec6d57c..82496b83190 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -622,6 +622,9 @@ impl RoomSendQueue { _ => false, }; + // Disable the queue for this room after any kind of error happened. + locally_enabled.store(false, Ordering::SeqCst); + if is_recoverable { warn!(txn_id = %txn_id, error = ?err, "Recoverable error when sending request: {err}, disabling send queue"); @@ -629,15 +632,11 @@ impl RoomSendQueue { // as not being sent anymore. queue.mark_as_not_being_sent(&txn_id).await; - // Let observers know about a failure *after* we've marked the item as not - // being sent anymore. Otherwise, there's a possible race where a caller - // might try to remove an item, while it's still marked as being sent, - // resulting in a cancellation failure. - - // Disable the queue for this room after a recoverable error happened. This - // should be the sign that this error is temporary (maybe network - // disconnected, maybe the server had a hiccup). - locally_enabled.store(false, Ordering::SeqCst); + // Let observers know about a failure *after* we've + // marked the item as not being sent anymore. Otherwise, + // there's a possible race where a caller might try to + // remove an item, while it's still marked as being + // sent, resulting in a cancellation failure. } else { warn!(txn_id = %txn_id, error = ?err, "Unrecoverable error when sending request: {err}"); diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 407e595e54e..32a8d92087b 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1197,6 +1197,11 @@ async fn test_edit_wakes_the_sending_task() { // Now edit the event's content (imagine we make it "shorter"). drop(send_mock_scope); + + // And re-enable the send queue (it was disabled since the edit caused a + // permanent error). + q.set_enabled(true); + mock.mock_room_send().ok(event_id!("$1")).mount().await; let edited = handle @@ -1436,10 +1441,13 @@ async fn test_unrecoverable_errors() { // too. assert_update!(watch => error { recoverable=false, txn=txn1 }); + // The permanent error disables the room send queue. + assert!(!room.send_queue().is_enabled()); + room.send_queue().set_enabled(true); + // The second message will be properly sent. assert_update!(watch => sent { txn=txn2, event_id=event_id!("$42") }); - // No queue is being disabled, because the error was unrecoverable. assert!(room.send_queue().is_enabled()); assert!(client.send_queue().is_enabled()); } @@ -1495,10 +1503,15 @@ async fn test_unwedge_unrecoverable_errors() { // too. assert_update!(watch => error { recoverable=false, txn=txn1 }); - // No queue is being disabled, because the error was unrecoverable. - assert!(room.send_queue().is_enabled()); + // The queue is disabled, because it ran into an error. + assert!(!room.send_queue().is_enabled()); + // Not *all* rooms' queues are disabled, though. assert!(client.send_queue().is_enabled()); + // Re-enable the room queue. + room.send_queue().set_enabled(true); + assert!(watch.is_empty()); + // Unwedge the previously failed message and try sending it again send_handle.unwedge().await.unwrap(); @@ -2105,12 +2118,15 @@ async fn test_unwedging_media_upload() { let error = assert_update!(watch => error { recoverable=false, txn=event_txn }); let error = error.as_client_api_error().unwrap(); assert_eq!(error.status_code, 413); - assert!(q.is_enabled()); + assert!(!q.is_enabled()); // Mount the mock for the upload and sending the event. mock.mock_upload().ok(mxc_uri!("mxc://sdk.rs/media")).mock_once().mount().await; mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + // Re-enable the room queue. + q.set_enabled(true); + // Unwedge the upload. send_handle.unwedge().await.unwrap(); @@ -2202,6 +2218,9 @@ async fn test_media_event_is_sent_in_order() { assert_update!(watch => error { recoverable = false, txn = text_txn }); } + // Re-enable the send queue after the permanent error. + q.set_enabled(true); + // We'll then send a media event, and then a text event with success. mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await; mock.mock_room_send().ok(event_id!("$text")).mock_once().mount().await; From bcad0a305961316c27eca965b077d23778cf82f6 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 6 Jan 2025 17:38:47 +0100 Subject: [PATCH 866/979] test(timeline): rewrite a test to use the MatrixMockServer instead --- .../tests/integration/timeline/edit.rs | 38 ++++--------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 9c8e8b2da6c..d07af71056f 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -165,38 +165,19 @@ async fn test_edit() { #[async_test] async fn test_edit_local_echo() { let room_id = room_id!("!a98sd12bjh:example.org"); - let (client, server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; + let room = server.sync_joined_room(&client, room_id).await; - mock_encryption_state(&server, false).await; + server.mock_room_state_encryption().plain().mount().await; - let room = client.get_room(room_id).unwrap(); let timeline = room.timeline().await.unwrap(); let (_, mut timeline_stream) = timeline.subscribe().await; - sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - - mock_sync(&server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - server.reset().await; - - mock_encryption_state(&server, false).await; - let mounted_send = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(413).set_body_json(json!({ - "errcode": "M_TOO_LARGE", - }))) - .expect(1) - .mount_as_scoped(&server) - .await; + let mounted_send = + server.mock_room_send().error_too_large().mock_once().mount_as_scoped().await; // Redacting a local event works. timeline.send(RoomMessageEventContent::text_plain("hello, just you").into()).await.unwrap(); @@ -230,12 +211,7 @@ async fn test_edit_local_echo() { // retry (the room's send queue is not blocked, since the one event it couldn't // send failed in an unrecoverable way). drop(mounted_send); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$1" }))) - .expect(1) - .mount(&server) - .await; + server.mock_room_send().ok(event_id!("$1")).mount().await; // Editing the local echo works, since it was in the failed state. timeline From 5110aa64aa5cfab993c0f8b6ad743e2eabe30115 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 6 Jan 2025 18:37:08 +0100 Subject: [PATCH 867/979] doc(base): update lying doc comment of `compute_display_name` It claimed that it would immediately return when the cached display name value was computed, but that's absolutely not the case. Spotted while reviewing a PR updating `iamb` to the latest version of the SDK. --- crates/matrix-sdk-base/src/rooms/normal.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index bd6da19832d..7dac462390b 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -623,15 +623,15 @@ impl Room { self.inner.read().active_room_call_participants() } - /// Return the cached display name of the room if it was provided via sync, - /// or otherwise calculate it, taking into account its name, aliases and - /// members. + /// Calculate a room's display name, taking into account its name, aliases + /// and members. /// /// The display name is calculated according to [this algorithm][spec]. /// - /// This is automatically recomputed on every successful sync, and the - /// cached result can be retrieved in - /// [`Self::cached_display_name`]. + /// ⚠ This may be slowish to compute. As such, the result is cached and can + /// be retrieved via [`Room::cached_display_name`], which should be + /// preferred in general. Indeed, the cached value is automatically + /// recomputed on every sync. /// /// [spec]: pub async fn compute_display_name(&self) -> StoreResult { From 618e47250de7ac3f8f4e51a4b95ba7bc19a7182f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 7 Jan 2025 14:46:49 +0100 Subject: [PATCH 868/979] feat!(base): reintroduce `Room::display_name` `compute_display_name` is made private again, and used only within the base crate. A new public counterpart `Room::display_name` is introduced, which returns a cached value for, or computes (and fills in cache) the display name. This is simpler to use, and likely what most users expect anyways. --- crates/matrix-sdk-base/CHANGELOG.md | 10 +++++ crates/matrix-sdk-base/src/rooms/normal.rs | 37 +++++++++++++++---- .../matrix-sdk-ui/src/notification_client.rs | 2 +- crates/matrix-sdk/src/room_preview.rs | 2 +- .../tests/integration/room/common.rs | 15 +++----- examples/oidc_cli/src/main.rs | 2 +- examples/persist_session/src/main.rs | 2 +- 7 files changed, 49 insertions(+), 21 deletions(-) diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 988064b396a..6716adf5e44 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -6,6 +6,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Breaking changes + +- Replaced `Room::compute_display_name` with the reintroduced `Room::display_name()`. The new + method computes a display name, or return a cached value from the previous successful computation. + If you need a sync variant, consider using `Room::cached_display_name()`. + +### Features + +### Bug Fixes + ## [0.9.0] - 2024-12-18 ### Features diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 7dac462390b..204a64d512a 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -623,18 +623,40 @@ impl Room { self.inner.read().active_room_call_participants() } - /// Calculate a room's display name, taking into account its name, aliases - /// and members. + /// Calculate a room's display name, or return the cached value, taking into + /// account its name, aliases and members. + /// + /// The display name is calculated according to [this algorithm][spec]. + /// + /// While the underlying computation can be slow, the result is cached and + /// returned on the following calls. The cache is also filled on every + /// successful sync, since a sync may cause a change in the display + /// name. + /// + /// If you need a variant that's sync (but with the drawback that it returns + /// an `Option`), consider using [`Room::cached_display_name`]. + /// + /// [spec]: + pub async fn display_name(&self) -> StoreResult { + if let Some(name) = self.cached_display_name() { + Ok(name) + } else { + self.compute_display_name().await + } + } + + /// Force recalculating a room's display name, taking into account its name, + /// aliases and members. /// /// The display name is calculated according to [this algorithm][spec]. /// /// ⚠ This may be slowish to compute. As such, the result is cached and can - /// be retrieved via [`Room::cached_display_name`], which should be - /// preferred in general. Indeed, the cached value is automatically - /// recomputed on every sync. + /// be retrieved via [`Room::cached_display_name`] (sync, returns an option) + /// or [`Room::display_name`] (async, always returns a value), which should + /// be preferred in general. /// /// [spec]: - pub async fn compute_display_name(&self) -> StoreResult { + pub(crate) async fn compute_display_name(&self) -> StoreResult { enum DisplayNameOrSummary { Summary(RoomSummary), DisplayName(RoomDisplayName), @@ -871,8 +893,7 @@ impl Room { /// Returns the cached computed display name, if available. /// - /// This cache is refilled every time we call - /// [`Self::compute_display_name`]. + /// This cache is refilled every time we call [`Self::display_name`]. pub fn cached_display_name(&self) -> Option { self.inner.read().cached_display_name.clone() } diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 48396c5777d..5af760e713f 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -740,7 +740,7 @@ impl NotificationItem { sender_display_name, sender_avatar_url, is_sender_name_ambiguous, - room_computed_display_name: room.compute_display_name().await?.to_string(), + room_computed_display_name: room.display_name().await?.to_string(), room_avatar_url: room.avatar_url().map(|s| s.to_string()), room_canonical_alias: room.canonical_alias().map(|c| c.to_string()), is_direct_message_room: room.is_direct().await?, diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index bcb52b2e6d6..9c25190f4b7 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -130,7 +130,7 @@ impl RoomPreview { pub(crate) async fn from_joined(room: &Room) -> Self { let is_direct = room.is_direct().await.ok(); - let display_name = room.compute_display_name().await.ok().map(|name| name.to_string()); + let display_name = room.display_name().await.ok().map(|name| name.to_string()); Self::from_room_info( room.clone_info(), diff --git a/crates/matrix-sdk/tests/integration/room/common.rs b/crates/matrix-sdk/tests/integration/room/common.rs index 3021a114a8d..04a1dc7fc10 100644 --- a/crates/matrix-sdk/tests/integration/room/common.rs +++ b/crates/matrix-sdk/tests/integration/room/common.rs @@ -65,7 +65,7 @@ async fn test_calculate_room_names_from_summary() { assert_eq!( RoomDisplayName::Calculated("example2".to_owned()), - room.compute_display_name().await.unwrap() + room.display_name().await.unwrap() ); } @@ -83,10 +83,7 @@ async fn test_room_names() { assert_eq!(client.rooms().len(), 1); let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - assert_eq!( - RoomDisplayName::Aliased("tutorial".to_owned()), - room.compute_display_name().await.unwrap() - ); + assert_eq!(RoomDisplayName::Aliased("tutorial".to_owned()), room.display_name().await.unwrap()); // Room with a name. mock_sync(&server, &*test_json::INVITE_SYNC, None).await; @@ -99,7 +96,7 @@ async fn test_room_names() { assert_eq!( RoomDisplayName::Named("My Room Name".to_owned()), - invited_room.compute_display_name().await.unwrap() + invited_room.display_name().await.unwrap() ); let mut sync_builder = SyncResponseBuilder::new(); @@ -137,7 +134,7 @@ async fn test_room_names() { RoomDisplayName::Calculated( "user_0, user_1, user_10, user_11, user_12, and 10 others".to_owned() ), - room.compute_display_name().await.unwrap() + room.display_name().await.unwrap() ); // Room with joined and invited members. @@ -185,7 +182,7 @@ async fn test_room_names() { assert_eq!( RoomDisplayName::Calculated("Bob, example1".to_owned()), - room.compute_display_name().await.unwrap() + room.display_name().await.unwrap() ); // Room with only left members. @@ -205,7 +202,7 @@ async fn test_room_names() { assert_eq!( RoomDisplayName::EmptyWas("user_0, user_1, user_2".to_owned()), - room.compute_display_name().await.unwrap() + room.display_name().await.unwrap() ); } diff --git a/examples/oidc_cli/src/main.rs b/examples/oidc_cli/src/main.rs index 3724d3bf519..8761bcef361 100644 --- a/examples/oidc_cli/src/main.rs +++ b/examples/oidc_cli/src/main.rs @@ -935,7 +935,7 @@ async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { } let MessageType::Text(text_content) = &event.content.msgtype else { return }; - let room_name = match room.compute_display_name().await { + let room_name = match room.display_name().await { Ok(room_name) => room_name.to_string(), Err(error) => { println!("Error getting room display name: {error}"); diff --git a/examples/persist_session/src/main.rs b/examples/persist_session/src/main.rs index 3ce375dd8b2..81b40545da1 100644 --- a/examples/persist_session/src/main.rs +++ b/examples/persist_session/src/main.rs @@ -297,7 +297,7 @@ async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { } let MessageType::Text(text_content) = &event.content.msgtype else { return }; - let room_name = match room.compute_display_name().await { + let room_name = match room.display_name().await { Ok(room_name) => room_name.to_string(), Err(error) => { println!("Error getting room display name: {error}"); From 8205da898e11c34b77b13fe4794d0b2e1bcdd2a8 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 6 Jan 2025 17:57:25 +0100 Subject: [PATCH 869/979] feat(send queue): allow setting intentional mentions in media captions edits Fixes #4302. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 24 +++- crates/matrix-sdk-ui/src/timeline/mod.rs | 4 +- crates/matrix-sdk/src/room/edit.rs | 120 ++++++++++++++++-- crates/matrix-sdk/src/send_queue.rs | 5 +- crates/matrix-sdk/src/send_queue/upload.rs | 7 +- .../tests/integration/send_queue.rs | 108 +++++++++++++++- 6 files changed, 240 insertions(+), 28 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 30660a00a7c..2c4798a63a6 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -71,8 +71,8 @@ use crate::{ event::EventOrTransactionId, helpers::unwrap_or_clone_arc, ruma::{ - AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, PollKind, ThumbnailInfo, - VideoInfo, + AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind, + ThumbnailInfo, VideoInfo, }, task_handle::TaskHandle, utils::Timestamp, @@ -1295,22 +1295,32 @@ impl From for ruma::api::client::receipt::create_receipt::v3::Recei #[derive(Clone, uniffi::Enum)] pub enum EditedContent { - RoomMessage { content: Arc }, - MediaCaption { caption: Option, formatted_caption: Option }, - PollStart { poll_data: PollData }, + RoomMessage { + content: Arc, + }, + MediaCaption { + caption: Option, + formatted_caption: Option, + mentions: Option, + }, + PollStart { + poll_data: PollData, + }, } impl TryFrom for SdkEditedContent { type Error = ClientError; + fn try_from(value: EditedContent) -> Result { match value { EditedContent::RoomMessage { content } => { Ok(SdkEditedContent::RoomMessage((*content).clone())) } - EditedContent::MediaCaption { caption, formatted_caption } => { + EditedContent::MediaCaption { caption, formatted_caption, mentions } => { Ok(SdkEditedContent::MediaCaption { caption, formatted_caption: formatted_caption.map(Into::into), + mentions: mentions.map(Into::into), }) } EditedContent::PollStart { poll_data } => { @@ -1332,12 +1342,14 @@ impl TryFrom for SdkEditedContent { fn create_caption_edit( caption: Option, formatted_caption: Option, + mentions: Option, ) -> EditedContent { let formatted_caption = formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); EditedContent::MediaCaption { caption, formatted_caption: formatted_caption.as_ref().map(Into::into), + mentions, } } diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index e4ce251b5fc..df2ce390a79 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -486,9 +486,9 @@ impl Timeline { } } - EditedContent::MediaCaption { caption, formatted_caption } => { + EditedContent::MediaCaption { caption, formatted_caption, mentions } => { if handle - .edit_media_caption(caption, formatted_caption) + .edit_media_caption(caption, formatted_caption, mentions) .await .map_err(RoomSendQueueError::StorageError)? { diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index 1f9381a334d..e0644ac8fba 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -28,8 +28,8 @@ use ruma::{ RoomMessageEventContentWithoutRelation, }, AnyMessageLikeEvent, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, - AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEvent, OriginalMessageLikeEvent, - SyncMessageLikeEvent, + AnySyncTimelineEvent, AnyTimelineEvent, Mentions, MessageLikeEvent, + OriginalMessageLikeEvent, SyncMessageLikeEvent, }, EventId, RoomId, UserId, }; @@ -54,6 +54,10 @@ pub enum EditedContent { /// /// Set to `None` to remove an existing formatted caption. formatted_caption: Option, + + /// New set of intentional mentions to be included in the edited + /// caption. + mentions: Option, }, /// The content is a new poll start. @@ -196,7 +200,7 @@ async fn make_edit_event( Ok(replacement.into()) } - EditedContent::MediaCaption { caption, formatted_caption } => { + EditedContent::MediaCaption { caption, formatted_caption, mentions } => { // Handle edits of m.room.message. let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original)) = message_like_event @@ -207,13 +211,13 @@ async fn make_edit_event( }); }; - let mentions = original.content.mentions.clone(); + let original_mentions = original.content.mentions.clone(); let replied_to_original_room_msg = extract_replied_to(source, room_id, original.content.relates_to.clone()).await; let mut prev_content = original.content; - if !update_media_caption(&mut prev_content, caption, formatted_caption) { + if !update_media_caption(&mut prev_content, caption, formatted_caption, mentions) { return Err(EditError::IncompatibleEditType { target: prev_content.msgtype.msgtype().to_owned(), new_content: "caption for a media room message", @@ -221,7 +225,7 @@ async fn make_edit_event( } let replacement = prev_content.make_replacement( - ReplacementMetadata::new(event_id.to_owned(), mentions), + ReplacementMetadata::new(event_id.to_owned(), original_mentions), replied_to_original_room_msg.as_ref(), ); @@ -282,7 +286,10 @@ pub(crate) fn update_media_caption( content: &mut RoomMessageEventContent, caption: Option, formatted_caption: Option, + mentions: Option, ) -> bool { + content.mentions = mentions; + match &mut content.msgtype { MessageType::Audio(event) => { set_caption!(event, caption); @@ -358,9 +365,9 @@ mod tests { event_id, events::{ room::message::{MessageType, Relation, RoomMessageEventContentWithoutRelation}, - AnyMessageLikeEventContent, AnySyncTimelineEvent, + AnyMessageLikeEventContent, AnySyncTimelineEvent, Mentions, }, - owned_mxc_uri, room_id, + owned_mxc_uri, owned_user_id, room_id, serde::Raw, user_id, EventId, OwnedEventId, }; @@ -506,7 +513,11 @@ mod tests { room_id, own_user_id, event_id, - EditedContent::MediaCaption { caption: Some("yo".to_owned()), formatted_caption: None }, + EditedContent::MediaCaption { + caption: Some("yo".to_owned()), + formatted_caption: None, + mentions: None, + }, ) .await .unwrap_err(); @@ -543,6 +554,7 @@ mod tests { EditedContent::MediaCaption { caption: Some("Best joke ever".to_owned()), formatted_caption: None, + mentions: None, }, ) .await @@ -572,12 +584,12 @@ mod tests { let mut cache = TestEventCache::default(); let f = EventFactory::new(); - let event: SyncTimelineEvent = f + let event = f .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll")) .caption(Some("caption".to_owned()), None) .event_id(event_id) .sender(own_user_id) - .into(); + .into_sync(); { // Sanity checks. @@ -602,7 +614,7 @@ mod tests { own_user_id, event_id, // Remove the caption by setting it to None. - EditedContent::MediaCaption { caption: None, formatted_caption: None }, + EditedContent::MediaCaption { caption: None, formatted_caption: None, mentions: None }, ) .await .unwrap(); @@ -621,6 +633,90 @@ mod tests { assert!(new_image.formatted_caption().is_none()); } + #[async_test] + async fn test_add_media_caption_mention() { + let event_id = event_id!("$1"); + let own_user_id = user_id!("@me:saucisse.bzh"); + + let filename = "rickroll.gif"; + + let mut cache = TestEventCache::default(); + let f = EventFactory::new(); + + // Start with a media event that has no mentions. + let event = f + .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll")) + .event_id(event_id) + .sender(own_user_id) + .into_sync(); + + { + // Sanity checks. + let event = event.raw().deserialize().unwrap(); + assert_let!(AnySyncTimelineEvent::MessageLike(event) = event); + assert_let!( + AnyMessageLikeEventContent::RoomMessage(msg) = event.original_content().unwrap() + ); + assert_matches!(msg.mentions, None); + } + + cache.events.insert(event_id.to_owned(), event); + + let room_id = room_id!("!galette:saucisse.bzh"); + + // Add an intentional mention in the caption. + let mentioned_user_id = owned_user_id!("@crepe:saucisse.bzh"); + let edit_event = { + let mentions = Mentions::with_user_ids([mentioned_user_id.clone()]); + make_edit_event( + cache, + room_id, + own_user_id, + event_id, + EditedContent::MediaCaption { + caption: None, + formatted_caption: None, + mentions: Some(mentions), + }, + ) + .await + .unwrap() + }; + + assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event); + assert_let!(MessageType::Image(image) = msg.msgtype); + + assert!(image.caption().is_none()); + assert!(image.formatted_caption().is_none()); + + // The raw event doesn't contain the mention :( + // TODO: this is a bug in Ruma! When Ruma gets upgraded in the SDK, this test + // may start failing. In this case, remove the following code, and replace it + // with the commented code below. + + assert_matches!(msg.mentions, None); + + /* + // The raw event contains the mention. + assert_let!(Some(mentions) = msg.mentions); + assert!(!mentions.room); + assert_eq!( + mentions.user_ids.into_iter().collect::>(), + vec![mentioned_user_id.clone()] + ); + */ + + assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to); + assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype); + assert!(new_image.caption().is_none()); + assert!(new_image.formatted_caption().is_none()); + + // The replacement contains the mention. + assert_let!(Some(mentions) = repl.new_content.mentions); + assert!(!mentions.room); + assert_eq!(mentions.user_ids.into_iter().collect::>(), vec![mentioned_user_id]); + } + #[async_test] async fn test_make_edit_event_success_with_response() { let event_id = event_id!("$1"); diff --git a/crates/matrix-sdk/src/send_queue.rs b/crates/matrix-sdk/src/send_queue.rs index 82496b83190..749917b19f5 100644 --- a/crates/matrix-sdk/src/send_queue.rs +++ b/crates/matrix-sdk/src/send_queue.rs @@ -159,7 +159,7 @@ use ruma::{ message::{FormattedBody, RoomMessageEventContent}, MediaSource, }, - AnyMessageLikeEventContent, EventContent as _, + AnyMessageLikeEventContent, EventContent as _, Mentions, }, serde::Raw, OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId, @@ -1995,12 +1995,13 @@ impl SendHandle { &self, caption: Option, formatted_caption: Option, + mentions: Option, ) -> Result { if let Some(new_content) = self .room .inner .queue - .edit_media_caption(&self.transaction_id, caption, formatted_caption) + .edit_media_caption(&self.transaction_id, caption, formatted_caption, mentions) .await? { trace!("successful edit of media caption"); diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 93a7aec7cb5..1d951b7caf2 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -26,7 +26,7 @@ use mime::Mime; use ruma::{ events::{ room::message::{FormattedBody, MessageType, RoomMessageEventContent}, - AnyMessageLikeEventContent, + AnyMessageLikeEventContent, Mentions, }, OwnedTransactionId, TransactionId, }; @@ -489,6 +489,7 @@ impl QueueStorage { txn: &TransactionId, caption: Option, formatted_caption: Option, + mentions: Option, ) -> Result, RoomSendQueueStorageError> { // This error will be popular here. use RoomSendQueueStorageError::InvalidMediaCaptionEdit; @@ -523,7 +524,7 @@ impl QueueStorage { return Err(InvalidMediaCaptionEdit); }; - if !update_media_caption(&mut local_echo, caption, formatted_caption) { + if !update_media_caption(&mut local_echo, caption, formatted_caption, mentions) { return Err(InvalidMediaCaptionEdit); } @@ -562,7 +563,7 @@ impl QueueStorage { return Err(InvalidMediaCaptionEdit); }; - if !update_media_caption(&mut content, caption, formatted_caption) { + if !update_media_caption(&mut content, caption, formatted_caption, mentions) { return Err(InvalidMediaCaptionEdit); } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 32a8d92087b..8b16194dba2 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -2650,7 +2650,8 @@ async fn test_update_caption_while_sending_media() { assert_eq!(local_content.filename(), filename); // We can edit the caption while the file is being uploaded. - let edited = upload_handle.edit_media_caption(Some("caption".to_owned()), None).await.unwrap(); + let edited = + upload_handle.edit_media_caption(Some("caption".to_owned()), None, None).await.unwrap(); assert!(edited); { @@ -2759,7 +2760,8 @@ async fn test_update_caption_before_event_is_sent() { assert!(watch.is_empty()); // We can edit the caption here. - let edited = upload_handle.edit_media_caption(Some("caption".to_owned()), None).await.unwrap(); + let edited = + upload_handle.edit_media_caption(Some("caption".to_owned()), None, None).await.unwrap(); assert!(edited); // The media event is updated with the captions. @@ -2786,6 +2788,105 @@ async fn test_update_caption_before_event_is_sent() { assert!(watch.is_empty()); } +#[async_test] +async fn test_add_mention_to_caption_before_media_sent() { + let mock = MatrixMockServer::new().await; + + // Mark the room as joined. + let room_id = room_id!("!a:b.c"); + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, room_id).await; + + let q = room.send_queue(); + + let (local_echoes, mut watch) = q.subscribe().await.unwrap(); + assert!(local_echoes.is_empty()); + + // Prepare endpoints. + mock.mock_room_state_encryption().plain().mount().await; + + // File upload will take a second. + mock.mock_upload() + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(1)).set_body_json( + json!({ + "content_uri": "mxc://sdk.rs/media" + }), + )) + .mock_once() + .named("file upload") + .mount() + .await; + + // Sending of the media event will succeed. + mock.mock_room_send() + .ok(event_id!("$media")) + .mock_once() + .named("send event") + .mock_once() + .mount() + .await; + + // Send the media. + assert!(watch.is_empty()); + + let (upload_handle, filename) = queue_attachment_no_thumbnail(&q).await; + + // Let the upload request start. + sleep(Duration::from_millis(300)).await; + + // Stop the send queue before upload is done. This will stall sending of the + // media event. + q.set_enabled(false); + + let (upload_txn, _send_handle, content) = assert_update!(watch => local echo event); + assert_let!(MessageType::Image(local_content) = content.msgtype); + assert_eq!(local_content.filename(), filename); + + // Wait for the media to be uploaded. + sleep(Duration::from_secs(1)).await; + assert_update!(watch => uploaded { related_to = upload_txn, mxc = mxc_uri!("mxc://sdk.rs/media") }); + + // The media event is updated with the remote MXC ID. + { + let new_content = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(MessageType::Image(image) = new_content.msgtype); + assert_eq!(image.filename(), filename); + assert_eq!(image.caption(), None); + assert!(image.formatted_caption().is_none()); + + let mxc = as_variant!(image.source, MediaSource::Plain).unwrap(); + assert!(!mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); + }; + + assert!(watch.is_empty()); + + // We can edit the caption here. + let mentioned_user_id = owned_user_id!("@damir:rust.sdk"); + let mentions = Mentions::with_user_ids([mentioned_user_id.clone()]); + let edited = upload_handle + .edit_media_caption(Some("caption".to_owned()), None, Some(mentions)) + .await + .unwrap(); + assert!(edited); + + // The media event is updated with the captions, including the mention. + { + let edit_msg = assert_update!(watch => edit local echo { txn = upload_txn }); + assert_let!(Some(mentions) = edit_msg.mentions); + assert!(!mentions.room); + assert_eq!(mentions.user_ids.into_iter().collect::>(), vec![mentioned_user_id]); + } + + // Re-enable the send queue. + q.set_enabled(true); + + // Then the event is sent. + assert_update!(watch => sent { txn = upload_txn, }); + + // That's all, folks! + assert!(watch.is_empty()); +} + #[async_test] async fn test_update_caption_while_sending_media_event() { let mock = MatrixMockServer::new().await; @@ -2874,7 +2975,8 @@ async fn test_update_caption_while_sending_media_event() { }; // We can edit the caption while the event is beint sent. - let edited = upload_handle.edit_media_caption(Some("caption".to_owned()), None).await.unwrap(); + let edited = + upload_handle.edit_media_caption(Some("caption".to_owned()), None, None).await.unwrap(); assert!(edited); // The media event is updated with the captions. From 2ef14ded41476bd5cc56d224c465b3560f156005 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 7 Jan 2025 17:25:52 +0100 Subject: [PATCH 870/979] refactor(event cache): a few `AllEventsCache` refactorings (#4471) I was investigating a potential deadlock with the event cache storage, and only found a few places where to make the code a bit more idiomatic and more readable. --- crates/matrix-sdk/src/event_cache/mod.rs | 116 +++++++++++++++- crates/matrix-sdk/src/event_cache/room/mod.rs | 129 ++---------------- 2 files changed, 124 insertions(+), 121 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 6f288583992..f85b85bc337 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -45,7 +45,12 @@ use matrix_sdk_common::executor::{spawn, JoinHandle}; use once_cell::sync::OnceCell; use room::RoomEventCacheState; use ruma::{ - events::{relation::RelationType, AnySyncEphemeralRoomEvent}, + events::{ + relation::RelationType, + room::{message::Relation, redaction::SyncRoomRedactionEvent}, + AnyMessageLikeEventContent, AnySyncEphemeralRoomEvent, AnySyncMessageLikeEvent, + AnySyncTimelineEvent, + }, serde::Raw, EventId, OwnedEventId, OwnedRoomId, RoomId, }; @@ -363,6 +368,115 @@ impl AllEventsCache { self.events.clear(); self.relations.clear(); } + + /// If the event is related to another one, its id is added to the relations + /// map. + fn append_related_event(&mut self, event: &SyncTimelineEvent) { + // Handle and cache events and relations. + let Ok(AnySyncTimelineEvent::MessageLike(ev)) = event.raw().deserialize() else { + return; + }; + + // Handle redactions separately, as their logic is slightly different. + if let AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(ev)) = &ev { + if let Some(redacted_event_id) = ev.content.redacts.as_ref().or(ev.redacts.as_ref()) { + self.relations + .entry(redacted_event_id.to_owned()) + .or_default() + .insert(ev.event_id.to_owned(), RelationType::Replacement); + } + return; + } + + let relationship = match ev.original_content() { + Some(AnyMessageLikeEventContent::RoomMessage(c)) => { + if let Some(relation) = c.relates_to { + match relation { + Relation::Replacement(replacement) => { + Some((replacement.event_id, RelationType::Replacement)) + } + Relation::Reply { in_reply_to } => { + Some((in_reply_to.event_id, RelationType::Reference)) + } + Relation::Thread(thread) => Some((thread.event_id, RelationType::Thread)), + // Do nothing for custom + _ => None, + } + } else { + None + } + } + Some(AnyMessageLikeEventContent::PollResponse(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::PollEnd(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::UnstablePollResponse(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::UnstablePollEnd(c)) => { + Some((c.relates_to.event_id, RelationType::Reference)) + } + Some(AnyMessageLikeEventContent::Reaction(c)) => { + Some((c.relates_to.event_id, RelationType::Annotation)) + } + _ => None, + }; + + if let Some(relationship) = relationship { + self.relations + .entry(relationship.0) + .or_default() + .insert(ev.event_id().to_owned(), relationship.1); + } + } + + /// Looks for related event ids for the passed event id, and appends them to + /// the `results` parameter. Then it'll recursively get the related + /// event ids for those too. + fn collect_related_events( + &self, + event_id: &EventId, + filter: Option<&[RelationType]>, + ) -> Vec { + let mut results = Vec::new(); + self.collect_related_events_rec(event_id, filter, &mut results); + results + } + + fn collect_related_events_rec( + &self, + event_id: &EventId, + filter: Option<&[RelationType]>, + results: &mut Vec, + ) { + let Some(related_event_ids) = self.relations.get(event_id) else { + return; + }; + + for (related_event_id, relation_type) in related_event_ids { + if let Some(filter) = filter { + if !filter.contains(relation_type) { + continue; + } + } + + // If the event was already added to the related ones, skip it. + if results.iter().any(|event| { + event.event_id().is_some_and(|added_related_event_id| { + added_related_event_id == *related_event_id + }) + }) { + continue; + } + + if let Some((_, ev)) = self.events.get(related_event_id) { + results.push(ev.clone()); + self.collect_related_events_rec(related_event_id, filter, results); + } + } + } } struct EventCacheInner { diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index b0efde1395d..0ef69e36096 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -23,18 +23,13 @@ use matrix_sdk_base::{ sync::{JoinedRoomUpdate, LeftRoomUpdate, Timeline}, }; use ruma::{ - events::{ - relation::RelationType, - room::{message::Relation, redaction::SyncRoomRedactionEvent}, - AnyMessageLikeEventContent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent, - AnySyncMessageLikeEvent, AnySyncTimelineEvent, - }, + events::{relation::RelationType, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent}, serde::Raw, EventId, OwnedEventId, OwnedRoomId, }; use tokio::sync::{ broadcast::{Receiver, Sender}, - Notify, RwLock, RwLockReadGuard, RwLockWriteGuard, + Notify, RwLock, }; use tracing::{debug, trace, warn}; @@ -118,12 +113,10 @@ impl RoomEventCache { event_id: &EventId, filter: Option>, ) -> Option<(SyncTimelineEvent, Vec)> { - let mut relation_events = Vec::new(); - let cache = self.inner.all_events.read().await; if let Some((_, event)) = cache.events.get(event_id) { - Self::collect_related_events(&cache, event_id, &filter, &mut relation_events); - Some((event.clone(), relation_events)) + let related_events = cache.collect_related_events(event_id, filter.as_deref()); + Some((event.clone(), related_events)) } else { None } @@ -150,39 +143,6 @@ impl RoomEventCache { Ok(()) } - /// Looks for related event ids for the passed event id, and appends them to - /// the `results` parameter. Then it'll recursively get the related - /// event ids for those too. - fn collect_related_events( - cache: &RwLockReadGuard<'_, AllEventsCache>, - event_id: &EventId, - filter: &Option>, - results: &mut Vec, - ) { - if let Some(related_event_ids) = cache.relations.get(event_id) { - for (related_event_id, relation_type) in related_event_ids { - if let Some(filter) = filter { - if !filter.contains(relation_type) { - continue; - } - } - - // If the event was already added to the related ones, skip it. - if results.iter().any(|e| { - e.event_id().is_some_and(|added_related_event_id| { - added_related_event_id == *related_event_id - }) - }) { - continue; - } - if let Some((_, ev)) = cache.events.get(related_event_id) { - results.push(ev.clone()); - Self::collect_related_events(cache, related_event_id, filter, results); - } - } - } - } - /// Save a single event in the event cache, for further retrieval with /// [`Self::event`]. // TODO: This doesn't insert the event into the linked chunk. In the future @@ -192,7 +152,7 @@ impl RoomEventCache { if let Some(event_id) = event.event_id() { let mut cache = self.inner.all_events.write().await; - self.inner.append_related_event(&mut cache, &event); + cache.append_related_event(&event); cache.events.insert(event_id, (self.inner.room_id.clone(), event)); } else { warn!("couldn't save event without event id in the event cache"); @@ -209,7 +169,7 @@ impl RoomEventCache { let mut cache = self.inner.all_events.write().await; for event in events { if let Some(event_id) = event.event_id() { - self.inner.append_related_event(&mut cache, &event); + cache.append_related_event(&event); cache.events.insert(event_id, (self.inner.room_id.clone(), event)); } else { warn!("couldn't save event without event id in the event cache"); @@ -235,9 +195,9 @@ pub(super) struct RoomEventCacheInner { /// State for this room's event cache. pub state: RwLock, - /// See comment of [`EventCacheInner::all_events`]. + /// See comment of [`super::EventCacheInner::all_events`]. /// - /// This is shared between the [`EventCacheInner`] singleton and all + /// This is shared between the [`super::EventCacheInner`] singleton and all /// [`RoomEventCacheInner`] instances. all_events: Arc>, @@ -441,77 +401,6 @@ impl RoomEventCacheInner { .await } - /// If the event is related to another one, its id is added to the - /// relations map. - fn append_related_event( - &self, - cache: &mut RwLockWriteGuard<'_, AllEventsCache>, - event: &SyncTimelineEvent, - ) { - // Handle and cache events and relations. - if let Ok(AnySyncTimelineEvent::MessageLike(ev)) = event.raw().deserialize() { - // Handle redactions separately, as their logic is slightly different. - if let AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(ev)) = - &ev - { - if let Some(redacted_event_id) = ev.content.redacts.as_ref().or(ev.redacts.as_ref()) - { - cache - .relations - .entry(redacted_event_id.to_owned()) - .or_default() - .insert(ev.event_id.to_owned(), RelationType::Replacement); - } - } else { - let relationship = match ev.original_content() { - Some(AnyMessageLikeEventContent::RoomMessage(c)) => { - if let Some(relation) = c.relates_to { - match relation { - Relation::Replacement(replacement) => { - Some((replacement.event_id, RelationType::Replacement)) - } - Relation::Reply { in_reply_to } => { - Some((in_reply_to.event_id, RelationType::Reference)) - } - Relation::Thread(thread) => { - Some((thread.event_id, RelationType::Thread)) - } - // Do nothing for custom - _ => None, - } - } else { - None - } - } - Some(AnyMessageLikeEventContent::PollResponse(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::PollEnd(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::UnstablePollResponse(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::UnstablePollEnd(c)) => { - Some((c.relates_to.event_id, RelationType::Reference)) - } - Some(AnyMessageLikeEventContent::Reaction(c)) => { - Some((c.relates_to.event_id, RelationType::Annotation)) - } - _ => None, - }; - - if let Some(relationship) = relationship { - cache - .relations - .entry(relationship.0) - .or_default() - .insert(ev.event_id().to_owned(), relationship.1); - } - } - } - } - /// Append a set of events and associated room data. /// /// This is a private implementation. It must not be exposed publicly. @@ -563,7 +452,7 @@ impl RoomEventCacheInner { for sync_timeline_event in &sync_timeline_events { if let Some(event_id) = sync_timeline_event.event_id() { - self.append_related_event(&mut all_events, sync_timeline_event); + all_events.append_related_event(sync_timeline_event); all_events.events.insert( event_id.to_owned(), (self.room_id.clone(), sync_timeline_event.clone()), From c5a9a1e215f858726eb38cd04f5294d7f218c391 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 7 Jan 2025 20:00:09 +0100 Subject: [PATCH 871/979] Clean up some imports With experimental-sliding-sync enabled and e2e-encryption disabled, there were a bunch of warnings about unused imports. This fixes them (but a few warnings about other unused items remain). --- crates/matrix-sdk-base/src/latest_event.rs | 22 ++++++++----------- .../matrix-sdk-base/src/sliding_sync/mod.rs | 17 +++++++------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index ccedb842edb..7884d585193 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -5,24 +5,20 @@ use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; #[cfg(feature = "e2e-encryption")] -use ruma::events::{ - call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, - poll::unstable_start::SyncUnstablePollStartEvent, - relation::RelationType, - room::message::SyncRoomMessageEvent, - AnySyncMessageLikeEvent, AnySyncTimelineEvent, -}; use ruma::{ events::{ - room::{ - member::{MembershipState, SyncRoomMemberEvent}, - power_levels::RoomPowerLevels, - }, + call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, + poll::unstable_start::SyncUnstablePollStartEvent, + relation::RelationType, + room::member::{MembershipState, SyncRoomMemberEvent}, + room::message::SyncRoomMessageEvent, + room::power_levels::RoomPowerLevels, sticker::SyncStickerEvent, - AnySyncStateEvent, + AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, }, - MxcUri, OwnedEventId, UserId, + UserId, }; +use ruma::{MxcUri, OwnedEventId}; use serde::{Deserialize, Serialize}; use crate::MinimalRoomMemberEvent; diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index d8a96aa34a6..a5b7d726c3d 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -22,26 +22,20 @@ use std::{borrow::Cow, collections::BTreeMap}; #[cfg(feature = "e2e-encryption")] use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; -#[cfg(feature = "e2e-encryption")] -use ruma::api::client::sync::sync_events::v5; -#[cfg(feature = "e2e-encryption")] -use ruma::events::AnyToDeviceEvent; use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom}, events::{ room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent, - AnySyncStateEvent, StateEventType, + AnySyncStateEvent, }, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, UserId, }; +#[cfg(feature = "e2e-encryption")] +use ruma::{api::client::sync::sync_events::v5, events::AnyToDeviceEvent, events::StateEventType}; use tracing::{debug, error, instrument, trace, warn}; use super::BaseClient; -#[cfg(feature = "e2e-encryption")] -use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent}; -#[cfg(feature = "e2e-encryption")] -use crate::RoomMemberships; use crate::{ error::Result, read_receipts::{compute_unread_counts, PreviousEventsProvider}, @@ -55,6 +49,11 @@ use crate::{ sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse}, Room, RoomInfo, }; +#[cfg(feature = "e2e-encryption")] +use crate::{ + latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent}, + RoomMemberships, +}; impl BaseClient { #[cfg(feature = "e2e-encryption")] From 47445b10f1ead9dc1e3fb19027aec4435254e820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 8 Jan 2025 10:03:45 +0100 Subject: [PATCH 872/979] chore: Upgrade Ruma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is using the ruma-0.12 branch where non-breaking changes are backported. Signed-off-by: Kévin Commaille --- Cargo.lock | 28 ++++++++++------------------ Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ae5eb6e07c..0c7e1a41a50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4786,8 +4786,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5100fcaf13d18b9c5c2dfdee5632c428e3201b04ddefd82c930953b461d000a" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "assign", "js_int", @@ -4803,8 +4802,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f5929f675a96adb22dcfbab1c527862d7f92a6346a280f2ddcfc6380b19391" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "as_variant", "assign", @@ -4827,8 +4825,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c537899b20312655aa9bf4cd825aaf00dd13203f215df2007bc4fbbeac8d8ba" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "as_variant", "base64 0.22.1", @@ -4860,8 +4857,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e4f72eb598c62f51a199bd9218f3fc36a5d50361ecc7a30d864df7bfcef220" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4886,8 +4882,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d70c3d37a8e42992aeaa5786cb406ad302bcd05c0e7e3073d5316b4574340dd" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "http", "js_int", @@ -4901,8 +4896,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3257ce3398e171ff15245767b1a3d201cfc5cce75f5af7ec7f6b8b5e1d2bdb" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "as_variant", "html5ever", @@ -4913,19 +4907,17 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7f9b534a65698d7db3c08d94bf91de0046fe6c7893a7b360502f65e7011ac4" +version = "0.10.1" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "js_int", - "thiserror 1.0.63", + "thiserror 2.0.3", ] [[package]] name = "ruma-macros" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7bc55ea278668253c9898dd905325bf1f72df4bf2abddd04ff1c99b7b3c4fb" +source = "git+https://github.com/ruma/ruma?rev=71be4a316198d6db91f512b2ceb8eb91238581f1#71be4a316198d6db91f512b2ceb8eb91238581f1" dependencies = [ "cfg-if", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index a1aa363abd6..a80e0cc06fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ proptest = { version = "1.5.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.4", default-features = false } rmp-serde = "1.3.0" -ruma = { version = "0.12.0", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "71be4a316198d6db91f512b2ceb8eb91238581f1", features = [ "client-api-c", "compat-upload-signatures", "compat-user-id", @@ -72,7 +72,7 @@ ruma = { version = "0.12.0", features = [ "unstable-msc4140", "unstable-msc4171", ] } -ruma-common = "0.15.0" +ruma-common = { git = "https://github.com/ruma/ruma", rev = "71be4a316198d6db91f512b2ceb8eb91238581f1" } serde = "1.0.151" serde_html_form = "0.2.0" serde_json = "1.0.91" From 47c24b9a17924536c7754d51d5eba60f0624c52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 8 Jan 2025 10:11:02 +0100 Subject: [PATCH 873/979] fix(sdk): Fix test now that Ruma is fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/room/edit.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index e0644ac8fba..850e70d3884 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -689,22 +689,13 @@ mod tests { assert!(image.caption().is_none()); assert!(image.formatted_caption().is_none()); - // The raw event doesn't contain the mention :( - // TODO: this is a bug in Ruma! When Ruma gets upgraded in the SDK, this test - // may start failing. In this case, remove the following code, and replace it - // with the commented code below. - - assert_matches!(msg.mentions, None); - - /* - // The raw event contains the mention. - assert_let!(Some(mentions) = msg.mentions); - assert!(!mentions.room); - assert_eq!( - mentions.user_ids.into_iter().collect::>(), - vec![mentioned_user_id.clone()] - ); - */ + // The raw event contains the mention. + assert_let!(Some(mentions) = msg.mentions); + assert!(!mentions.room); + assert_eq!( + mentions.user_ids.into_iter().collect::>(), + vec![mentioned_user_id.clone()] + ); assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to); assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype); From aca8c8b8ee1950fa7aa0fb531ade27b76b029910 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 8 Jan 2025 10:37:18 +0100 Subject: [PATCH 874/979] chore: remove some `allow(dead_code)` annotations and associated dead code (#4472) We have quite a few `allow(dead_code)` annotations. While it's OK to use in situations where the Cargo-feature combination explodes and makes it hard to reason about when something is actually used or not, in other situations it can be avoided, and show actual, dead code. --- .../src/linked_chunk/builder.rs | 4 +--- .../matrix-sdk-common/src/linked_chunk/mod.rs | 1 - .../src/linked_chunk/updates.rs | 3 +++ .../src/olm/group_sessions/inbound.rs | 21 ---------------- .../matrix-sdk-indexeddb/src/safe_encode.rs | 18 -------------- .../src/state_store/migrations.rs | 1 - crates/matrix-sdk-sqlite/src/crypto_store.rs | 7 +++--- .../src/event_cache_store.rs | 24 ------------------- .../matrix-sdk/src/sliding_sync/list/mod.rs | 16 ++++++------- .../src/test_json/keys_query_sets.rs | 3 --- 10 files changed, 14 insertions(+), 84 deletions(-) diff --git a/crates/matrix-sdk-common/src/linked_chunk/builder.rs b/crates/matrix-sdk-common/src/linked_chunk/builder.rs index 81459e2d605..6ba403f3e60 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/builder.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/builder.rs @@ -31,7 +31,6 @@ use super::{ /// which will get resolved later when re-building the full data structure. This /// allows using chunks that references other chunks that aren't known yet. struct TemporaryChunk { - id: ChunkIdentifier, previous: Option, next: Option, content: ChunkContent, @@ -79,7 +78,7 @@ impl LinkedChunkBuilder { next: Option, content: Gap, ) { - let chunk = TemporaryChunk { id, previous, next, content: ChunkContent::Gap(content) }; + let chunk = TemporaryChunk { previous, next, content: ChunkContent::Gap(content) }; self.chunks.insert(id, chunk); } @@ -96,7 +95,6 @@ impl LinkedChunkBuilder { items: impl IntoIterator, ) { let chunk = TemporaryChunk { - id, previous, next, content: ChunkContent::Items(items.into_iter().collect()), diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index 3294f58845f..b8f14589ad4 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(dead_code)] #![allow(rustdoc::private_intra_doc_links)] //! A linked chunk is the underlying data structure that holds all events. diff --git a/crates/matrix-sdk-common/src/linked_chunk/updates.rs b/crates/matrix-sdk-common/src/linked_chunk/updates.rs index e05944e989e..c14ee4ab09e 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/updates.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/updates.rs @@ -138,6 +138,7 @@ impl ObservableUpdates { } /// Subscribe to updates by using a [`Stream`]. + #[cfg(test)] pub(super) fn subscribe(&mut self) -> UpdatesSubscriber { // A subscriber is a new update reader, it needs its own token. let token = self.new_reader_token(); @@ -264,6 +265,7 @@ impl UpdatesInner { } /// Return the number of updates in the buffer. + #[cfg(test)] fn len(&self) -> usize { self.updates.len() } @@ -302,6 +304,7 @@ pub(super) struct UpdatesSubscriber { impl UpdatesSubscriber { /// Create a new [`Self`]. + #[cfg(test)] fn new(updates: Weak>>, token: ReaderToken) -> Self { Self { updates, token } } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index c42d7ba889d..1608498a76f 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -221,27 +221,6 @@ impl InboundGroupSession { Self::try_from(exported_session) } - #[allow(dead_code)] - fn from_backup( - room_id: &RoomId, - backup: BackedUpRoomKey, - ) -> Result { - // We're using this session only to get the session id, the session - // config doesn't matter here. - let session = InnerSession::import(&backup.session_key, SessionConfig::default()); - let session_id = session.session_id(); - - Self::from_export(&ExportedRoomKey { - algorithm: backup.algorithm, - room_id: room_id.to_owned(), - sender_key: backup.sender_key, - session_id, - forwarding_curve25519_key_chain: vec![], - session_key: backup.session_key, - sender_claimed_keys: backup.sender_claimed_keys, - }) - } - /// Store the group session as a base64 encoded string. /// /// # Arguments diff --git a/crates/matrix-sdk-indexeddb/src/safe_encode.rs b/crates/matrix-sdk-indexeddb/src/safe_encode.rs index 4121b5aa430..7f8917fe40a 100644 --- a/crates/matrix-sdk-indexeddb/src/safe_encode.rs +++ b/crates/matrix-sdk-indexeddb/src/safe_encode.rs @@ -1,6 +1,5 @@ //! Helpers for wasm32/browser environments -#![allow(dead_code)] use base64::{ alphabet, engine::{general_purpose, GeneralPurpose}, @@ -51,23 +50,6 @@ pub trait SafeEncode { .encode(store_cipher.hash_key(table_name, self.as_encoded_string().as_bytes())) } - /// encode self into a JsValue, internally using `as_encoded_string` - /// to escape the value of self, and append the given counter - fn encode_with_counter(&self, i: usize) -> JsValue { - format!("{}{KEY_SEPARATOR}{i:016x}", self.as_encoded_string()).into() - } - - /// encode self into a JsValue, internally using `as_secure_string` - /// to escape the value of self, and append the given counter - fn encode_with_counter_secure( - &self, - table_name: &str, - store_cipher: &StoreCipher, - i: usize, - ) -> JsValue { - format!("{}{KEY_SEPARATOR}{i:016x}", self.as_secure_string(table_name, store_cipher)).into() - } - /// Encode self into a IdbKeyRange for searching all keys that are /// prefixed with this key, followed by `KEY_SEPARATOR`. Internally /// uses `as_encoded_string` to ensure the given key is escaped properly. diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index 5d86dcf61a6..01c0a0a20db 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -51,7 +51,6 @@ const CURRENT_META_DB_VERSION: u32 = 2; /// Sometimes Migrations can't proceed without having to drop existing /// data. This allows you to configure, how these cases should be handled. -#[allow(dead_code)] #[derive(Clone, Debug, PartialEq, Eq)] pub enum MigrationConflictStrategy { /// Just drop the data, we don't care that we have to sync again diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 236b6b7fcb4..fe6ce5e88a1 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -1420,8 +1420,7 @@ mod tests { struct TestDb { // Needs to be kept alive because the Drop implementation for TempDir deletes the // directory. - #[allow(dead_code)] - dir: TempDir, + _dir: TempDir, database: SqliteCryptoStore, } @@ -1440,14 +1439,14 @@ mod tests { let database = SqliteCryptoStore::open(tmpdir.path(), None).await.expect("Can't open the test store"); - TestDb { dir: tmpdir, database } + TestDb { _dir: tmpdir, database } } /// Test that we didn't regress in our storage layer by loading data from a /// pre-filled database, or in other words use a test vector for this. #[async_test] async fn test_open_test_vector_store() { - let TestDb { dir: _, database } = get_test_db().await; + let TestDb { _dir: _, database } = get_test_db().await; let account = database .load_account() diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index fd7657684ef..88c9d1801bb 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -14,8 +14,6 @@ //! A sqlite-based backend for the [`EventCacheStore`]. -#![allow(dead_code)] // Most of the unused code may be used soonish. - use std::{borrow::Cow, fmt, path::Path, sync::Arc}; use async_trait::async_trait; @@ -143,28 +141,6 @@ impl SqliteEventCacheStore { row.get::<_, String>(3)?, )) } - - async fn load_chunk_with_id( - &self, - room_id: &RoomId, - chunk_id: ChunkIdentifier, - ) -> Result> { - let hashed_room_id = self.encode_key(keys::LINKED_CHUNKS, room_id); - - let this = self.clone(); - - self - .acquire() - .await? - .with_transaction(move |txn| -> Result<_> { - let (id, previous, next, chunk_type) = txn.query_row( - "SELECT id, previous, next, type FROM linked_chunks WHERE room_id = ? AND chunk_id = ?", - (&hashed_room_id, chunk_id.index()), - Self::map_row_to_chunk - )?; - txn.rebuild_chunk(&this, &hashed_room_id, previous, id, next, chunk_type.as_str()) - }).await - } } trait TransactionExtForLinkedChunks { diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index a3b3ec3b0f4..1471dfc7bf9 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -204,20 +204,18 @@ impl SlidingSyncList { pub(super) fn invalidate_sticky_data(&self) { let _ = self.inner.sticky.write().unwrap().data_mut(); } -} - -#[cfg(any(test, feature = "testing"))] -#[allow(dead_code)] -impl SlidingSyncList { - /// Set the maximum number of rooms. - pub(super) fn set_maximum_number_of_rooms(&self, maximum_number_of_rooms: Option) { - self.inner.maximum_number_of_rooms.set(maximum_number_of_rooms); - } /// Get the sync-mode. + #[cfg(feature = "testing")] pub fn sync_mode(&self) -> SlidingSyncMode { self.inner.sync_mode.read().unwrap().clone() } + + /// Set the maximum number of rooms. + #[cfg(test)] + pub(super) fn set_maximum_number_of_rooms(&self, maximum_number_of_rooms: Option) { + self.inner.maximum_number_of_rooms.set(maximum_number_of_rooms); + } } #[derive(Debug)] diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 1ee8591162c..9e0e43a7a8b 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -36,7 +36,6 @@ use crate::{ /// devices are properly signed by `@good` (i.e were self-verified by @good) pub struct KeyDistributionTestData {} -#[allow(dead_code)] impl KeyDistributionTestData { pub const MASTER_KEY_PRIVATE_EXPORT: &'static str = "9kquJqAtEUoTXljh5W2QSsCm4FH9WvWzIkDkIMUsM2k"; @@ -529,7 +528,6 @@ impl IdentityChangeDataSet { /// The `/keys/query` responses were generated using a local synapse. pub struct VerificationViolationTestData {} -#[allow(dead_code)] impl VerificationViolationTestData { /// Secret part of Alice's master cross-signing key. /// @@ -1130,7 +1128,6 @@ impl VerificationViolationTestData { /// For user @malo, that performed an identity change with the same device. pub struct MaloIdentityChangeDataSet {} -#[allow(dead_code)] impl MaloIdentityChangeDataSet { pub fn user_id() -> &'static UserId { user_id!("@malo:localhost") From 3f977b79fa74b39a7a0a39fdd72a63d7fb80c866 Mon Sep 17 00:00:00 2001 From: Joe Groocock Date: Wed, 8 Jan 2025 00:14:05 +0000 Subject: [PATCH 875/979] feat(timeline): allow sending mentions along with media Since 8205da898e11c34b77b13fe4794d0b2e1bcdd2a8 it has been possible to attach (intentional) mentions to _edited_ media captions, but the send_$mediatype() timeline APIs provided no way to send them with the initial event. This fixes that. Signed-off-by: Joe Groocock --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 22 ++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 2c4798a63a6..bcb486f3077 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -298,6 +298,7 @@ impl Timeline { image_info: ImageInfo, caption: Option, formatted_caption: Option, + mentions: Option, progress_watcher: Option>, use_send_queue: bool, ) -> Arc { @@ -313,7 +314,8 @@ impl Timeline { .thumbnail(thumbnail) .info(attachment_info) .caption(caption) - .formatted_caption(formatted_caption); + .formatted_caption(formatted_caption) + .mentions(mentions.map(Into::into)); self.send_attachment( url, @@ -334,6 +336,7 @@ impl Timeline { video_info: VideoInfo, caption: Option, formatted_caption: Option, + mentions: Option, progress_watcher: Option>, use_send_queue: bool, ) -> Arc { @@ -349,7 +352,8 @@ impl Timeline { .thumbnail(thumbnail) .info(attachment_info) .caption(caption) - .formatted_caption(formatted_caption.map(Into::into)); + .formatted_caption(formatted_caption.map(Into::into)) + .mentions(mentions.map(Into::into)); self.send_attachment( url, @@ -362,12 +366,14 @@ impl Timeline { })) } + #[allow(clippy::too_many_arguments)] pub fn send_audio( self: Arc, url: String, audio_info: AudioInfo, caption: Option, formatted_caption: Option, + mentions: Option, progress_watcher: Option>, use_send_queue: bool, ) -> Arc { @@ -381,7 +387,8 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .info(attachment_info) .caption(caption) - .formatted_caption(formatted_caption.map(Into::into)); + .formatted_caption(formatted_caption.map(Into::into)) + .mentions(mentions.map(Into::into)); self.send_attachment( url, @@ -401,6 +408,7 @@ impl Timeline { audio_info: AudioInfo, waveform: Vec, caption: Option, + mentions: Option, formatted_caption: Option, progress_watcher: Option>, use_send_queue: bool, @@ -416,7 +424,8 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .info(attachment_info) .caption(caption) - .formatted_caption(formatted_caption.map(Into::into)); + .formatted_caption(formatted_caption.map(Into::into)) + .mentions(mentions.map(Into::into)); self.send_attachment( url, @@ -429,12 +438,14 @@ impl Timeline { })) } + #[allow(clippy::too_many_arguments)] pub fn send_file( self: Arc, url: String, file_info: FileInfo, caption: Option, formatted_caption: Option, + mentions: Option, progress_watcher: Option>, use_send_queue: bool, ) -> Arc { @@ -448,7 +459,8 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .info(attachment_info) .caption(caption) - .formatted_caption(formatted_caption.map(Into::into)); + .formatted_caption(formatted_caption.map(Into::into)) + .mentions(mentions.map(Into::into)); self.send_attachment( url, From 55e25a37170eff94b2e2c974b6cc7cacdf123dd4 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 8 Jan 2025 10:51:58 +0100 Subject: [PATCH 876/979] feat(sdk,ui): Add `EventsOrigin::Pagination`. This patch adds the `Pagination` variant to the `EventsOrigin` enum. Not something really mandatory and that is likely to fix a bug, but it's now correct. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 5 ++++- crates/matrix-sdk/src/event_cache/mod.rs | 3 +++ crates/matrix-sdk/src/event_cache/pagination.rs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 2032ad5f4d2..622fef4ac2a 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -297,8 +297,10 @@ impl TimelineBuilder { inner.add_events_at( events.into_iter(), - TimelineNewItemPosition::End { origin: match origin { + TimelineNewItemPosition::End { + origin: match origin { EventsOrigin::Sync => RemoteEventOrigin::Sync, + EventsOrigin::Pagination => RemoteEventOrigin::Pagination, } } ).await; @@ -313,6 +315,7 @@ impl TimelineBuilder { diffs, match origin { EventsOrigin::Sync => RemoteEventOrigin::Sync, + EventsOrigin::Pagination => RemoteEventOrigin::Pagination, } ).await; } diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index f85b85bc337..0b5b2b0e346 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -694,6 +694,9 @@ pub enum RoomEventCacheUpdate { pub enum EventsOrigin { /// Events are coming from a sync. Sync, + + /// Events are coming from pagination. + Pagination, } #[cfg(test)] diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 0d40c1046a0..93a87bc730a 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -234,7 +234,7 @@ impl RoomPagination { if !updates_as_vector_diffs.is_empty() { let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents { diffs: updates_as_vector_diffs, - origin: EventsOrigin::Sync, + origin: EventsOrigin::Pagination, }); } From 7ff11706815d7599ef28a52fab7a000df4a810ac Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 7 Jan 2025 13:32:25 +0100 Subject: [PATCH 877/979] chore: Re-indent. --- .../timeline/controller/observable_items.rs | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 0d326c5a71a..ee19ecc5a9d 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -438,64 +438,64 @@ mod observable_items_tests { } macro_rules! assert_mapping { - ( on $transaction:ident: - | event_id | event_index | timeline_item_index | - | $( - )+ | $( - )+ | $( - )+ | - $( - | $event_id:literal | $event_index:literal | $( $timeline_item_index:literal )? | - )+ - ) => { - let all_remote_events = $transaction .all_remote_events(); - - $( - // Remote event exists at this index… - assert_matches!(all_remote_events.0.get( $event_index ), Some(EventMeta { event_id, timeline_item_index, .. }) => { - // … this is the remote event with the expected event ID - assert_eq!( - event_id.as_str(), - $event_id , - concat!("event #", $event_index, " should have ID ", $event_id) - ); - - - // (tiny hack to handle the case where `$timeline_item_index` is absent) - #[allow(unused_variables)] - let timeline_item_index_is_expected = false; - $( - let timeline_item_index_is_expected = true; - let _ = $timeline_item_index; - )? - - if timeline_item_index_is_expected.not() { - // … this remote event does NOT map to a timeline item index - assert!( - timeline_item_index.is_none(), - concat!("event #", $event_index, " with ID ", $event_id, " should NOT map to a timeline item index" ) - ); - } + ( on $transaction:ident: + | event_id | event_index | timeline_item_index | + | $( - )+ | $( - )+ | $( - )+ | + $( + | $event_id:literal | $event_index:literal | $( $timeline_item_index:literal )? | + )+ + ) => { + let all_remote_events = $transaction .all_remote_events(); - $( - // … this remote event maps to a timeline item index + $( + // Remote event exists at this index… + assert_matches!(all_remote_events.0.get( $event_index ), Some(EventMeta { event_id, timeline_item_index, .. }) => { + // … this is the remote event with the expected event ID assert_eq!( - *timeline_item_index, - Some( $timeline_item_index ), - concat!("event #", $event_index, " with ID ", $event_id, " should map to timeline item #", $timeline_item_index ) + event_id.as_str(), + $event_id , + concat!("event #", $event_index, " should have ID ", $event_id) ); - // … this timeline index exists - assert_matches!( $transaction .get( $timeline_item_index ), Some(timeline_item) => { - // … this timelime item has the expected event ID - assert_event_id!( - timeline_item, - $event_id , - concat!("timeline item #", $timeline_item_index, " should map to event ID ", $event_id ) + + // (tiny hack to handle the case where `$timeline_item_index` is absent) + #[allow(unused_variables)] + let timeline_item_index_is_expected = false; + $( + let timeline_item_index_is_expected = true; + let _ = $timeline_item_index; + )? + + if timeline_item_index_is_expected.not() { + // … this remote event does NOT map to a timeline item index + assert!( + timeline_item_index.is_none(), + concat!("event #", $event_index, " with ID ", $event_id, " should NOT map to a timeline item index" ) ); - }); - )? - }); - )* + } + + $( + // … this remote event maps to a timeline item index + assert_eq!( + *timeline_item_index, + Some( $timeline_item_index ), + concat!("event #", $event_index, " with ID ", $event_id, " should map to timeline item #", $timeline_item_index ) + ); + + // … this timeline index exists + assert_matches!( $transaction .get( $timeline_item_index ), Some(timeline_item) => { + // … this timelime item has the expected event ID + assert_event_id!( + timeline_item, + $event_id , + concat!("timeline item #", $timeline_item_index, " should map to event ID ", $event_id ) + ); + }); + )? + }); + )* + } } -} #[test] fn test_is_empty() { From d64960679f49c2713ba5429c393326fb29f0e12a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 6 Jan 2025 15:31:52 +0100 Subject: [PATCH 878/979] refactor(sdk): Rename `RoomEvents::filter_duplicated_events`. This patch renames `RoomEvents::filter_duplicated_events` to `collect_valid_and_duplicated_events` as I believe it improves the understanding of the code. The variables named `unique_events` are renamed `events` as all (valid) events are returned, not only the unique ones. --- .../matrix-sdk/src/event_cache/room/events.rs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index eb352c99580..12fdf70f96b 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -101,10 +101,10 @@ impl RoomEvents { where I: IntoIterator, { - let (unique_events, duplicated_event_ids) = - self.filter_duplicated_events(events.into_iter()); + let (events, duplicated_event_ids) = + self.collect_valid_and_duplicated_events(events.into_iter()); - if deduplicated_all_new_events(unique_events.len(), duplicated_event_ids.len()) { + if deduplicated_all_new_events(events.len(), duplicated_event_ids.len()) { return false; } @@ -115,7 +115,7 @@ impl RoomEvents { self.remove_events(duplicated_event_ids); // Push new `events`. - self.chunks.push_items_back(unique_events); + self.chunks.push_items_back(events); true } @@ -132,10 +132,10 @@ impl RoomEvents { where I: IntoIterator, { - let (unique_events, duplicated_event_ids) = - self.filter_duplicated_events(events.into_iter()); + let (events, duplicated_event_ids) = + self.collect_valid_and_duplicated_events(events.into_iter()); - if deduplicated_all_new_events(unique_events.len(), duplicated_event_ids.len()) { + if deduplicated_all_new_events(events.len(), duplicated_event_ids.len()) { return Ok(false); } @@ -146,7 +146,7 @@ impl RoomEvents { // argument value for each removal. self.remove_events_and_update_insert_position(duplicated_event_ids, &mut position); - self.chunks.insert_items_at(unique_events, position)?; + self.chunks.insert_items_at(events, position)?; Ok(true) } @@ -163,8 +163,8 @@ impl RoomEvents { /// /// This method returns: /// - a boolean indicating if we updated the linked chunk, - /// - a reference to the (first if many) newly created `Chunk` that contains - /// the `items`. + /// - the position of the (first if many) newly created `Chunk` that + /// contains the `items`. pub fn replace_gap_at( &mut self, events: I, @@ -173,10 +173,10 @@ impl RoomEvents { where I: IntoIterator, { - let (unique_events, duplicated_event_ids) = - self.filter_duplicated_events(events.into_iter()); + let (events, duplicated_event_ids) = + self.collect_valid_and_duplicated_events(events.into_iter()); - if deduplicated_all_new_events(unique_events.len(), duplicated_event_ids.len()) { + if deduplicated_all_new_events(events.len(), duplicated_event_ids.len()) { let pos = self.chunks.remove_gap_at(gap_identifier)?; return Ok((false, pos)); } @@ -188,13 +188,13 @@ impl RoomEvents { // because of the removals. self.remove_events(duplicated_event_ids); - let next_pos = if unique_events.is_empty() { + let next_pos = if events.is_empty() { // There are no new events, so there's no need to create a new empty items // chunk; instead, remove the gap. self.chunks.remove_gap_at(gap_identifier)? } else { // Replace the gap by new events. - Some(self.chunks.replace_gap_at(unique_events, gap_identifier)?.first_position()) + Some(self.chunks.replace_gap_at(events, gap_identifier)?.first_position()) }; Ok((true, next_pos)) @@ -257,15 +257,18 @@ impl RoomEvents { /// Deduplicate `events` considering all events in `Self::chunks`. /// - /// The returned tuple contains (i) the unique events, and (ii) the + /// The returned tuple contains (i) all events with an ID, and (ii) the /// duplicated events (by ID). - fn filter_duplicated_events<'a, I>(&'a mut self, events: I) -> (Vec, Vec) + fn collect_valid_and_duplicated_events<'a, I>( + &'a mut self, + events: I, + ) -> (Vec, Vec) where I: Iterator + 'a, { let mut duplicated_event_ids = Vec::new(); - let deduplicated_events = self + let events = self .deduplicator .scan_and_learn(events, self) .filter_map(|decorated_event| match decorated_event { @@ -292,7 +295,7 @@ impl RoomEvents { }) .collect(); - (deduplicated_events, duplicated_event_ids) + (events, duplicated_event_ids) } /// Return a nice debug string (a vector of lines) for the linked chunk of From 5ff556f6c3026ab53b21ba0b0a24749ffa715e17 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 20 Dec 2024 18:19:04 +0100 Subject: [PATCH 879/979] fix(crypto): Serialize sender data msk in base64 instead of numbers --- .../src/olm/group_sessions/sender_data.rs | 74 ++++++++++++++++++- ...r_data__tests__snapshot_sender_data-4.snap | 35 +-------- ...r_data__tests__snapshot_sender_data-5.snap | 35 +-------- ...r_data__tests__snapshot_sender_data-6.snap | 35 +-------- 4 files changed, 74 insertions(+), 105 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs index d10758546d1..fa79b6b6ab3 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/sender_data.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::cmp::Ordering; +use std::{cmp::Ordering, fmt}; use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId}; -use serde::{Deserialize, Serialize}; +use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize}; use vodozemac::Ed25519PublicKey; -use crate::types::DeviceKeys; +use crate::types::{serialize_ed25519_key, DeviceKeys}; /// Information about the sender of a megolm session where we know the /// cross-signing identity of the sender. @@ -33,9 +33,55 @@ pub struct KnownSenderData { pub device_id: Option, /// The cross-signing key of the user who established this session. + #[serde( + serialize_with = "serialize_ed25519_key", + deserialize_with = "deserialize_sender_msk_base64_or_array" + )] pub master_key: Box, } +/// In an initial version the master key was serialized as an array of number, +/// it is now exported in base64. This code adds backward compatibility. +pub(crate) fn deserialize_sender_msk_base64_or_array<'de, D>( + de: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct KeyVisitor; + + impl<'de> Visitor<'de> for KeyVisitor { + type Value = Box; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "a base64 string or an array of 32 bytes") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + let decoded = Ed25519PublicKey::from_base64(v) + .map_err(|_| de::Error::custom("Base64 decoding error"))?; + Ok(Box::new(decoded)) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut buf = [0u8; 32]; + for (i, item) in buf.iter_mut().enumerate() { + *item = seq.next_element()?.ok_or_else(|| de::Error::invalid_length(i, &self))?; + } + let key = Ed25519PublicKey::from_slice(&buf).map_err(|e| de::Error::custom(&e))?; + Ok(Box::new(key)) + } + } + + de.deserialize_any(KeyVisitor) +} + /// Information on the device and user that sent the megolm session data to us /// /// Sessions start off in `UnknownDevice` state, and progress into `DeviceInfo` @@ -287,6 +333,7 @@ mod tests { use ruma::{ device_id, owned_device_id, owned_user_id, user_id, DeviceKeyAlgorithm, DeviceKeyId, }; + use serde_json::json; use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; use super::SenderData; @@ -530,4 +577,25 @@ mod tests { master_key: Box::new(Ed25519PublicKey::from_slice(&[1u8; 32]).unwrap()), })); } + + #[test] + fn test_sender_known_data_migration() { + let old_format = json!( + { + "SenderVerified": { + "user_id": "@foo:bar.baz", + "device_id": null, + "master_key": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + } + }); + + let migrated: SenderData = serde_json::from_value(old_format).unwrap(); + + assert_let!(SenderData::SenderVerified(KnownSenderData { master_key, .. }) = migrated); + + assert_eq!( + master_key.to_base64(), + Ed25519PublicKey::from_slice(&[0u8; 32]).unwrap().to_base64() + ); + } } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap index da1c8e4c40d..d1fb11f1902 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-4.snap @@ -6,39 +6,6 @@ expression: "SenderData::VerificationViolation(KnownSenderData\n{\n user_id: "VerificationViolation": { "user_id": "@foo:bar.baz", "device_id": "DEV", - "master_key": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] + "master_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" } } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap index 36f6beb3dcf..2a737872f75 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-5.snap @@ -6,39 +6,6 @@ expression: "SenderData::SenderUnverified(KnownSenderData\n{\n user_id: owned "SenderUnverified": { "user_id": "@foo:bar.baz", "device_id": null, - "master_key": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] + "master_key": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE" } } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap index 62e7b8e7ceb..b2d09e4f345 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/snapshots/matrix_sdk_crypto__olm__group_sessions__sender_data__tests__snapshot_sender_data-6.snap @@ -6,39 +6,6 @@ expression: "SenderData::SenderVerified(KnownSenderData\n{\n user_id: owned_u "SenderVerified": { "user_id": "@foo:bar.baz", "device_id": null, - "master_key": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] + "master_key": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE" } } From 35a03278c37fdacddf4ab1045c906c1f65e07c14 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 8 Jan 2025 11:05:39 +0100 Subject: [PATCH 880/979] chore(ffi): rename `url` to `filename` in the FFI methods for sending attachments --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index bcb486f3077..6cebb6ed5d8 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -293,7 +293,7 @@ impl Timeline { #[allow(clippy::too_many_arguments)] pub fn send_image( self: Arc, - url: String, + filename: String, thumbnail_url: Option, image_info: ImageInfo, caption: Option, @@ -318,7 +318,7 @@ impl Timeline { .mentions(mentions.map(Into::into)); self.send_attachment( - url, + filename, image_info.mimetype, attachment_config, progress_watcher, @@ -331,7 +331,7 @@ impl Timeline { #[allow(clippy::too_many_arguments)] pub fn send_video( self: Arc, - url: String, + filename: String, thumbnail_url: Option, video_info: VideoInfo, caption: Option, @@ -356,7 +356,7 @@ impl Timeline { .mentions(mentions.map(Into::into)); self.send_attachment( - url, + filename, video_info.mimetype, attachment_config, progress_watcher, @@ -369,7 +369,7 @@ impl Timeline { #[allow(clippy::too_many_arguments)] pub fn send_audio( self: Arc, - url: String, + filename: String, audio_info: AudioInfo, caption: Option, formatted_caption: Option, @@ -391,7 +391,7 @@ impl Timeline { .mentions(mentions.map(Into::into)); self.send_attachment( - url, + filename, audio_info.mimetype, attachment_config, progress_watcher, @@ -404,7 +404,7 @@ impl Timeline { #[allow(clippy::too_many_arguments)] pub fn send_voice_message( self: Arc, - url: String, + filename: String, audio_info: AudioInfo, waveform: Vec, caption: Option, @@ -428,7 +428,7 @@ impl Timeline { .mentions(mentions.map(Into::into)); self.send_attachment( - url, + filename, audio_info.mimetype, attachment_config, progress_watcher, @@ -441,7 +441,7 @@ impl Timeline { #[allow(clippy::too_many_arguments)] pub fn send_file( self: Arc, - url: String, + filename: String, file_info: FileInfo, caption: Option, formatted_caption: Option, @@ -463,7 +463,7 @@ impl Timeline { .mentions(mentions.map(Into::into)); self.send_attachment( - url, + filename, file_info.mimetype, attachment_config, progress_watcher, From ed178602d7d9e1029d93a2fdb6579c8f0678b7fd Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 8 Jan 2025 11:11:04 +0100 Subject: [PATCH 881/979] chore!(ffi): group parameters to upload in `UploadParameters` Note: `Box` couldn't be put in a `Record`, so doesn't belong in `UploadParameters` as a result. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 126 ++++++++++---------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 6cebb6ed5d8..f1916e0d612 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -179,6 +179,22 @@ fn build_thumbnail_info( } } +#[derive(uniffi::Record)] +pub struct UploadParameters { + /// Filename (previously called "url") for the media to be sent. + filename: String, + /// Optional non-formatted caption, for clients that support it. + caption: Option, + /// Optional HTML-formatted caption, for clients that support it. + formatted_caption: Option, + // Optional intentional mentions to be sent with the media. + mentions: Option, + /// Should the media be sent with the send queue, or synchronously? + /// + /// Watching progress only works with the synchronous method, at the moment. + use_send_queue: bool, +} + #[matrix_sdk_ffi_macros::export] impl Timeline { pub async fn add_listener(&self, listener: Box) -> Arc { @@ -290,20 +306,18 @@ impl Timeline { } } - #[allow(clippy::too_many_arguments)] pub fn send_image( self: Arc, - filename: String, + params: UploadParameters, thumbnail_url: Option, image_info: ImageInfo, - caption: Option, - formatted_caption: Option, - mentions: Option, progress_watcher: Option>, - use_send_queue: bool, ) -> Arc { - let formatted_caption = - formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); + let formatted_caption = formatted_body_from( + params.caption.as_deref(), + params.formatted_caption.map(Into::into), + ); + SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_image_info = BaseImageInfo::try_from(&image_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -313,35 +327,33 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .thumbnail(thumbnail) .info(attachment_info) - .caption(caption) + .caption(params.caption) .formatted_caption(formatted_caption) - .mentions(mentions.map(Into::into)); + .mentions(params.mentions.map(Into::into)); self.send_attachment( - filename, + params.filename, image_info.mimetype, attachment_config, progress_watcher, - use_send_queue, + params.use_send_queue, ) .await })) } - #[allow(clippy::too_many_arguments)] pub fn send_video( self: Arc, - filename: String, + params: UploadParameters, thumbnail_url: Option, video_info: VideoInfo, - caption: Option, - formatted_caption: Option, - mentions: Option, progress_watcher: Option>, - use_send_queue: bool, ) -> Arc { - let formatted_caption = - formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); + let formatted_caption = formatted_body_from( + params.caption.as_deref(), + params.formatted_caption.map(Into::into), + ); + SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -351,34 +363,32 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .thumbnail(thumbnail) .info(attachment_info) - .caption(caption) + .caption(params.caption) .formatted_caption(formatted_caption.map(Into::into)) - .mentions(mentions.map(Into::into)); + .mentions(params.mentions.map(Into::into)); self.send_attachment( - filename, + params.filename, video_info.mimetype, attachment_config, progress_watcher, - use_send_queue, + params.use_send_queue, ) .await })) } - #[allow(clippy::too_many_arguments)] pub fn send_audio( self: Arc, - filename: String, + params: UploadParameters, audio_info: AudioInfo, - caption: Option, - formatted_caption: Option, - mentions: Option, progress_watcher: Option>, - use_send_queue: bool, ) -> Arc { - let formatted_caption = - formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); + let formatted_caption = formatted_body_from( + params.caption.as_deref(), + params.formatted_caption.map(Into::into), + ); + SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -386,35 +396,33 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .info(attachment_info) - .caption(caption) + .caption(params.caption) .formatted_caption(formatted_caption.map(Into::into)) - .mentions(mentions.map(Into::into)); + .mentions(params.mentions.map(Into::into)); self.send_attachment( - filename, + params.filename, audio_info.mimetype, attachment_config, progress_watcher, - use_send_queue, + params.use_send_queue, ) .await })) } - #[allow(clippy::too_many_arguments)] pub fn send_voice_message( self: Arc, - filename: String, + params: UploadParameters, audio_info: AudioInfo, waveform: Vec, - caption: Option, - mentions: Option, - formatted_caption: Option, progress_watcher: Option>, - use_send_queue: bool, ) -> Arc { - let formatted_caption = - formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); + let formatted_caption = formatted_body_from( + params.caption.as_deref(), + params.formatted_caption.map(Into::into), + ); + SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) .map_err(|_| RoomError::InvalidAttachmentData)?; @@ -423,34 +431,32 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .info(attachment_info) - .caption(caption) + .caption(params.caption) .formatted_caption(formatted_caption.map(Into::into)) - .mentions(mentions.map(Into::into)); + .mentions(params.mentions.map(Into::into)); self.send_attachment( - filename, + params.filename, audio_info.mimetype, attachment_config, progress_watcher, - use_send_queue, + params.use_send_queue, ) .await })) } - #[allow(clippy::too_many_arguments)] pub fn send_file( self: Arc, - filename: String, + params: UploadParameters, file_info: FileInfo, - caption: Option, - formatted_caption: Option, - mentions: Option, progress_watcher: Option>, - use_send_queue: bool, ) -> Arc { - let formatted_caption = - formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into)); + let formatted_caption = formatted_body_from( + params.caption.as_deref(), + params.formatted_caption.map(Into::into), + ); + SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { let base_file_info: BaseFileInfo = BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?; @@ -458,16 +464,16 @@ impl Timeline { let attachment_config = AttachmentConfig::new() .info(attachment_info) - .caption(caption) + .caption(params.caption) .formatted_caption(formatted_caption.map(Into::into)) - .mentions(mentions.map(Into::into)); + .mentions(params.mentions.map(Into::into)); self.send_attachment( - filename, + params.filename, file_info.mimetype, attachment_config, progress_watcher, - use_send_queue, + params.use_send_queue, ) .await })) From 45c3752caefee65457742d5e51298cd2547328b9 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 8 Jan 2025 11:41:37 +0100 Subject: [PATCH 882/979] refactor!(ffi): common out more code in `send_attachment` and distinguish early from late errors Some errors can be handled immediately and don't need a request to be spawned, e.g. invalid mimetype and so on. The returned task handle still deals about "late" errors about the upload failing (for sync uploads) or the send queue failing to push the media upload (for async uploads). --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 225 +++++++------------- 1 file changed, 74 insertions(+), 151 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index f1916e0d612..a18d1b178a5 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -102,35 +102,52 @@ impl Timeline { unsafe { Arc::from_raw(Arc::into_raw(inner) as _) } } - async fn send_attachment( - &self, - filename: String, + fn send_attachment( + self: Arc, + params: UploadParameters, + attachment_info: AttachmentInfo, mime_type: Option, - attachment_config: AttachmentConfig, progress_watcher: Option>, - use_send_queue: bool, - ) -> Result<(), RoomError> { + thumbnail: Option, + ) -> Result, RoomError> { let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; let mime_type = mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; - let mut request = self.inner.send_attachment(filename, mime_type, attachment_config); + let formatted_caption = formatted_body_from( + params.caption.as_deref(), + params.formatted_caption.map(Into::into), + ); - if use_send_queue { - request = request.use_send_queue(); - } + let attachment_config = AttachmentConfig::new() + .thumbnail(thumbnail) + .info(attachment_info) + .caption(params.caption) + .formatted_caption(formatted_caption.map(Into::into)) + .mentions(params.mentions.map(Into::into)); - if let Some(progress_watcher) = progress_watcher { - let mut subscriber = request.subscribe_to_send_progress(); - RUNTIME.spawn(async move { - while let Some(progress) = subscriber.next().await { - progress_watcher.transmission_progress(progress.into()); - } - }); - } + let handle = SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { + let mut request = + self.inner.send_attachment(params.filename, mime_type, attachment_config); - request.await.map_err(|_| RoomError::FailedSendingAttachment)?; - Ok(()) + if params.use_send_queue { + request = request.use_send_queue(); + } + + if let Some(progress_watcher) = progress_watcher { + let mut subscriber = request.subscribe_to_send_progress(); + RUNTIME.spawn(async move { + while let Some(progress) = subscriber.next().await { + progress_watcher.transmission_progress(progress.into()); + } + }); + } + + request.await.map_err(|_| RoomError::FailedSendingAttachment)?; + Ok(()) + })); + + Ok(handle) } } @@ -312,34 +329,18 @@ impl Timeline { thumbnail_url: Option, image_info: ImageInfo, progress_watcher: Option>, - ) -> Arc { - let formatted_caption = formatted_body_from( - params.caption.as_deref(), - params.formatted_caption.map(Into::into), + ) -> Result, RoomError> { + let attachment_info = AttachmentInfo::Image( + BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?, ); - - SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let base_image_info = BaseImageInfo::try_from(&image_info) - .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::Image(base_image_info); - let thumbnail = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?; - - let attachment_config = AttachmentConfig::new() - .thumbnail(thumbnail) - .info(attachment_info) - .caption(params.caption) - .formatted_caption(formatted_caption) - .mentions(params.mentions.map(Into::into)); - - self.send_attachment( - params.filename, - image_info.mimetype, - attachment_config, - progress_watcher, - params.use_send_queue, - ) - .await - })) + let thumbnail = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?; + self.send_attachment( + params, + attachment_info, + image_info.mimetype, + progress_watcher, + thumbnail, + ) } pub fn send_video( @@ -348,34 +349,18 @@ impl Timeline { thumbnail_url: Option, video_info: VideoInfo, progress_watcher: Option>, - ) -> Arc { - let formatted_caption = formatted_body_from( - params.caption.as_deref(), - params.formatted_caption.map(Into::into), + ) -> Result, RoomError> { + let attachment_info = AttachmentInfo::Video( + BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?, ); - - SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info) - .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::Video(base_video_info); - let thumbnail = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?; - - let attachment_config = AttachmentConfig::new() - .thumbnail(thumbnail) - .info(attachment_info) - .caption(params.caption) - .formatted_caption(formatted_caption.map(Into::into)) - .mentions(params.mentions.map(Into::into)); - - self.send_attachment( - params.filename, - video_info.mimetype, - attachment_config, - progress_watcher, - params.use_send_queue, - ) - .await - })) + let thumbnail = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?; + self.send_attachment( + params, + attachment_info, + video_info.mimetype, + progress_watcher, + thumbnail, + ) } pub fn send_audio( @@ -383,32 +368,11 @@ impl Timeline { params: UploadParameters, audio_info: AudioInfo, progress_watcher: Option>, - ) -> Arc { - let formatted_caption = formatted_body_from( - params.caption.as_deref(), - params.formatted_caption.map(Into::into), + ) -> Result, RoomError> { + let attachment_info = AttachmentInfo::Audio( + BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?, ); - - SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) - .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::Audio(base_audio_info); - - let attachment_config = AttachmentConfig::new() - .info(attachment_info) - .caption(params.caption) - .formatted_caption(formatted_caption.map(Into::into)) - .mentions(params.mentions.map(Into::into)); - - self.send_attachment( - params.filename, - audio_info.mimetype, - attachment_config, - progress_watcher, - params.use_send_queue, - ) - .await - })) + self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None) } pub fn send_voice_message( @@ -417,33 +381,13 @@ impl Timeline { audio_info: AudioInfo, waveform: Vec, progress_watcher: Option>, - ) -> Arc { - let formatted_caption = formatted_body_from( - params.caption.as_deref(), - params.formatted_caption.map(Into::into), - ); - - SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info) - .map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = - AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) }; - - let attachment_config = AttachmentConfig::new() - .info(attachment_info) - .caption(params.caption) - .formatted_caption(formatted_caption.map(Into::into)) - .mentions(params.mentions.map(Into::into)); - - self.send_attachment( - params.filename, - audio_info.mimetype, - attachment_config, - progress_watcher, - params.use_send_queue, - ) - .await - })) + ) -> Result, RoomError> { + let attachment_info = AttachmentInfo::Voice { + audio_info: BaseAudioInfo::try_from(&audio_info) + .map_err(|_| RoomError::InvalidAttachmentData)?, + waveform: Some(waveform), + }; + self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None) } pub fn send_file( @@ -451,32 +395,11 @@ impl Timeline { params: UploadParameters, file_info: FileInfo, progress_watcher: Option>, - ) -> Arc { - let formatted_caption = formatted_body_from( - params.caption.as_deref(), - params.formatted_caption.map(Into::into), + ) -> Result, RoomError> { + let attachment_info = AttachmentInfo::File( + BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?, ); - - SendAttachmentJoinHandle::new(RUNTIME.spawn(async move { - let base_file_info: BaseFileInfo = - BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?; - let attachment_info = AttachmentInfo::File(base_file_info); - - let attachment_config = AttachmentConfig::new() - .info(attachment_info) - .caption(params.caption) - .formatted_caption(formatted_caption.map(Into::into)) - .mentions(params.mentions.map(Into::into)); - - self.send_attachment( - params.filename, - file_info.mimetype, - attachment_config, - progress_watcher, - params.use_send_queue, - ) - .await - })) + self.send_attachment(params, attachment_info, file_info.mimetype, progress_watcher, None) } pub async fn create_poll( From dc2775e1948ad1c543921633e5d4cf9797aa4e39 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 8 Jan 2025 11:44:45 +0100 Subject: [PATCH 883/979] chore!(ffi): rename `thumbnail_url` to `thumbnail_path` This is a breaking change because uniffi may use foreign-language named parameters based on the Rust parameter name. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index a18d1b178a5..5773717714b 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -152,15 +152,15 @@ impl Timeline { } fn build_thumbnail_info( - thumbnail_url: Option, + thumbnail_path: Option, thumbnail_info: Option, ) -> Result, RoomError> { - match (thumbnail_url, thumbnail_info) { + match (thumbnail_path, thumbnail_info) { (None, None) => Ok(None), - (Some(thumbnail_url), Some(thumbnail_info)) => { + (Some(thumbnail_path), Some(thumbnail_info)) => { let thumbnail_data = - fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?; + fs::read(thumbnail_path).map_err(|_| RoomError::InvalidThumbnailData)?; let height = thumbnail_info .height @@ -190,7 +190,7 @@ fn build_thumbnail_info( } _ => { - warn!("Ignoring thumbnail because either the thumbnail URL or info isn't defined"); + warn!("Ignoring thumbnail because either the thumbnail path or info isn't defined"); Ok(None) } } @@ -326,14 +326,14 @@ impl Timeline { pub fn send_image( self: Arc, params: UploadParameters, - thumbnail_url: Option, + thumbnail_path: Option, image_info: ImageInfo, progress_watcher: Option>, ) -> Result, RoomError> { let attachment_info = AttachmentInfo::Image( BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?, ); - let thumbnail = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?; + let thumbnail = build_thumbnail_info(thumbnail_path, image_info.thumbnail_info)?; self.send_attachment( params, attachment_info, @@ -346,14 +346,14 @@ impl Timeline { pub fn send_video( self: Arc, params: UploadParameters, - thumbnail_url: Option, + thumbnail_path: Option, video_info: VideoInfo, progress_watcher: Option>, ) -> Result, RoomError> { let attachment_info = AttachmentInfo::Video( BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?, ); - let thumbnail = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?; + let thumbnail = build_thumbnail_info(thumbnail_path, video_info.thumbnail_info)?; self.send_attachment( params, attachment_info, From 34e993435d0b4fa6caf35c128911d5b4e4cb0c52 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 8 Jan 2025 16:51:14 +0100 Subject: [PATCH 884/979] fix(ffi): ensure the log level for `panic` is always set (#4485) If it's present, we just let it untouched. Otherwise, we set it to `error` if it's missing. See code comment explaining why we need this. This makes sure we log panics at the FFI layer, since the `log-panics` crate will use the `panic` target at the error level. --- bindings/matrix-sdk-ffi/src/platform.rs | 49 +++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/platform.rs b/bindings/matrix-sdk-ffi/src/platform.rs index a811474f9f3..009d3c501fb 100644 --- a/bindings/matrix-sdk-ffi/src/platform.rs +++ b/bindings/matrix-sdk-ffi/src/platform.rs @@ -14,8 +14,25 @@ use tracing_subscriber::{ EnvFilter, Layer, }; -pub fn log_panics() { +/// Add panic=error to a filter line, if it's missing from it. +/// +/// Doesn't do anything if the directive is already present. +fn add_panic_to_filter(filter: &mut String) { + if filter.split(',').all(|pair| pair.split('=').next().is_none_or(|lhs| lhs != "panic")) { + if !filter.is_empty() { + filter.push(','); + } + filter.push_str("panic=error"); + } +} + +pub fn log_panics(filter: &mut String) { std::env::set_var("RUST_BACKTRACE", "1"); + + // Make sure that panics will be properly logged. On 2025-01-08, `log_panics` + // uses the `panic` target, at the error log level. + add_panic_to_filter(filter); + log_panics::init(); } @@ -243,11 +260,37 @@ pub struct TracingConfiguration { } #[matrix_sdk_ffi_macros::export] -pub fn setup_tracing(config: TracingConfiguration) { - log_panics(); +pub fn setup_tracing(mut config: TracingConfiguration) { + log_panics(&mut config.filter); tracing_subscriber::registry() .with(EnvFilter::new(&config.filter)) .with(text_layers(config)) .init(); } + +#[cfg(test)] +mod tests { + use super::add_panic_to_filter; + + #[test] + fn test_add_panic_when_not_provided_empty() { + let mut filter = String::from(""); + add_panic_to_filter(&mut filter); + assert_eq!(filter, "panic=error"); + } + + #[test] + fn test_add_panic_when_not_provided_non_empty() { + let mut filter = String::from("a=b,c=d"); + add_panic_to_filter(&mut filter); + assert_eq!(filter, "a=b,c=d,panic=error"); + } + + #[test] + fn test_do_nothing_when_provided() { + let mut filter = String::from("a=b,panic=info,c=d"); + add_panic_to_filter(&mut filter); + assert_eq!(filter, "a=b,panic=info,c=d"); + } +} From 251433382fc41119791483970db10efe3ee94830 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 6 Jan 2025 14:46:52 +0100 Subject: [PATCH 885/979] chore(test): Remove a warning. `owned_user_id` is only used by a test behind the `experimental-sliding-sync` feature flag. --- crates/matrix-sdk/tests/integration/room_preview.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/room_preview.rs b/crates/matrix-sdk/tests/integration/room_preview.rs index 85e887e2e26..71d2dcb8d7a 100644 --- a/crates/matrix-sdk/tests/integration/room_preview.rs +++ b/crates/matrix-sdk/tests/integration/room_preview.rs @@ -8,10 +8,8 @@ use matrix_sdk_test::{ async_test, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, SyncResponseBuilder, }; #[cfg(feature = "experimental-sliding-sync")] -use ruma::{api::client::sync::sync_events::v5::response::Hero, assign}; -use ruma::{ - events::room::member::MembershipState, owned_user_id, room_id, space::SpaceRoomJoinRule, RoomId, -}; +use ruma::{api::client::sync::sync_events::v5::response::Hero, assign, owned_user_id}; +use ruma::{events::room::member::MembershipState, room_id, space::SpaceRoomJoinRule, RoomId}; use serde_json::json; use wiremock::{ matchers::{header, method, path_regex}, From 4e0a6d15cab42b988aadf11358dbb5e6872d3075 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 6 Jan 2025 14:58:26 +0100 Subject: [PATCH 886/979] chore(sdk): Merge imports for the sake of clarity. --- crates/matrix-sdk/src/event_cache/room/events.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 12fdf70f96b..516432304a6 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -15,13 +15,11 @@ use std::cmp::Ordering; use eyeball_im::VectorDiff; +use matrix_sdk_base::event_cache::store::DEFAULT_CHUNK_CAPACITY; pub use matrix_sdk_base::event_cache::{Event, Gap}; -use matrix_sdk_base::{ - event_cache::store::DEFAULT_CHUNK_CAPACITY, - linked_chunk::{AsVector, IterBackward, ObservableUpdates}, -}; use matrix_sdk_common::linked_chunk::{ - Chunk, ChunkIdentifier, EmptyChunk, Error, LinkedChunk, Position, + AsVector, Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, IterBackward, LinkedChunk, + ObservableUpdates, Position, }; use ruma::OwnedEventId; use tracing::{debug, error, warn}; @@ -211,9 +209,7 @@ impl RoomEvents { /// Iterate over the chunks, forward. /// /// The oldest chunk comes first. - pub fn chunks( - &self, - ) -> matrix_sdk_common::linked_chunk::Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> { + pub fn chunks(&self) -> Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> { self.chunks.chunks() } From b8d0384da7291f41295ead8422cc61b949061505 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 6 Jan 2025 17:29:06 +0100 Subject: [PATCH 887/979] refactor: Remove `RoomEventCacheUpdate::AddTimelineEvents`. This patch removes the `AddTimelineEvents` variant from `RoomEventCacheUpdate` since it is replaced by `UpdateTimelineEvents` which shares `VectorDiff`. This patch also tests all uses of `UpdateTimelineEvents` in existing tests. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 17 -- crates/matrix-sdk/src/event_cache/mod.rs | 10 - .../matrix-sdk/src/event_cache/pagination.rs | 7 +- crates/matrix-sdk/src/event_cache/room/mod.rs | 19 +- .../tests/integration/event_cache.rs | 259 +++++++++++++----- 5 files changed, 204 insertions(+), 108 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 622fef4ac2a..b6598c221be 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -290,23 +290,6 @@ impl TimelineBuilder { inner.clear().await; } - // TODO: remove once `UpdateTimelineEvents` is stabilized. - RoomEventCacheUpdate::AddTimelineEvents { events, origin } => { - if !settings_vectordiffs_as_inputs { - trace!("Received new timeline events."); - - inner.add_events_at( - events.into_iter(), - TimelineNewItemPosition::End { - origin: match origin { - EventsOrigin::Sync => RemoteEventOrigin::Sync, - EventsOrigin::Pagination => RemoteEventOrigin::Pagination, - } - } - ).await; - } - } - RoomEventCacheUpdate::UpdateTimelineEvents { diffs, origin } => { if settings_vectordiffs_as_inputs { trace!("Received new timeline events diffs"); diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 0b5b2b0e346..6fbbbb7deaf 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -662,16 +662,6 @@ pub enum RoomEventCacheUpdate { ambiguity_changes: BTreeMap, }, - /// The room has received new timeline events. - // TODO: remove once `UpdateTimelineEvents` is stabilized - AddTimelineEvents { - /// All the new events that have been added to the room's timeline. - events: Vec, - - /// Where the events are coming from. - origin: EventsOrigin, - }, - /// The room has received updates for the timeline as _diffs_. UpdateTimelineEvents { /// Diffs to apply to the timeline. diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index 93a87bc730a..4d135666649 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -165,9 +165,10 @@ impl RoomPagination { None }; + // The new prev token from this pagination. let new_gap = paginator.prev_batch_token().map(|prev_token| Gap { prev_token }); - let (backpagination_outcome, updates_as_vector_diffs) = state + let (backpagination_outcome, sync_timeline_events_diffs) = state .with_events_mut(move |room_events| { // Note: The chunk could be empty. // @@ -231,9 +232,9 @@ impl RoomPagination { }) .await?; - if !updates_as_vector_diffs.is_empty() { + if !sync_timeline_events_diffs.is_empty() { let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents { - diffs: updates_as_vector_diffs, + diffs: sync_timeline_events_diffs, origin: EventsOrigin::Pagination, }); } diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 0ef69e36096..7ad4a6afe2b 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -450,13 +450,12 @@ impl RoomEventCacheInner { let mut all_events = self.all_events.write().await; - for sync_timeline_event in &sync_timeline_events { + for sync_timeline_event in sync_timeline_events { if let Some(event_id) = sync_timeline_event.event_id() { - all_events.append_related_event(sync_timeline_event); - all_events.events.insert( - event_id.to_owned(), - (self.room_id.clone(), sync_timeline_event.clone()), - ); + all_events.append_related_event(&sync_timeline_event); + all_events + .events + .insert(event_id.to_owned(), (self.room_id.clone(), sync_timeline_event)); } } @@ -471,14 +470,6 @@ impl RoomEventCacheInner { // The order of `RoomEventCacheUpdate`s is **really** important here. { - // TODO: remove once `UpdateTimelineEvents` is stabilized. - if !sync_timeline_events.is_empty() { - let _ = self.sender.send(RoomEventCacheUpdate::AddTimelineEvents { - events: sync_timeline_events, - origin: EventsOrigin::Sync, - }); - } - if !sync_timeline_events_diffs.is_empty() { let _ = self.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents { diffs: sync_timeline_events_diffs, diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index 5f9f76782d9..08189f34404 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -1,8 +1,12 @@ -use std::{future::ready, ops::ControlFlow, time::Duration}; +use std::{ + future::ready, + ops::{ControlFlow, Not}, + time::Duration, +}; use assert_matches::assert_matches; use assert_matches2::assert_let; -use futures_util::FutureExt as _; +use eyeball_im::VectorDiff; use matrix_sdk::{ assert_let_timeout, assert_next_matches_with_timeout, deserialized_responses::SyncTimelineEvent, @@ -82,10 +86,10 @@ async fn test_event_cache_receives_events() { // It does receive one update, assert_let_timeout!( - Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() ); - // It does also receive the update as `VectorDiff`. - assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv()); + assert_eq!(diffs.len(), 1); + assert_let!(VectorDiff::Append { values: events } = &diffs[0]); // Which contains the event that was sent beforehand. assert_eq!(events.len(), 1); @@ -170,10 +174,13 @@ async fn test_ignored_unignored() { // We do receive one update, assert_let_timeout!( - Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() ); - // It does also receive the update as `VectorDiff`. - assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv()); + assert_eq!(diffs.len(), 2); + + // Similar to the `RoomEventCacheUpdate::Clear`. + assert_let!(VectorDiff::Clear = &diffs[0]); + assert_let!(VectorDiff::Append { values: events } = &diffs[1]); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "i don't like this dexter"); @@ -195,14 +202,14 @@ async fn wait_for_initial_events( room_stream: &mut broadcast::Receiver, ) { if events.is_empty() { - let mut update = room_stream.recv().await.expect("read error"); + assert_let_timeout!(Ok(update) = room_stream.recv()); + let mut update = update; + // Could be a clear because of the limited timeline. if matches!(update, RoomEventCacheUpdate::Clear) { - update = room_stream.recv().await.expect("read error"); + assert_let_timeout!(Ok(new_update) = room_stream.recv()); + update = new_update; } - assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); - - let update = room_stream.recv().await.expect("read error"); assert_matches!(update, RoomEventCacheUpdate::UpdateTimelineEvents { .. }); } else { @@ -277,15 +284,26 @@ async fn test_backpaginate_once() { let BackPaginationOutcome { events, reached_start } = outcome; assert!(reached_start); + assert_eq!(events.len(), 2); assert_event_matches_msg(&events[0], "world"); assert_event_matches_msg(&events[1], "hello"); - assert_eq!(events.len(), 2); - let next = room_stream.recv().now_or_never(); - assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + // And we get update as diffs. + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + + assert_eq!(diffs.len(), 2); + assert_matches!(&diffs[0], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 0); + assert_event_matches_msg(event, "hello"); + }); + assert_matches!(&diffs[1], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 1); + assert_event_matches_msg(event, "world"); + }); - let next = room_stream.recv().now_or_never(); - assert_matches!(next, None); + assert!(room_stream.is_empty()); } #[async_test] @@ -394,8 +412,36 @@ async fn test_backpaginate_many_times_with_many_iterations() { assert_event_matches_msg(&global_events[2], "oh well"); assert_eq!(global_events.len(), 3); + // First iteration. + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + + assert_eq!(diffs.len(), 2); + assert_matches!(&diffs[0], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 0); + assert_event_matches_msg(event, "hello"); + }); + assert_matches!(&diffs[1], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 1); + assert_event_matches_msg(event, "world"); + }); + + // Second iteration. + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + + assert_eq!(diffs.len(), 1); + assert_matches!(&diffs[0], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 0); + assert_event_matches_msg(event, "oh well"); + }); + + assert!(room_stream.is_empty()); + // And next time I'll open the room, I'll get the events in the right order. - let (events, _receiver) = room_event_cache.subscribe().await.unwrap(); + let (events, room_stream) = room_event_cache.subscribe().await.unwrap(); assert_event_matches_msg(&events[0], "oh well"); assert_event_matches_msg(&events[1], "hello"); @@ -403,14 +449,6 @@ async fn test_backpaginate_many_times_with_many_iterations() { assert_event_matches_msg(&events[3], "heyo"); assert_eq!(events.len(), 4); - // First iteration. - let next = room_stream.recv().now_or_never(); - assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); - - // Second iteration. - let next = room_stream.recv().now_or_never(); - assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); - assert!(room_stream.is_empty()); } @@ -525,8 +563,34 @@ async fn test_backpaginate_many_times_with_one_iteration() { assert_event_matches_msg(&global_events[2], "oh well"); assert_eq!(global_events.len(), 3); + // First pagination. + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + + assert_eq!(diffs.len(), 2); + assert_matches!(&diffs[0], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 0); + assert_event_matches_msg(event, "hello"); + }); + assert_matches!(&diffs[1], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 1); + assert_event_matches_msg(event, "world"); + }); + + // Second pagination. + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + + assert_eq!(diffs.len(), 1); + assert_matches!(&diffs[0], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 0); + assert_event_matches_msg(event, "oh well"); + }); + // And next time I'll open the room, I'll get the events in the right order. - let (events, _receiver) = room_event_cache.subscribe().await.unwrap(); + let (events, room_stream) = room_event_cache.subscribe().await.unwrap(); assert_event_matches_msg(&events[0], "oh well"); assert_event_matches_msg(&events[1], "hello"); @@ -534,14 +598,6 @@ async fn test_backpaginate_many_times_with_one_iteration() { assert_event_matches_msg(&events[3], "heyo"); assert_eq!(events.len(), 4); - // First pagination. - let next = room_stream.recv().now_or_never(); - assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); - - // Second pagination. - let next = room_stream.recv().now_or_never(); - assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); - assert!(room_stream.is_empty()); } @@ -582,7 +638,10 @@ async fn test_reset_while_backpaginating() { // cache (and no room updates will happen in this case), or it hasn't, and // the stream will return the next message soon. if events.is_empty() { - let _ = room_stream.recv().await.expect("read error"); + assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = room_stream.recv()); + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = room_stream.recv() + ); } else { assert_eq!(events.len(), 1); } @@ -679,6 +738,35 @@ async fn test_reset_while_backpaginating() { ); assert!(first_token != second_token); assert_eq!(second_token, "third_backpagination"); + + // Assert the updates as diffs. + + // Being cleared from the reset. + assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = room_stream.recv()); + + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + assert_eq!(diffs.len(), 2); + // The clear, again. + assert_matches!(&diffs[0], VectorDiff::Clear); + // The event from the sync. + assert_matches!(&diffs[1], VectorDiff::Append { values: events } => { + assert_eq!(events.len(), 1); + assert_event_matches_msg(&events[0], "heyo"); + }); + + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + assert_eq!(diffs.len(), 1); + // The event from the pagination. + assert_matches!(&diffs[0], VectorDiff::Insert { index, value: event } => { + assert_eq!(*index, 0); + assert_event_matches_msg(event, "finally!"); + }); + + assert!(room_stream.is_empty()); } #[async_test] @@ -731,8 +819,14 @@ async fn test_backpaginating_without_token() { assert_event_matches_msg(&events[0], "hi"); assert_eq!(events.len(), 1); - let next = room_stream.recv().now_or_never(); - assert_matches!(next, Some(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }))); + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + assert_eq!(diffs.len(), 1); + assert_matches!(&diffs[0], VectorDiff::Append { values: events } => { + assert_eq!(events.len(), 1); + assert_event_matches_msg(&events[0], "hi"); + }); assert!(room_stream.is_empty()); } @@ -785,6 +879,15 @@ async fn test_limited_timeline_resets_pagination() { assert_eq!(events.len(), 1); assert!(reached_start); + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = room_stream.recv() + ); + assert_eq!(diffs.len(), 1); + assert_matches!(&diffs[0], VectorDiff::Append { values: events } => { + assert_eq!(events.len(), 1); + assert_event_matches_msg(&events[0], "hi"); + }); + // And the paginator state delives this as an update, and is internally // consistent with it: assert_next_matches_with_timeout!(pagination_status, PaginatorState::Idle); @@ -794,7 +897,6 @@ async fn test_limited_timeline_resets_pagination() { server.sync_room(&client, JoinedRoomBuilder::new(room_id).set_timeline_limited()).await; // We receive an update about the limited timeline. - assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = room_stream.recv()); assert_let_timeout!(Ok(RoomEventCacheUpdate::Clear) = room_stream.recv()); // The paginator state is reset: status set to Initial, hasn't hit the timeline @@ -839,12 +941,11 @@ async fn test_limited_timeline_with_storage() { // This is racy: either the sync has been handled, or it hasn't yet. if initial_events.is_empty() { assert_let_timeout!( - Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() - ); - // It does also receive the update as `VectorDiff`. - assert_let_timeout!( - Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv() + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() ); + assert_eq!(diffs.len(), 1); + + assert_let!(VectorDiff::Append { values: events } = &diffs[0]); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "hey yo"); } else { @@ -865,10 +966,11 @@ async fn test_limited_timeline_with_storage() { .await; assert_let_timeout!( - Ok(RoomEventCacheUpdate::AddTimelineEvents { events, .. }) = subscriber.recv() + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = subscriber.recv() ); - // It does also receive the update as `VectorDiff`. - assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = subscriber.recv()); + assert_eq!(diffs.len(), 1); + + assert_let!(VectorDiff::Append { values: events } = &diffs[0]); assert_eq!(events.len(), 1); assert_event_matches_msg(&events[0], "gappy!"); @@ -1092,13 +1194,11 @@ async fn test_no_gap_stored_after_deduplicated_sync() { let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); + if events.is_empty() { - let update = stream.recv().await.expect("read error"); - assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); - // It does also receive the update as `VectorDiff`. - let update = stream.recv().await.expect("read error"); - assert_matches!(update, RoomEventCacheUpdate::UpdateTimelineEvents { .. }); + assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = stream.recv()); } + drop(events); // Backpagination will return nothing. @@ -1126,15 +1226,14 @@ async fn test_no_gap_stored_after_deduplicated_sync() { ) .await; - let update = stream.recv().await.expect("read error"); - assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); + assert!(stream.is_empty()); // If this back-pagination fails, that's because we've stored a gap that's // useless. It should be short-circuited because there's no previous gap. let outcome = pagination.run_backwards(20, once).await.unwrap(); assert!(outcome.reached_start); - let (events, _stream) = room_event_cache.subscribe().await.unwrap(); + let (events, stream) = room_event_cache.subscribe().await.unwrap(); assert_event_matches_msg(&events[0], "hello"); assert_event_matches_msg(&events[1], "world"); assert_event_matches_msg(&events[2], "sup"); @@ -1172,13 +1271,11 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap(); let (events, mut stream) = room_event_cache.subscribe().await.unwrap(); + if events.is_empty() { - let update = stream.recv().await.expect("read error"); - assert_matches!(update, RoomEventCacheUpdate::AddTimelineEvents { .. }); - // It does also receive the update as `VectorDiff`. - let update = stream.recv().await.expect("read error"); - assert_matches!(update, RoomEventCacheUpdate::UpdateTimelineEvents { .. }); + assert_let_timeout!(Ok(RoomEventCacheUpdate::UpdateTimelineEvents { .. }) = stream.recv()); } + drop(events); // Now, simulate that we expanded the timeline window with sliding sync, by @@ -1197,6 +1294,25 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { ) .await; + assert_let_timeout!( + Ok(RoomEventCacheUpdate::UpdateTimelineEvents { diffs, .. }) = stream.recv() + ); + assert_eq!(diffs.len(), 2); + + // `$ev3` is duplicated, the older `$ev3` event is removed + assert_matches!(&diffs[0], VectorDiff::Remove { index } => { + assert_eq!(*index, 0); + }); + // `$ev1`, `$ev2` and `$ev3` are added. + assert_matches!(&diffs[1], VectorDiff::Append { values: events } => { + assert_eq!(events.len(), 3); + assert_eq!(events[0].event_id().unwrap().as_str(), "$1"); + assert_eq!(events[1].event_id().unwrap().as_str(), "$2"); + assert_eq!(events[2].event_id().unwrap().as_str(), "$3"); + }); + + assert!(stream.is_empty()); + // For prev-batch2, the back-pagination returns nothing. server .mock_room_messages() @@ -1229,21 +1345,36 @@ async fn test_no_gap_stored_after_deduplicated_backpagination() { // Run pagination once: it will consume prev-batch2 first, which is the most // recent token. - pagination.run_backwards(20, once).await.unwrap(); + let outcome = pagination.run_backwards(20, once).await.unwrap(); + + // The pagination is empty: no new event. + assert!(outcome.reached_start); + assert!(outcome.events.is_empty()); + assert!(stream.is_empty()); // Run pagination a second time: it will consume prev-batch, which is the least // recent token. - pagination.run_backwards(20, once).await.unwrap(); + let outcome = pagination.run_backwards(20, once).await.unwrap(); + + // The pagination contains events, but they are all duplicated; the gap is + // replaced by zero event: nothing happens. + assert!(outcome.reached_start.not()); + assert_eq!(outcome.events.len(), 2); + assert!(stream.is_empty()); // If this back-pagination fails, that's because we've stored a gap that's // useless. It should be short-circuited because storing the previous gap was // useless. let outcome = pagination.run_backwards(20, once).await.unwrap(); assert!(outcome.reached_start); + assert!(outcome.events.is_empty()); + assert!(stream.is_empty()); - let (events, _stream) = room_event_cache.subscribe().await.unwrap(); + let (events, stream) = room_event_cache.subscribe().await.unwrap(); assert_event_matches_msg(&events[0], "hello"); assert_event_matches_msg(&events[1], "world"); assert_event_matches_msg(&events[2], "sup"); assert_eq!(events.len(), 3); + + assert!(stream.is_empty()); } From 14d0cc1935b81c5f4b470aa2236515a273f05d0a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 7 Jan 2025 13:21:07 +0100 Subject: [PATCH 888/979] refactor(ui): Remove `TimelineSettings::vectordiffs_as_inputs`. From now on, this patch considers that `VectorDiff`s are the official input type for the `Timeline`, via `RoomEventCacheUpdate` (notably `::UpdateTimelineEvents`). This patch removes `TimelineSettings::vectordiffs_as_inputs`. It thus removes all deduplication logics, as it is supposed to be managed by the `EventCache` itself. --- crates/matrix-sdk-ui/src/timeline/builder.rs | 41 ++++---------- .../src/timeline/controller/mod.rs | 9 ---- .../src/timeline/controller/state.rs | 34 +----------- .../src/timeline/event_handler.rs | 53 +------------------ .../src/timeline/event_item/content/mod.rs | 1 + .../matrix-sdk-ui/src/timeline/pagination.rs | 12 ----- 6 files changed, 16 insertions(+), 134 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index b6598c221be..c5f02b028ed 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -31,10 +31,7 @@ use super::{ to_device::{handle_forwarded_room_key_event, handle_room_key_event}, DateDividerMode, Error, Timeline, TimelineDropHandle, TimelineFocus, }; -use crate::{ - timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin}, - unable_to_decrypt_hook::UtdHookManager, -}; +use crate::{timeline::event_item::RemoteEventOrigin, unable_to_decrypt_hook::UtdHookManager}; /// Builder that allows creating and configuring various parts of a /// [`Timeline`]. @@ -145,14 +142,6 @@ impl TimelineBuilder { self } - /// Use `VectorDiff`s as the new “input mechanism” for the `Timeline`. - /// - /// Read `TimelineSettings::vectordiffs_as_inputs` to learn more. - pub fn with_vectordiffs_as_inputs(mut self) -> Self { - self.settings.vectordiffs_as_inputs = true; - self - } - /// Create a [`Timeline`] with the options set on this builder. #[tracing::instrument( skip(self), @@ -162,17 +151,11 @@ impl TimelineBuilder { ) )] pub async fn build(self) -> Result { - let Self { room, mut settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self; + let Self { room, settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self; let client = room.client(); let event_cache = client.event_cache(); - // Enable `TimelineSettings::vectordiffs_as_inputs` if and only if the event - // cache storage is enabled. - settings.vectordiffs_as_inputs = event_cache.has_storage(); - - let settings_vectordiffs_as_inputs = settings.vectordiffs_as_inputs; - // Subscribe the event cache to sync responses, in case we hadn't done it yet. event_cache.subscribe()?; @@ -291,17 +274,15 @@ impl TimelineBuilder { } RoomEventCacheUpdate::UpdateTimelineEvents { diffs, origin } => { - if settings_vectordiffs_as_inputs { - trace!("Received new timeline events diffs"); - - inner.handle_remote_events_with_diffs( - diffs, - match origin { - EventsOrigin::Sync => RemoteEventOrigin::Sync, - EventsOrigin::Pagination => RemoteEventOrigin::Pagination, - } - ).await; - } + trace!("Received new timeline events diffs"); + + inner.handle_remote_events_with_diffs( + diffs, + match origin { + EventsOrigin::Sync => RemoteEventOrigin::Sync, + EventsOrigin::Pagination => RemoteEventOrigin::Pagination, + } + ).await; } RoomEventCacheUpdate::AddEphemeralEvents { events } => { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 36409f66222..faa4305ea7e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -141,12 +141,6 @@ pub(super) struct TimelineSettings { /// Should the timeline items be grouped by day or month? pub(super) date_divider_mode: DateDividerMode, - - /// Whether `VectorDiff` is the “input mechanism” to use. - /// - /// This mechanism will replace the existing one, but this runtime feature - /// flag is necessary for the transition and the testing phase. - pub(super) vectordiffs_as_inputs: bool, } #[cfg(not(tarpaulin_include))] @@ -155,7 +149,6 @@ impl fmt::Debug for TimelineSettings { f.debug_struct("TimelineSettings") .field("track_read_receipts", &self.track_read_receipts) .field("add_failed_to_parse", &self.add_failed_to_parse) - .field("vectordiffs_as_inputs", &self.vectordiffs_as_inputs) .finish_non_exhaustive() } } @@ -167,7 +160,6 @@ impl Default for TimelineSettings { event_filter: Arc::new(default_event_filter), add_failed_to_parse: true, date_divider_mode: DateDividerMode::Daily, - vectordiffs_as_inputs: false, } } } @@ -789,7 +781,6 @@ impl TimelineController